// Hero variants. Default: animated node graph. Also: minimal, code-block, scroll-pile.

function Hero({ variant = "node" }) {
  const N = window.NODESTONE;
  const h = N.hero;
  const go = (e, p) => { e.preventDefault(); window.NodestoneNavigate(p); };

  return (
    <section className="hero" data-comment-anchor="9b5f5342cc-section-9-5">
      {variant === "node" && <NodeGraph intensity="subtle" />}
      {variant === "code" && <CodeHeroDecor />}

      <div className="wrap">
        <Reveal>
          <div className="eyebrow hero-eyebrow">{h.eyebrow}</div>
        </Reveal>
        <Reveal delay={80}>
          <h1 className="hero-title">{h.title}</h1>
        </Reveal>
        <Reveal delay={160}>
          <p className="lede hero-sub">{h.subtitle}</p>
        </Reveal>
        <Reveal delay={240}>
          <div className="hero-ctas">
            <a href={h.primaryCta.path} className="btn btn-primary" onClick={(e) => go(e, h.primaryCta.path)}>
              {h.primaryCta.label} <span className="arr">→</span>
            </a>
            <a href={h.secondaryCta.path} className="btn btn-ghost" onClick={(e) => go(e, h.secondaryCta.path)}>
              {h.secondaryCta.label}
            </a>
          </div>
        </Reveal>
        <Reveal delay={320}>
          <ul className="hero-bullets">
            {h.bullets.map((b, i) => <li key={i}>{b}</li>)}
          </ul>
        </Reveal>
      </div>
    </section>);

}

function CodeHeroDecor() {
  return (
    <div className="hero-canvas-wrap" aria-hidden="true" style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", paddingRight: "8%" }}>
      <pre style={{
        fontFamily: "var(--font-mono)",
        fontSize: 13,
        color: "var(--fg-3)",
        background: "transparent",
        margin: 0,
        opacity: 0.7,
        lineHeight: 1.6
      }}>{`$ nodestone diagnose --target platform
✓ scanning cloud estate ............. 248 resources
✓ mapping dependencies .............. 31 services
✓ identifying drift ................. 12 deltas
✓ delivery pipeline review .......... 4 risks
✓ shipping the report.

→ next: stabilise, then build the right thing.`}</pre>
    </div>);

}

// ────────────────────────────────────────────────────────────────────────────
// Reveal — IntersectionObserver scroll-in
// ────────────────────────────────────────────────────────────────────────────
function Reveal({ children, delay = 0, stagger = false, as: Tag = "div", className = "", ...rest }) {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      ([e]) => {
        if (e.isIntersecting) {
          setSeen(true);
          io.disconnect();
        }
      },
      { rootMargin: "-40px 0px -10% 0px", threshold: 0.05 }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  const base = stagger ? "reveal-stagger" : "reveal";
  return (
    <Tag
      ref={ref}
      className={`${base} ${seen ? "in" : ""} ${className}`}
      style={{ transitionDelay: delay ? delay + "ms" : undefined }}
      {...rest}>
      
      {children}
    </Tag>);

}

// ────────────────────────────────────────────────────────────────────────────
// HOME
// ────────────────────────────────────────────────────────────────────────────
function HomePage({ heroVariant }) {
  const N = window.NODESTONE;
  return (
    <>
      <Hero variant={heroVariant} />

      {/* Problems we fix — pastel 1 */}
      <section className="block pastel-1" data-comment-anchor="72ed5e5073-section-110-7">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>{N.problems.eyebrow}</div>
            <h2 style={{ maxWidth: "20ch", marginBottom: 24 }}>{N.problems.title}</h2>
            {N.problems.intro && <p className="lede" style={{ marginBottom: 64 }}>{N.problems.intro}</p>}
          </Reveal>
          <Reveal stagger className="grid-3">
            {N.problems.items.map((p) =>
            <div className="card" key={p.n}>
                <div className="card-icon"><Icon name={p.icon} /></div>
                <h3>{p.title}</h3>
                <p>{p.body}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      {/* Services overview — neutral */}
      <section className="block">
        <div className="wrap">
          <Reveal>
            <h2 style={{ maxWidth: "20ch", marginBottom: 24 }}>What we specialise in.</h2>
            <p className="lede" style={{ marginBottom: 56 }}>
              From the infrastructure nobody sees, to the software people use every day — and everything that holds them together.
            </p>
          </Reveal>
          <Reveal>
            <div>
              {(() => {
                const FEATURED = ["infrastructure-automation", "saas-product", "application-development"];
                return FEATURED.map((slug, i) => {
                  const s = N.services.find((x) => x.slug === slug);
                  if (!s) return null;
                  return (
                    <a key={s.slug} href={"/services/" + s.slug} className="service-row"
                    onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/services/" + s.slug);}}>
                      <span className="mono">{String(i + 1).padStart(2, "0")}</span>
                      <div>
                        <h3 style={{ marginBottom: 6 }}>{s.label}</h3>
                        <p>{s.short}</p>
                      </div>
                      <span className="arr">→</span>
                    </a>);

                });
              })()}
            </div>
          </Reveal>
          <Reveal>
            <div style={{ marginTop: 32, display: "flex", gap: 12, flexWrap: "wrap", alignItems: "center" }}>
              <a href="/services" className="btn btn-ghost"
              onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/services");}}>
                See all services <span className="arr">→</span>
              </a>
            </div>
          </Reveal>
        </div>
      </section>

      {/* How we work — pastel 3 (lavender) */}
      <section className="block pastel-3">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>{N.process.eyebrow}</div>
            <h2 style={{ maxWidth: "22ch", marginBottom: 64 }}>{N.process.title}</h2>
          </Reveal>
          <Reveal stagger className="grid-4">
            {N.process.steps.map((s) =>
            <div className="card" key={s.n}>
                <div className="card-icon"><Icon name={s.icon} /></div>
                <h3>{s.title}</h3>
                <p>{s.body}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      {/* Flexible / modes — neutral */}
      <section className="block">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>{N.modes.eyebrow}</div>
            <h2 style={{ maxWidth: "22ch", marginBottom: 64 }}>{N.modes.title}</h2>
          </Reveal>
          <Reveal stagger className="grid-3">
            {N.modes.items.map((m) =>
            <div className="card" key={m.kind}>
                <div className="card-icon"><Icon name={m.icon} /></div>
                <h3 style={{ fontSize: 20 }}>{m.kind}</h3>
                <p style={{ color: "var(--fg-3)", fontSize: 15, lineHeight: 1.55 }}>{m.body}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      {/* Products preview — pastel 2 (sage) */}
      <section className="block pastel-2">
        <div className="wrap">
          <Reveal>
            <h2 style={{ maxWidth: "22ch", marginBottom: 24 }}>{N.products.title}</h2>
            <p className="lede" style={{ marginBottom: 56 }}>{N.products.intro}</p>
          </Reveal>
          <Reveal stagger className="grid-2">
            {N.products.items.map((p) =>
            <div className="workshop-card" key={p.name}>
                <span className="status">{p.status}</span>
                <h3 style={{ fontSize: 26, letterSpacing: "-0.024em" }}>{p.name}</h3>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                  {p.kind.map((k) => <span key={k} className="label-pill">{k}</span>)}
                </div>
                <p style={{ color: "var(--fg-3)", fontSize: 15 }}>{p.body}</p>
              </div>
            )}
          </Reveal>
          <Reveal>
            <div style={{ marginTop: 40 }}>
              <a href="/workshop" className="btn btn-ghost"
              onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/workshop");}}>
                See the workshop <span className="arr">→</span>
              </a>
            </div>
          </Reveal>
        </div>
      </section>

      <ClosingCta />
    </>);

}

// ────────────────────────────────────────────────────────────────────────────
// SERVICES OVERVIEW
// ────────────────────────────────────────────────────────────────────────────
function ServicesPage() {
  const N = window.NODESTONE;
  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <h1>Cloud, platforms, applications and integration. Done properly.</h1>
            <p className="lede">
              We help businesses untangle the parts of their technology that matter most. Platforms, infrastructure, delivery pipelines, applications, integrations — and the operating habits around them.
            </p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal>
            <div className="divider-mono">What we can do</div>
          </Reveal>
          <Reveal>
            <div>
              {N.services.map((s, i) =>
              <a key={s.slug} href={"/services/" + s.slug} className="service-row"
              onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/services/" + s.slug);}}>
                  <span className="mono">{String(i + 1).padStart(2, "0")}</span>
                  <div>
                    <h3 style={{ marginBottom: 6 }}>{s.label}</h3>
                    <p>{s.long}</p>
                  </div>
                  <span className="arr">→</span>
                </a>
              )}
            </div>
          </Reveal>

          <Reveal>
            <a href="/contact" className="service-cta"
            onClick={(e) => {e.preventDefault();sessionStorage.setItem('contactPreselect','other');window.NodestoneNavigate("/contact");}}>
              <div className="service-cta-body" data-comment-anchor="31897ee9f7-div-292-15">
                <h3 className="service-cta-title">Don't see what you need?<br />Let's chat anyway.</h3>
                <p className="service-cta-copy">
                  If you need something that isn't shown above, start a conversation with us anyway. We're happy to listen, give an honest first read, and point you in the right direction — even if that direction isn't us.
                </p>
              </div>
              <div className="service-cta-action">
                <span>Start a conversation</span>
                <span className="arr">→</span>
              </div>
            </a>
          </Reveal>
        </div>
      </section>

      {/* problems framing */}
      <section className="block pastel-4">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>How we can help</div>
            <h2 style={{ maxWidth: "22ch", marginBottom: 48 }}>Most conversations start with one of these problems.</h2>
          </Reveal>
          <Reveal stagger className="grid-3">
            {N.problems.items.map((p) =>
            <div className="card" key={p.n}>
                <div className="card-icon"><Icon name={p.icon} /></div>
                <h3>{p.title}</h3>
                <p>{p.body}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      {/* engagement modes */}
      <section className="block">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>Flexible by design</div>
            <h2 style={{ maxWidth: "22ch", marginBottom: 48 }}>Shaped around what you need.</h2>
          </Reveal>
          <Reveal stagger className="grid-3">
            {N.modes.items.map((m) =>
            <div className="card" key={m.kind}>
                <div className="card-icon"><Icon name={m.icon} /></div>
                <h3 style={{ fontSize: 20 }}>{m.kind}</h3>
                <p style={{ color: "var(--fg-3)", fontSize: 15, lineHeight: 1.55 }}>{m.body}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      <ClosingCta />
    </>);

}

// ────────────────────────────────────────────────────────────────────────────
// SERVICE DETAIL (deep page — for cloud-migration and devops-platform)
// ────────────────────────────────────────────────────────────────────────────
const SERVICE_DETAILS = {
  "cloud-migration": {
    eyebrow: "Service · 01",
    title: "Cloud migration built for the long run.",
    lede: "Most migrations finish, then quietly start to drift. We plan for running it, not just moving it.",
    sections: [
    {
      h: "What we do",
      body:
      "Assess the infrastructure. Map dependencies — including the awkward ones. Plan the move with visible risk and a clear order of operations. Build cloud platforms that are secure, easy to monitor and manageable after the project closes."
    },
    {
      h: "How it usually starts",
      body:
      "An ageing on-premise setup, a half-finished migration, or a cloud bill that nobody fully understands. Sometimes the migration plan exists; it just hasn't survived contact with the business."
    },
    {
      h: "What you get",
      body:
      "A migration roadmap your engineers actually believe. A secure, well-structured cloud environment that holds up under audit. A delivery cadence that doesn't pause the rest of the business."
    },
    {
      h: "What we don't do",
      body:
      "Eight-week discovery phases that produce a deck. Migrations that finish on a slide and break in production. Cloud architectures that need a team of specialists just to keep the lights on."
    }],

    deliverables: [
    "Infrastructure assessment & dependency map",
    "Target platform architecture",
    "Cloud environment setup (AWS / Azure / GCP)",
    "Migration waves & risk register",
    "Monitoring, alerting & cost management baseline",
    "Operations guide & handover"],

    stack: ["AWS", "Azure", "GCP", "Terraform", "Kubernetes", "Docker", "Ansible", "Helm", "Prometheus", "Datadog", "GitHub Actions"]
  },
  "devops-platform": {
    eyebrow: "Service · 02",
    title: "Delivery pipelines and platform engineering, without the complexity.",
    lede: "We treat the platform as a product, with internal users and a roadmap of its own. The point is calmer releases and quicker recovery — not a wall of dashboards.",
    sections: [
    {
      h: "What we do",
      body:
      "Build delivery pipelines, environments and platform patterns that help teams release calmly and recover quickly. Codify the path from idea to production, so the next change doesn't need a working group to ship."
    },
    {
      h: "How it usually starts",
      body:
      "Deployments that turn into a whole-team event. Environments that drift between deployments. A backlog of platform tickets that everyone routes around. We pick the highest-leverage thread and pull."
    },
    {
      h: "What you get",
      body:
      "Pipelines your team owns. Environments that match production within tolerance. Monitoring that surfaces the right alerts, not all of them. Documented patterns the next hire can read on day one."
    },
    {
      h: "What we don't do",
      body:
      "Platform rebuilds that take a year before anyone benefits. Tooling collections without an operating model. Maturity assessments without a route forward."
    }],

    deliverables: [
    "Automated pipeline design & implementation",
    "Environment & release pattern reset",
    "Internal developer platform baseline",
    "Observability & on-call practices",
    "Service catalogue & standard deployment patterns",
    "Team enablement & ownership transfer"],

    stack: ["GitHub Actions", "GitLab", "Jenkins", "CircleCI", "Terraform", "Kubernetes", "Docker", "Helm", "ArgoCD", "Prometheus", "Grafana", "Datadog", "Elasticsearch", "HashiCorp Vault", "Nginx", "Backstage", "Azure DevOps"]
  },
  "infrastructure-automation": {
    eyebrow: "Service · 03",
    title: "Infrastructure that runs itself. Not you.",
    lede: "Manual steps create risk. We replace them with repeatable, documented automation that holds up when the person who built it isn't around.",
    sections: [
    {
      h: "What we do",
      body: "Audit the current infrastructure, identify fragile manual processes and build automation that's properly documented and maintainable. Provisioning, configuration, patching, deployments — made repeatable."
    },
    {
      h: "How it usually starts",
      body: "A new joiner asking how to set up the environment. A deployment that only one person knows how to run. An audit that surfaces inconsistencies nobody can explain."
    },
    {
      h: "What you get",
      body: "Infrastructure as code across the environment. Drift detection so you know when things wander. Runbooks your team can actually follow — not just the person who wrote them."
    },
    {
      h: "What we don't do",
      body: "Automation for the sake of it. Toolchains that need a specialist to operate. Scripts that work once and break silently."
    }],
    deliverables: [
    "Infrastructure audit & gap analysis",
    "Infrastructure as code (Terraform / Ansible)",
    "Automated provisioning pipelines",
    "Drift detection setup",
    "Runbooks & operating documentation",
    "Team handover & enablement"],
    stack: ["Terraform", "Ansible", "Puppet", "GitHub Actions", "AWS", "Azure", "GCP", "Kubernetes", "Docker", "Python", "PowerShell", "Prometheus", "Datadog", "HashiCorp Vault", "Packer"]
  },
  "digital-transformation": {
    eyebrow: "Service · 04",
    title: "Better work. Not louder language.",
    lede: "Transformation that produces a strategy deck and calls it done isn't transformation. We focus on how work actually gets done — and make targeted improvements that hold.",
    sections: [
    {
      h: "What we do",
      body: "Assess how work moves through the business, identify where technology helps and where it gets in the way, and make changes the team can sustain. Platforms, processes and people — considered together."
    },
    {
      h: "How it usually starts",
      body: "A business that's changed but whose tools haven't kept up. Processes built around workarounds. Technology that drives the team, rather than the other way around."
    },
    {
      h: "What you get",
      body: "A clear picture of where change will have the most impact. A practical roadmap — phased, prioritised and honest about effort. Improvements the team understands and can own."
    },
    {
      h: "What we don't do",
      body: "Multi-year programmes with uncertain outcomes. Change for the sake of modernisation. Recommendations that ignore the people who'll live with them."
    }],
    deliverables: [
    "Current-state process assessment",
    "Opportunity & priority map",
    "Phased transformation roadmap",
    "Platform selection & vendor guidance",
    "Adoption approach & change support",
    "Progress reviews & course correction"],
    stack: ["Confluence", "Jira", "Notion", "Slack", "SharePoint", "Microsoft 365", "Power Platform", "Dynamics 365", "Salesforce", "HubSpot", "ServiceNow"]
  },
  "application-development": {
    eyebrow: "Service · 05",
    title: "Applications built around how the business actually works.",
    lede: "Off-the-shelf doesn't always fit. We design and build applications around the real workflow — with the people who'll use them in mind from the start.",
    sections: [
    {
      h: "What we do",
      body: "Design, build and iterate on business applications — from internal tools to customer-facing products. We start with the workflow the business actually runs, not the idealised version in the requirements doc."
    },
    {
      h: "How it usually starts",
      body: "A spreadsheet that's become load-bearing. An off-the-shelf tool that doesn't quite fit. An application that needs rebuilding because the original is no longer maintainable."
    },
    {
      h: "What you get",
      body: "An application that fits the workflow, not the other way around. Clean, maintainable code with documentation that doesn't go stale. A team that understands what they're running — not just how to use it."
    },
    {
      h: "What we don't do",
      body: "Feature factories without a clear outcome. Applications that can only be maintained by whoever built them. Over-engineered solutions to straightforward problems."
    }],
    deliverables: [
    "Requirements & workflow mapping",
    "Application architecture & design",
    "Frontend & backend development",
    "Integration with existing systems",
    "Testing & quality assurance",
    "Documentation & handover"],
    stack: ["React", "Next.js", "Node.js", "Python", "Django", "TypeScript", "PostgreSQL", "MySQL", "MongoDB", "Redis", "REST APIs", "GraphQL", "Docker", "AWS", "GitHub Actions", "Flutter", "React Native", "Swift", "Kotlin"]
  },
  "saas-product": {
    eyebrow: "Service · 06",
    title: "Software products built to ship and scale.",
    lede: "We've built our own. We know where the scope creeps, where the architecture decisions bite back, and where cutting corners costs twice as much later.",
    sections: [
    {
      h: "What we do",
      body: "Shape, build and operate software products from the ground up — end to end. Product thinking, architecture, delivery and the operating habits that keep it running well after launch."
    },
    {
      h: "How it usually starts",
      body: "A validated idea that hasn't been built yet. A prototype that needs to become a real product. Something that was built fast and now needs to be built properly."
    },
    {
      h: "What you get",
      body: "A product with sensible architecture and a delivery process to match. Decisions documented so the next person isn't starting from scratch. A roadmap that lives somewhere other than a slide."
    },
    {
      h: "What we don't do",
      body: "Endless discovery phases. Scope that grows without a conversation about trade-offs. Products handed over without the team knowing how to run them."
    }],
    deliverables: [
    "Product scoping & architecture",
    "Frontend & backend development",
    "Infrastructure & deployment setup",
    "Billing & subscription integration",
    "Observability & alerting baseline",
    "Launch support & roadmap handover"],
    stack: ["React", "Next.js", "Node.js", "TypeScript", "PostgreSQL", "Redis", "Stripe", "Auth0", "AWS", "Terraform", "Docker", "Kubernetes", "GitHub Actions", "Datadog"]
  },
  "systems-integration": {
    eyebrow: "Service · 07",
    title: "Connections that actually hold.",
    lede: "When systems don't talk to each other, people fill the gap — with exports, copy-paste and inboxes. We remove the gap.",
    sections: [
    {
      h: "What we do",
      body: "Design and build integrations between platforms, data sources and processes. Clean connections, reliable event flows and documented patterns — so it holds up, not just on day one."
    },
    {
      h: "How it usually starts",
      body: "Data that lives in two places and needs to be in one. A report someone manually compiles every week. A new platform that needs to work with the five that came before it."
    },
    {
      h: "What you get",
      body: "Integrations that are reliable, monitored and documented. Data that flows without manual intervention. A clear picture of what connects to what — and what to do when something breaks."
    },
    {
      h: "What we don't do",
      body: "Point-to-point connections that become unmaintainable. Integrations without error handling or monitoring. Solutions that only the person who built them understands."
    }],
    deliverables: [
    "Integration mapping & dependency review",
    "API design & implementation",
    "Event-driven patterns where appropriate",
    "Error handling & retry logic",
    "Monitoring & alerting setup",
    "Integration documentation & runbook"],
    stack: ["REST APIs", "GraphQL", "Webhooks", "ETL", "AWS EventBridge", "AWS SNS", "AWS Lambda", "Node.js", "PostgreSQL", "MySQL", "Kafka", "RabbitMQ", "Azure Service Bus", "MuleSoft", "Apache Camel", "Zapier", "Make", "Datadog"]
  }
};

function ServiceDetailPage({ slug }) {
  const N = window.NODESTONE;
  const service = N.services.find((s) => s.slug === slug);
  const detail = SERVICE_DETAILS[slug];

  if (!service) {
    return (
      <section className="page-hero">
        <div className="wrap">
          <div className="eyebrow" style={{ marginBottom: 24 }}>Service not found</div>
          <h1>That service doesn't exist (yet).</h1>
          <p className="lede">Have a look at <a href="/services" onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/services");}} style={{ borderBottom: "1px solid var(--line-strong)" }}>everything we do</a>.</p>
        </div>
      </section>);

  }

  // If no deep detail authored, fall back to short page
  if (!detail) {
    return (
      <>
        <section className="page-hero">
          <div className="wrap">
            <Reveal>
              <div className="eyebrow" style={{ marginBottom: 24 }}>Service</div>
              <h1>{service.label}.</h1>
              <p className="lede">{service.long}</p>
            </Reveal>
          </div>
        </section>
        <section className="block pastel-5">
          <div className="wrap">
            <Reveal>
              <h2 style={{ maxWidth: "20ch", marginBottom: 24 }}>Want the deep version?</h2>
              <p className="lede" style={{ marginBottom: 32 }}>
                Detailed scope, deliverables and engagement shape — sent across the same afternoon you ask.
              </p>
              <a href="/contact" className="btn btn-primary"
              onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/contact");}}>
                Get the deep brief <span className="arr">→</span>
              </a>
            </Reveal>
          </div>
        </section>
        <ClosingCta serviceSlug={slug} />
      </>);

  }

  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 24 }}>{detail.eyebrow}</div>
            <h1>{detail.title}</h1>
            <p className="lede">{detail.lede}</p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <div style={{ display: "grid", gridTemplateColumns: "1fr 2fr", gap: 64 }} className="detail-grid">
            <Reveal>
              <div style={{ position: "sticky", top: 120 }}>
                <div className="mono" style={{ color: "var(--fg-4)", fontSize: 11, letterSpacing: ".08em", textTransform: "uppercase", marginBottom: 12 }}>
                  At a glance
                </div>
                <h3 style={{ fontSize: 20, fontWeight: 500, marginBottom: 12 }}>{service.label}</h3>
                <p style={{ color: "var(--fg-3)", fontSize: 14.5 }}>{service.short}</p>
                <div style={{ marginTop: 24, display: "flex", flexWrap: "wrap", gap: 6 }}>
                  {detail.stack.map((t) =>
                  <span key={t} className="kbd" style={{ fontSize: 11 }}>{t}</span>
                  )}
                </div>
              </div>
            </Reveal>

            <div style={{ display: "flex", flexDirection: "column", gap: 48 }}>
              {detail.sections.map((sec, i) =>
              <Reveal key={i}>
                  <div>
                    <h3 style={{ fontSize: 26, fontWeight: 500, letterSpacing: "-0.024em", marginBottom: 16 }}>{sec.h}</h3>
                    <p style={{ color: "var(--fg-2)", fontSize: 17, lineHeight: 1.6, maxWidth: "62ch" }}>{sec.body}</p>
                  </div>
                </Reveal>
              )}
            </div>
          </div>

          <style>{`
            @media (max-width: 820px) {
              .detail-grid { grid-template-columns: 1fr !important; gap: 32px !important; }
            }
          `}</style>
        </div>
      </section>

      <section className="block pastel-3">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>Deliverables</div>
            <h2 style={{ maxWidth: "20ch", marginBottom: 56 }}>What ends up in your hands.</h2>
          </Reveal>
          <Reveal stagger style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 16 }}>
            {detail.deliverables.map((d, i) =>
            <div key={i} style={{
              display: "flex", gap: 16, alignItems: "baseline",
              padding: "18px 20px", borderRadius: "var(--radius)",
              background: "color-mix(in oklab, var(--bg-elev) 80%, transparent)",
              border: "1px solid var(--line)"
            }}>
                <span className="mono" style={{ color: "var(--fg-4)", fontSize: 12 }}>
                  {String(i + 1).padStart(2, "0")}
                </span>
                <span style={{ fontSize: 15.5 }}>{d}</span>
              </div>
            )}
          </Reveal>
          <style>{`
            @media (max-width: 720px) {
              .pastel-3 .reveal-stagger { grid-template-columns: 1fr !important; }
            }
          `}</style>
        </div>
      </section>

      {/* Other services nav */}
      <section className="block-tight">
        <div className="wrap">
          <Reveal>
            <div className="divider-mono">Other services</div>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 12 }}>
              {N.services.filter((s) => s.slug !== slug).slice(0, 4).map((s) =>
              <a key={s.slug} href={"/services/" + s.slug}
              onClick={(e) => {e.preventDefault();window.NodestoneNavigate("/services/" + s.slug);}}
              className="card" style={{ padding: 20, gap: 6 }}>
                  <h3 style={{ fontSize: 17 }}>{s.label}</h3>
                  <p style={{ fontSize: 13.5, color: "var(--fg-3)" }}>{s.short}</p>
                  <span className="mono" style={{ marginTop: 8, fontSize: 11, color: "var(--fg-4)" }}>Read more →</span>
                </a>
              )}
            </div>
          </Reveal>
        </div>
      </section>

      <ClosingCta serviceSlug={slug} />
    </>);

}

// ────────────────────────────────────────────────────────────────────────────
// ABOUT
// ────────────────────────────────────────────────────────────────────────────
function AboutPage() {
  const N = window.NODESTONE;
  const a = N.about;
  return (
    <>
      <section className="page-hero" style={{ paddingBottom: 40 }}>
        <div className="wrap">
          <Reveal>
            <h1>{a.title}</h1>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal>
            <div style={{ display: "flex", flexDirection: "column", gap: 28, maxWidth: "62ch", fontSize: 18, lineHeight: 1.6, color: "var(--fg-2)" }}>
              {a.paragraphs.map((p, i) => <p key={i}>{p}</p>)}
            </div>
          </Reveal>
        </div>
      </section>

      <section className="block pastel-5">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>What we believe</div>
            <h2 style={{ maxWidth: "22ch", marginBottom: 56 }}>Four habits we won't compromise on.</h2>
          </Reveal>
          <Reveal stagger className="grid-4">
            {a.pillars.map((p) =>
            <div className="card" key={p.label}>
                <div className="card-icon"><Icon name={p.icon} /></div>
                <h3 style={{ fontSize: 19, fontWeight: 500 }}>{p.label}</h3>
                <p style={{ fontSize: 14.5 }}>{p.body}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      {/* photos-coming-later placeholder area */}
      <section className="block">
        <div className="wrap">
          <Reveal>
            <div className="divider-mono">Team · photos coming soon</div>
          </Reveal>
          <Reveal stagger style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 20 }}>
            {[1, 2, 3].map((i) =>
            <div className="img-placeholder" key={i} style={{ aspectRatio: "3 / 4" }}>
                portrait · slot {i}
              </div>
            )}
          </Reveal>
          <style>{`@media (max-width:720px){.reveal-stagger{grid-template-columns:1fr 1fr !important}}`}</style>
        </div>
      </section>

      <ClosingCta />
    </>);

}

// ────────────────────────────────────────────────────────────────────────────
// PRODUCTS
// ────────────────────────────────────────────────────────────────────────────
function ProductsPage() {
  const N = window.NODESTONE;
  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <h1>{N.products.title}</h1>
            <p className="lede">{N.products.intro}</p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal stagger className="grid-2">
            {N.products.items.map((p) =>
            <div className="workshop-card" key={p.name} style={{ padding: 40 }}>
                <span className="status">{p.status}</span>
                <h3 style={{ fontSize: 32, letterSpacing: "-0.028em", lineHeight: 1.1 }}>{p.name}</h3>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                  {p.kind.map((k) => <span key={k} className="label-pill">{k}</span>)}
                </div>
                <p style={{ color: "var(--fg-2)", fontSize: 16, lineHeight: 1.55 }}>{p.body}</p>
                <div className="img-placeholder" style={{ aspectRatio: "16 / 9", marginTop: 16 }}>
                  product visual · coming
                </div>
              </div>
            )}
          </Reveal>
        </div>
      </section>

      <section className="block pastel-6">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>Other things on the bench</div>
            <h2 style={{ maxWidth: "22ch", marginBottom: 32 }}>Quiet experiments. Some will become products. Some won't.</h2>
            <p className="lede" style={{ marginBottom: 48 }}>
              When we keep seeing the same gap in client work, we tend to prototype the thing we wish existed. Most stay internal tools. A few earn a name and a roadmap.
            </p>
          </Reveal>
          <Reveal stagger style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 16 }}>
            {[
            { k: ["Plugin", "Idea"], icon: "calendarClock", t: "Trello Calendar Plugin", b: "A plugin that rethinks Trello's calendar view — cleaner layout, less noise, easier to see what actually needs attention and when." },
            { k: ["API", "Idea"], icon: "route", t: "Working Days API", b: "A lightweight API for UK working day and bank holiday lookups — the kind of utility that shouldn't need a third-party subscription or a scraping script." },
            { k: ["App", "Idea"], icon: "book", t: "Brief builder", b: "A simple web app for shaping project briefs. Structured questions, clean output, less back-and-forth before work begins." }].
            map((x) =>
            <div className="card" key={x.t}>
                <div className="card-icon"><Icon name={x.icon} /></div>
                <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
                  {x.k.map((k) => <span key={k} className="label-pill">{k}</span>)}
                </div>
                <h3 style={{ fontSize: 19 }}>{x.t}</h3>
                <p style={{ fontSize: 14.5 }}>{x.b}</p>
              </div>
            )}
          </Reveal>
          <style>{`@media (max-width:820px){.pastel-6 .reveal-stagger{grid-template-columns:1fr 1fr !important}} @media (max-width:520px){.pastel-6 .reveal-stagger{grid-template-columns:1fr !important}}`}</style>
        </div>
      </section>

      <ClosingCta />
  </>);

}

// ────────────────────────────────────────────────────────────────────────────
// LEGAL
// ────────────────────────────────────────────────────────────────────────────
const LEGAL_UPDATED = "25 May 2026";
const TERMS_UPDATED = "26 May 2026";

function LegalSection({ id, title, children }) {
  return (
    <section id={id} className="legal-section">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

function PrivacyPage() {
  const N = window.NODESTONE;
  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 24 }}>Privacy</div>
            <h1>How we handle website data.</h1>
            <p className="lede">
              Plain details on what we collect, why we collect it, who helps us process it, and how to ask us to change or delete it.
            </p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal className="legal-layout">
            <aside className="legal-rail">
              <span className="mono">Last updated</span>
              <strong>{LEGAL_UPDATED}</strong>
              <p>This page covers this website, analytics and enquiries sent through the contact form.</p>
            </aside>

            <div className="legal-copy">
              <LegalSection id="controller" title="Who controls the data">
                <p>
                  Nodestone is the controller for personal data collected through this website and through enquiries sent to {N.brand.contactEmail}.
                </p>
              </LegalSection>

              <LegalSection id="collect" title="What we collect">
                <ul>
                  <li>Contact form details: name, email, phone number, company, preferred contact method, selected services and message.</li>
                  <li>Source details attached to enquiries: page URL, path, referrer, user agent, timestamp and submission reference.</li>
                  <li>Security signals needed to verify Cloudflare Turnstile and protect the contact endpoint.</li>
                  <li>Google Analytics 4 information, but only if you accept analytics cookies. This covers page views, route changes, outbound clicks, file downloads and generic contact-form interaction events.</li>
                </ul>
              </LegalSection>

              <LegalSection id="why" title="Why we use it">
                <ul>
                  <li>To respond to enquiries and decide whether we can help.</li>
                  <li>To run and secure the website, contact form and anti-spam checks.</li>
                  <li>To create internal follow-up records for legitimate business enquiries.</li>
                  <li>To understand website performance, page usage and whether visitors start or submit the contact form when analytics consent has been given.</li>
                </ul>
              </LegalSection>

              <LegalSection id="basis" title="Legal basis">
                <p>
                  Enquiry handling is processed under legitimate interests and, where relevant, steps before a contract. Security processing is handled under legitimate interests and legal obligations around keeping systems safe. Google Analytics is based on consent.
                </p>
              </LegalSection>

              <LegalSection id="processors" title="Who helps process it">
                <p>
                  We use selected service providers to run the site and handle enquiries: Cloudflare Pages and Turnstile, Google Analytics, n8n Cloud, Microsoft 365 and Microsoft Graph, and Trello/Atlassian.
                </p>
                <p>
                  These providers may process data outside the UK. Where they do, we rely on the safeguards available through their contracts, transfer terms and data processing arrangements.
                </p>
              </LegalSection>

              <LegalSection id="retention" title="How long we keep it">
                <p>
                  Website enquiries are kept for up to 24 months from the last meaningful contact unless the enquiry becomes client work, a supplier record, or another business record that needs to be retained for longer.
                </p>
                <p>
                  Analytics data is retained according to the Google Analytics property settings.
                </p>
              </LegalSection>

              <LegalSection id="rights" title="Your rights">
                <p>
                  You can ask for access, correction, deletion, restriction, objection, portability, or withdrawal of consent where those rights apply. Email {N.brand.contactEmail} and we will handle the request from there.
                </p>
                <p>
                  You can also complain to the Information Commissioner's Office at <a href="https://ico.org.uk/make-a-complaint/" target="_blank" rel="noreferrer">ico.org.uk</a>.
                </p>
              </LegalSection>
            </div>
          </Reveal>
        </div>
      </section>
    </>
  );
}

function CookiesPage() {
  const openPreferences = () => {
    window.NodestoneOpenCookiePreferences?.();
  };

  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 24 }}>Cookies</div>
            <h1>Essential storage, optional analytics.</h1>
            <p className="lede">
              The site works without analytics. If you accept analytics, Google Analytics helps us understand which pages are useful.
            </p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal className="legal-layout">
            <aside className="legal-rail">
              <span className="mono">Last updated</span>
              <strong>{LEGAL_UPDATED}</strong>
              <p>You can change your analytics choice at any time.</p>
              <button type="button" className="btn btn-ghost" onClick={openPreferences}>Cookie preferences</button>
            </aside>

            <div className="legal-copy">
              <LegalSection id="essential-storage" title="Essential storage">
                <p>
                  Some storage is needed for the site to work properly. This includes remembering your cookie choice, keeping short-lived contact-page selections in session storage, and supporting Cloudflare Turnstile security checks on the contact form.
                </p>
                <div className="legal-data-list">
                  <div>
                    <span className="mono">nodestone.cookieConsent.v1</span>
                    <p>Stores your analytics choice in local browser storage so we do not ask on every page.</p>
                  </div>
                  <div>
                    <span className="mono">contactPreselect</span>
                    <p>Session-only storage used when you click from a service page to the contact form.</p>
                  </div>
                  <div>
                    <span className="mono">Cloudflare Turnstile</span>
                    <p>Security technology used to check that contact submissions are not automated abuse.</p>
                  </div>
                </div>
              </LegalSection>

              <LegalSection id="analytics-storage" title="Google Analytics">
                <p>
                  Google Analytics 4 is optional. It is configured with Google Consent Mode and analytics storage is denied by default. If you accept analytics, GA4 may set cookies such as <span className="mono">_ga</span> and <span className="mono">_ga_*</span>.
                </p>
                <p>
                  We use GA4 for page views, route changes, outbound clicks, file downloads and generic contact-form interaction events such as form start and form submit. We do not intentionally send form field values such as name, email, phone number, company or message text to Google Analytics.
                </p>
                <p>
                  Site search, scroll tracking and video engagement measurement are not enabled. We do not use Google Ads, remarketing, Google Signals, ads personalisation, User-ID collection or user-provided data collection.
                </p>
              </LegalSection>

              <LegalSection id="choices" title="Your choices">
                <p>
                  Rejecting analytics keeps optional analytics storage denied. Accepting analytics lets GA4 measure site usage. You can reopen preferences from this page or the footer.
                </p>
                <button type="button" className="btn btn-primary" onClick={openPreferences}>Change cookie preferences</button>
              </LegalSection>
            </div>
          </Reveal>
        </div>
      </section>
    </>
  );
}

function TermsPage() {
  const N = window.NODESTONE;
  const go = (e, p) => { e.preventDefault(); window.NodestoneNavigate(p); };
  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 24 }}>Terms and Conditions</div>
            <h1>The working terms for Nodestone.</h1>
            <p className="lede">
              The plain version of how we scope, protect, deliver and support technology work. Signed project documents still win where they say something different.
            </p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal className="legal-layout">
            <aside className="legal-rail">
              <span className="mono">Last updated</span>
              <strong>{TERMS_UPDATED}</strong>
              <p>These terms cover this website, enquiries, early discussions and services unless a signed agreement says otherwise.</p>
              <a href="/contact" className="btn btn-ghost" onClick={(e) => go(e, "/contact")}>Ask a question</a>
            </aside>

            <div className="legal-copy">
              <LegalSection id="who" title="Who these terms are with">
                <p>
                  These terms are between you and Nodestone Labs Ltd trading as Nodestone, a company registered in England and Wales with company number 17240950. Our registered office is 71-75 Shelton Street, Covent Garden, London, WC2H 9JQ.
                </p>
                <p>
                  You can contact us at <a href={"mailto:" + N.brand.contactEmail}>{N.brand.contactEmail}</a>.
                </p>
              </LegalSection>

              <LegalSection id="scope" title="When these terms apply">
                <p>
                  These terms apply when you use this website, make an enquiry, discuss a possible project with us, or ask us to provide IT consultancy or bespoke technology services. Those services may include discovery, analysis, design, development, integration, configuration, testing, deployment, support, audits, diagnostics, application work, website or back-end plugins, management software, workflow systems and related digital solutions.
                </p>
                <p>
                  If we enter into a signed proposal, statement of work, master services agreement, data processing agreement, non-disclosure agreement or other written contract with you, that signed document takes priority for the work it covers.
                </p>
              </LegalSection>

              <LegalSection id="website" title="Using this website">
                <p>
                  The website is provided for general information and enquiry purposes. We try to keep it accurate, but it is not technical, legal, financial or security advice for your specific situation.
                </p>
                <p>
                  You must not misuse the website, attempt to disrupt it, probe it without permission, submit malicious content, impersonate another person, or use the contact form for unlawful, abusive, fraudulent or automated activity.
                </p>
              </LegalSection>

              <LegalSection id="scoping" title="Scoping and client information">
                <p>
                  To scope, quote, design, develop, secure, test, support or maintain bespoke software and integrations, we may need to understand your business processes, daily operations, infrastructure, systems architecture, data flows, workflows, user roles, permissions, software stack, APIs, hosting environment, databases, credential management, technical dependencies, security controls, suppliers, policies, pain points, reporting needs and commercial requirements.
                </p>
                <p>
                  You agree to provide complete, truthful and timely information we reasonably request. If information is incomplete, inaccurate, delayed or withheld, we are not responsible for resulting delay, defects, rework, incompatibility, cost increases, security gaps or failure to meet expected outcomes.
                </p>
              </LegalSection>

              <LegalSection id="access" title="Access, credentials and security">
                <p>
                  You must not send live credentials, production access, personal data, secrets, API keys, security keys or privileged access unless they are needed for the work, approved by you, and provided through secure channels agreed by both parties.
                </p>
                <p>
                  We may record technical assumptions, dependencies, limitations, risks, third-party constraints and actions needed from you. You remain responsible for your own backups, access approvals, internal permissions, production change controls and decisions to accept or reject recommendations.
                </p>
              </LegalSection>

              <LegalSection id="confidentiality" title="Confidential information">
                <p>
                  Each party may receive confidential information from the other before, during or after discussions or work. Confidential information includes information disclosed orally, visually, electronically, in writing, by system access, demonstrations, meetings, site visits, screen sharing or any other means, where the information is confidential by nature or would reasonably be regarded as confidential.
                </p>
                <p>
                  Confidential information includes business plans, finances, pricing, proposals, statements of work, client lists, suppliers, trade secrets, software, source code, scripts, architecture, databases, schemas, roadmaps, vulnerabilities, security observations, access credentials, API keys, tokens, system diagrams, operational processes, user behaviour, personal data, policies, internal procedures, contracts, know-how, prototypes, designs, wireframes, UX flows, algorithms, integrations and documentation.
                </p>
                <p>
                  The receiving party must keep confidential information confidential, use it only for evaluating, negotiating, scoping, delivering, supporting, auditing or maintaining services between the parties, and protect it with at least reasonable technical, organisational and contractual safeguards.
                </p>
              </LegalSection>

              <LegalSection id="permitted-sharing" title="Permitted sharing">
                <p>
                  Confidential information may be shared only with directors, employees, contractors, professional advisers and approved subcontractors who need it for the relevant purpose and who are bound by confidentiality duties no less protective than these terms.
                </p>
                <p>
                  Confidentiality duties do not apply to information the receiving party can prove was already lawfully known without restriction, becomes public other than through breach, is lawfully received from a third party without restriction, is independently developed without using the confidential information, or must be disclosed by law, regulator, court or competent authority.
                </p>
              </LegalSection>

              <LegalSection id="due-diligence" title="Due diligence and refusal of work">
                <p>
                  We may carry out client vetting, verification and due diligence before or during an engagement. This may include checking company identity, signatory authority, beneficial ownership or key decision-makers where appropriate, website or domain ownership, legitimacy of requested access, lawful project purpose, sanctions or fraud-risk indicators, payment risk, cyber-risk and conflicts.
                </p>
                <p>
                  We may refuse, suspend or stop discussions or services if verification is not completed to our reasonable satisfaction or if we suspect unlawful, unsafe, fraudulent, abusive, malicious or unethical use of our services.
                </p>
              </LegalSection>

              <LegalSection id="proposals" title="Proposals, pricing and changes">
                <p>
                  Proposals, estimates and indicative timings are based on the information available when they are given. Unless a written proposal says otherwise, they are not a binding commitment to provide services until both parties agree the scope, price and start conditions.
                </p>
                <p>
                  If scope, assumptions, dependencies, client actions, third-party behaviour or technical conditions change, we may need to revise price, timing, delivery approach or risk notes before continuing.
                </p>
              </LegalSection>

              <LegalSection id="deliverables" title="Deliverables and intellectual property">
                <p>
                  Disclosure of information or early project discussion does not transfer ownership of confidential information, intellectual property, software, code, tools, know-how, trade marks, designs, inventions or data.
                </p>
                <p>
                  Ownership and licence terms for project deliverables should be set out in the relevant proposal or signed agreement. Unless agreed in writing, we retain ownership of our pre-existing methods, tooling, templates, diagnostics, architecture patterns, reusable code, know-how and internal business processes.
                </p>
              </LegalSection>

              <LegalSection id="third-parties" title="Third-party systems">
                <p>
                  Many projects rely on hosting providers, platforms, APIs, plugins, vendors, software libraries, internet services and client-controlled systems. We are not responsible for failures, outages, policy changes, pricing changes, limitations, security weaknesses or incompatibilities in third-party systems outside our control.
                </p>
              </LegalSection>

              <LegalSection id="data-protection" title="Data protection">
                <p>
                  Where confidential information includes personal data, both parties must comply with applicable data protection laws. If we process personal data for you as a processor, the parties must enter into a data processing agreement before that processing begins, unless adequate processor terms are already included in a signed main agreement.
                </p>
                <p>
                  Website data is handled as described in our <a href="/privacy" onClick={(e) => go(e, "/privacy")}>privacy notice</a> and <a href="/cookies" onClick={(e) => go(e, "/cookies")}>cookie notice</a>.
                </p>
              </LegalSection>

              <LegalSection id="misuse" title="Misuse of information and outputs">
                <p>
                  You must not use our information, tools, findings, reports, code, recommendations or deliverables to attack, bypass, harm, unlawfully access, disrupt or compromise any system, network, person, business or third-party service.
                </p>
                <p>
                  Each party must promptly notify the other of suspected unauthorised access, disclosure, loss, compromise or misuse of confidential information and cooperate to investigate, mitigate and remediate the incident.
                </p>
              </LegalSection>

              <LegalSection id="liability" title="Responsibility and liability">
                <p>
                  Nothing in these terms excludes or limits liability where it would be unlawful to do so, including liability for fraud, fraudulent misrepresentation, death or personal injury caused by negligence, or statutory rights that cannot legally be excluded.
                </p>
                <p>
                  Subject to any signed agreement, we are not responsible for losses arising from your instructions, inaccurate or incomplete information, third-party systems, client security weaknesses, misuse of deliverables, failure to maintain backups, failure to implement recommendations, unauthorised changes made by you or third parties, or use of outputs outside the agreed purpose.
                </p>
              </LegalSection>

              <LegalSection id="ending" title="Ending discussions or work">
                <p>
                  Either party may decide not to proceed with a project unless a signed agreement says otherwise. Once work has started, cancellation, suspension, payment and handover terms should be handled under the relevant proposal or signed agreement.
                </p>
                <p>
                  On request or when the relevant purpose ends, each party should return or securely destroy confidential information, except where retention is needed for routine backups, audit logs, professional records, insurance files, legal compliance or legitimate business governance. Retained information remains protected.
                </p>
              </LegalSection>

              <LegalSection id="law" title="Governing law">
                <p>
                  These terms and any dispute or non-contractual obligation arising from them are governed by the laws of England and Wales. The courts of England and Wales have exclusive jurisdiction, unless the parties expressly agree in writing to Irish law or Irish courts for a specific Irish engagement.
                </p>
              </LegalSection>
            </div>
          </Reveal>
        </div>
      </section>
    </>
  );
}

// ────────────────────────────────────────────────────────────────────────────
// CONTACT
// ────────────────────────────────────────────────────────────────────────────
const CONTACT_METHODS = [
  { id: "email", label: "Email" },
  { id: "phone", label: "Phone" },
  { id: "whatsapp", label: "WhatsApp" },
];
const CONTACT_MESSAGE_WORD_LIMIT = 512;
const CONTACT_NAME_MAX_LENGTH = 120;
const CONTACT_COMPANY_MAX_LENGTH = 160;
const CONTACT_ALPHABETIC_PATTERN = /^[\p{L}\p{M}][\p{L}\p{M}\s'’-]*$/u;

function countWords(value) {
  return (String(value || "").trim().match(/\S+/g) || []).length;
}

function limitWords(value, maxWords) {
  const words = String(value || "").trim().match(/\S+/g) || [];
  if (words.length <= maxWords) return value;
  return words.slice(0, maxWords).join(" ");
}

function isValidContactEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || "").trim());
}

function isValidContactPhone(value) {
  const next = String(value || "").trim();
  const digitCount = (next.match(/\d/g) || []).length;
  return digitCount >= 7 && digitCount <= 32 && /^[+\d\s()-]+$/.test(next);
}

function cleanAlphabeticContactField(value, maxLength) {
  return String(value || "")
    .replace(/[^\p{L}\p{M}\s'’-]/gu, "")
    .replace(/\s+/g, " ")
    .slice(0, maxLength);
}

function isValidAlphabeticContactField(value, { optional = false } = {}) {
  const next = String(value || "").trim();
  if (!next) return optional;
  return CONTACT_ALPHABETIC_PATTERN.test(next);
}

function ContactPage() {
  const N = window.NODESTONE;
  const c = N.contact;
  const [sent, setSent] = React.useState(false);
  const [status, setStatus] = React.useState("idle");
  const [formError, setFormError] = React.useState("");
  const [fields, setFields] = React.useState({ name: "", email: "", phone: "", company: "", message: "" });
  const [turnstileToken, setTurnstileToken] = React.useState("");
  const [turnstileError, setTurnstileError] = React.useState("");
  const [selectedServices, setSelectedServices] = React.useState([]);
  const [selectedContactMethods, setSelectedContactMethods] = React.useState(["email"]);
  const formRef = React.useRef(null);
  const turnstileRef = React.useRef(null);
  const turnstileWidgetRef = React.useRef(null);
  const turnstileSiteKey = c.turnstileSiteKey || "";
  React.useEffect(() => {
    const pre = sessionStorage.getItem('contactPreselect');
    if (pre) { setSelectedServices([pre]); sessionStorage.removeItem('contactPreselect'); }
  }, []);
  React.useEffect(() => {
    if (!turnstileSiteKey) {
      setTurnstileError("Contact verification is not configured yet.");
      return;
    }

    let cancelled = false;
    let timer = null;

    const renderTurnstile = () => {
      if (cancelled || !turnstileRef.current || !window.turnstile || turnstileWidgetRef.current !== null) return;
      turnstileWidgetRef.current = window.turnstile.render(turnstileRef.current, {
        sitekey: turnstileSiteKey,
        action: "contact",
        callback: (token) => {
          setTurnstileToken(token);
          setTurnstileError("");
        },
        "expired-callback": () => {
          setTurnstileToken("");
          setTurnstileError("Verification expired. Please try again.");
        },
        "error-callback": () => {
          setTurnstileToken("");
          setTurnstileError("Verification could not complete. Please refresh and try again.");
        },
      });
      if (timer) {
        window.clearInterval(timer);
        timer = null;
      }
    };

    if (window.turnstile) {
      renderTurnstile();
    } else {
      timer = window.setInterval(renderTurnstile, 150);
    }

    return () => {
      cancelled = true;
      if (timer) window.clearInterval(timer);
      if (turnstileWidgetRef.current !== null && window.turnstile?.remove) {
        window.turnstile.remove(turnstileWidgetRef.current);
      }
      turnstileWidgetRef.current = null;
    };
  }, [turnstileSiteKey]);
  const toggleService = (slug) => {
    setSelectedServices((prev) => prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug]);
    if (formError) setFormError("");
  };
  const toggleContactMethod = (method) => {
    setSelectedContactMethods((prev) => prev.includes(method) ? prev.filter((m) => m !== method) : [...prev, method]);
    if (formError) setFormError("");
  };
  const messageWordCount = countWords(fields.message);
  const hasMessage = fields.message.trim().length > 0;
  const isFormReady =
    isValidAlphabeticContactField(fields.name) &&
    isValidContactEmail(fields.email) &&
    isValidContactPhone(fields.phone) &&
    isValidAlphabeticContactField(fields.company, { optional: true }) &&
    selectedContactMethods.length > 0 &&
    selectedServices.length > 0 &&
    hasMessage &&
    messageWordCount <= CONTACT_MESSAGE_WORD_LIMIT &&
    !!turnstileToken;
  const handleFieldChange = (field) => (event) => {
    let value = event.target.value;
    if (field === "message") value = limitWords(value, CONTACT_MESSAGE_WORD_LIMIT);
    if (field === "name") value = cleanAlphabeticContactField(value, CONTACT_NAME_MAX_LENGTH);
    if (field === "company") value = cleanAlphabeticContactField(value, CONTACT_COMPANY_MAX_LENGTH);
    setFields((prev) => ({ ...prev, [field]: value }));
    if (formError) setFormError("");
  };
  const resetTurnstile = () => {
    setTurnstileToken("");
    if (turnstileWidgetRef.current !== null && window.turnstile?.reset) {
      window.turnstile.reset(turnstileWidgetRef.current);
    }
  };
  const dismissSuccess = () => {
    formRef.current?.reset();
    setFields({ name: "", email: "", phone: "", company: "", message: "" });
    setSelectedServices([]);
    setSelectedContactMethods(["email"]);
    setStatus("idle");
    setFormError("");
    setSent(false);
    resetTurnstile();
  };
  const handleSubmit = async (e) => {
    e.preventDefault();
    if (status === "sending") return;

    const form = e.currentTarget;
    const formData = new FormData(form);

    if (!fields.name.trim()) {
      setFormError("Your name is required.");
      return;
    }

    if (!isValidAlphabeticContactField(fields.name)) {
      setFormError("Use letters only for your name.");
      return;
    }

    if (!isValidContactEmail(fields.email)) {
      setFormError("A valid email is required.");
      return;
    }

    if (!isValidContactPhone(fields.phone)) {
      setFormError("A valid phone number is required.");
      return;
    }

    if (!isValidAlphabeticContactField(fields.company, { optional: true })) {
      setFormError("Use letters only for the company name.");
      return;
    }

    if (selectedContactMethods.length === 0) {
      setFormError("Choose at least one preferred contact method.");
      return;
    }

    if (selectedServices.length === 0) {
      setFormError("Choose at least one service area.");
      return;
    }

    if (!hasMessage) {
      setFormError("A short message is required.");
      return;
    }

    if (messageWordCount > CONTACT_MESSAGE_WORD_LIMIT) {
      setFormError(`Keep the message to ${CONTACT_MESSAGE_WORD_LIMIT} words or fewer.`);
      return;
    }

    if (!turnstileToken) {
      setFormError("Please complete the verification check before sending.");
      return;
    }

    setStatus("sending");
    setFormError("");

    const payload = {
      name: String(formData.get("name") || ""),
      email: String(formData.get("email") || ""),
      phone: String(formData.get("phone") || ""),
      company: String(formData.get("company") || ""),
      preferredContactMethods: selectedContactMethods,
      services: selectedServices,
      message: String(formData.get("message") || ""),
      turnstileToken,
      source: {
        url: window.location.href,
        path: window.location.pathname,
        referrer: document.referrer || "",
      },
    };

    try {
      const response = await fetch("/api/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "same-origin",
        cache: "no-store",
        body: JSON.stringify(payload),
      });
      const result = await response.json().catch(() => ({}));

      if (!response.ok || result.ok !== true) {
        throw new Error(result.message || "Unable to send your message right now.");
      }

      setSent(true);
      setStatus("sent");
    } catch (error) {
      setStatus("error");
      setFormError(error.message || "Unable to send your message right now.");
      resetTurnstile();
    }
  };

  return (
    <>
      <section className="page-hero">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 24 }}>{c.eyebrow}</div>
            <h1>{c.title}</h1>
            <p className="lede">{c.body}</p>
          </Reveal>
        </div>
      </section>

      <section className="block-tight">
        <div className="wrap">
          <Reveal>
            <div className="contact-card" data-comment-anchor="73af8044c1-div-741-13">
              {sent && (
                <div className="contact-success">
                  <button className="contact-success-dismiss" onClick={dismissSuccess} aria-label="Dismiss">
                    <Icon name="x" size={18} />
                  </button>
                  <div className="contact-success-check">
                    <svg viewBox="0 0 52 52">
                      <circle cx="26" cy="26" r="23" pathLength="1" />
                      <path d="M14 26l9 9 16-16" pathLength="1" />
                    </svg>
                  </div>
                  <h3 className="contact-success-title">Message received.</h3>
                  <p className="contact-success-body">We'll be in touch soon. A confirmation email is on its way so you know we've got it.</p>
                </div>
              )}
              <form ref={formRef} className="contact-form" onSubmit={handleSubmit} aria-busy={status === "sending"}>
                <div>
                  <label htmlFor="cf-name">Your name</label>
                  <input id="cf-name" name="name" type="text" required autoComplete="name" maxLength={CONTACT_NAME_MAX_LENGTH} placeholder="Jane Doe" title="Use letters only." value={fields.name} onChange={handleFieldChange("name")} />
                </div>
                <div>
                  <label htmlFor="cf-email">Email</label>
                  <input id="cf-email" name="email" type="email" required autoComplete="email" maxLength={254} placeholder="jane@company.com" value={fields.email} onChange={handleFieldChange("email")} />
                </div>
                <div>
                  <label htmlFor="cf-phone">Phone number</label>
                  <input id="cf-phone" name="phone" type="tel" required autoComplete="tel" maxLength={32} placeholder="+44 7700 900000" value={fields.phone} onChange={handleFieldChange("phone")} />
                </div>
                <div>
                  <label htmlFor="cf-co">Company (optional)</label>
                  <input id="cf-co" name="company" type="text" autoComplete="organization" maxLength={CONTACT_COMPANY_MAX_LENGTH} placeholder="Acme Ltd" title="Use letters only." value={fields.company} onChange={handleFieldChange("company")} />
                </div>
                <div>
                  <label>Preferred contact method</label>
                  <div className="service-chips">
                    {CONTACT_METHODS.map((method) =>
                    <button type="button" key={method.id}
                      className={"service-chip" + (selectedContactMethods.includes(method.id) ? " selected" : "")}
                      aria-pressed={selectedContactMethods.includes(method.id)}
                      onClick={() => toggleContactMethod(method.id)}>{method.label}</button>
                    )}
                  </div>
                </div>
                <div>
                  <label>What are you looking for help with?</label>
                  <div className="service-chips">
                    {N.services.map((s) =>
                    <button type="button" key={s.slug}
                      className={"service-chip" + (selectedServices.includes(s.slug) ? " selected" : "")}
                      aria-pressed={selectedServices.includes(s.slug)}
                      onClick={() => toggleService(s.slug)}>{s.label}</button>
                    )}
                    <button type="button"
                      className={"service-chip" + (selectedServices.includes("other") ? " selected" : "")}
                      aria-pressed={selectedServices.includes("other")}
                      onClick={() => toggleService("other")}>Something else</button>
                  </div>
                </div>
                <div>
                  <label htmlFor="cf-msg">More context</label>
                  <textarea id="cf-msg" name="message" required maxLength={3000} placeholder="The platform, the pressure, the outcome you need. Short is fine." rows={5} value={fields.message} onChange={handleFieldChange("message")} aria-describedby="cf-msg-count" />
                  <p id="cf-msg-count" className={"contact-form-count" + (messageWordCount >= CONTACT_MESSAGE_WORD_LIMIT ? " at-limit" : "")}>
                    {messageWordCount}/{CONTACT_MESSAGE_WORD_LIMIT} words
                  </p>
                </div>
                <div>
                  <label>Verification</label>
                  <div className="turnstile-box">
                    <div ref={turnstileRef} />
                    {turnstileError && <p className="contact-form-note">{turnstileError}</p>}
                  </div>
                </div>
                {formError && <p className="contact-form-error" role="alert">{formError}</p>}
                <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, marginTop: 8 }}>
                  <button type="submit" className="btn btn-primary" disabled={status === "sending" || !isFormReady}>
                    {status === "sending" ? "Sending..." : "Send it"} <span className="arr">→</span>
                  </button>
                  <span className="mono" style={{ fontSize: 11, color: "var(--fg-4)" }}>
                    or email <a href={"mailto:" + c.email} style={{ borderBottom: "1px solid var(--line-strong)" }}>{c.email}</a>
                  </span>
                </div>
                <p className="contact-form-note">
                  By sending an enquiry, you agree that we can use the details to respond. Read the <a href="/terms-and-conditions" onClick={(e) => { e.preventDefault(); window.NodestoneNavigate("/terms-and-conditions"); }}>terms</a> and <a href="/privacy" onClick={(e) => { e.preventDefault(); window.NodestoneNavigate("/privacy"); }}>privacy notice</a>.
                </p>
              </form>
              <dl className="contact-meta">
                <div>
                  <dt>Email</dt>
                  <dd><a href={"mailto:" + c.email}>{c.email}</a></dd>
                </div>
                <div>
                  <dt>Response</dt>
                  <dd data-comment-anchor="43d50b9fc9-dd-775-19">{c.response}</dd>
                </div>
                <div>
                  <dt>Hours</dt>
                  <dd data-comment-anchor="a927195845-dd-779-19">{c.hours}</dd>
                </div>
                <div>
                  <dt>Location</dt>
                  <dd>{N.brand.location}</dd>
                </div>
              </dl>
            </div>
          </Reveal>
        </div>
      </section>

      {/* FAQ-ish strip */}
      <section className="block pastel-2">
        <div className="wrap">
          <Reveal>
            <div className="eyebrow" style={{ marginBottom: 16 }}>What to expect</div>
            <h2 style={{ maxWidth: "20ch", marginBottom: 48 }}>What happens after you hit send.</h2>
          </Reveal>
          <Reveal stagger className="grid-3">
            {[
            { n: "01", icon: "send", t: "A short reply, quickly", b: "Usually within one working day. We confirm we've understood the problem and the pressure." },
            { n: "02", icon: "chat", t: "A 30-minute conversation", b: "No deck, no questionnaire. We listen, ask the awkward questions, share an honest first read." },
            { n: "03", icon: "route", t: "A sensible next move", b: "Diagnostic, delivery or leadership — whichever fits. We'll be specific about scope and shape." }].
            map((s) =>
            <div className="card" key={s.n}>
                <div className="card-icon"><Icon name={s.icon} /></div>
                <h3>{s.t}</h3>
                <p>{s.b}</p>
              </div>
            )}
          </Reveal>
        </div>
      </section>
    </>);

}

// ────────────────────────────────────────────────────────────────────────────
// CLOSING CTA + FOOTER
// ────────────────────────────────────────────────────────────────────────────
function ClosingCta({ serviceSlug } = {}) {
  const N = window.NODESTONE;
  const c = N.cta;
  return (
    <section className="block-tight">
      <div className="wrap">
        <Reveal>
          <div style={{
            background: "var(--bg-elev)",
            border: "1px solid var(--line)",
            borderRadius: "var(--radius-lg)",
            padding: "64px 56px",
            display: "grid",
            gridTemplateColumns: "1.4fr 1fr",
            gap: 48,
            alignItems: "end",
            position: "relative",
            overflow: "hidden"
          }} className="closing-cta">
            <div style={{
              position: "absolute",
              inset: 0,
              background: "radial-gradient(circle at 90% 10%, var(--pastel-3) 0%, transparent 45%), radial-gradient(circle at 0% 100%, var(--pastel-1) 0%, transparent 50%)",
              opacity: 0.5,
              pointerEvents: "none"
            }} />
            <div style={{ position: "relative" }}>
              {c.eyebrow && <div className="eyebrow" style={{ marginBottom: 16 }}>{c.eyebrow}</div>}
              <h2 style={{ marginBottom: 16, maxWidth: "16ch" }}>{c.title}</h2>
              <p className="lede" style={{ marginBottom: 0 }}>{c.body}</p>
            </div>
            <div style={{ position: "relative", display: "flex", justifyContent: "flex-end", alignItems: "end", gap: 12 }}>
              <a href={c.button.path} className="btn btn-primary"
              onClick={(e) => {e.preventDefault();if(serviceSlug){sessionStorage.setItem('contactPreselect',serviceSlug);}window.NodestoneNavigate(c.button.path);}}>
                {c.button.label} <span className="arr">→</span>
              </a>
            </div>
            <style>{`@media (max-width:720px){.closing-cta{grid-template-columns:1fr !important; padding: 40px 28px !important;}}`}</style>
          </div>
        </Reveal>
      </div>
    </section>);

}

function Footer() {
  const N = window.NODESTONE;
  const go = (e, p) => { e.preventDefault(); window.NodestoneNavigate(p); };
  return (
    <footer className="footer">
      <div className="wrap">
        <div className="footer-grid">
          <div>
            <div style={{ display: "inline-flex", alignItems: "center", gap: 8, fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 19, letterSpacing: "-0.03em", color: "var(--fg)", marginBottom: 16 }}>
              <span className="nav-brand-dot" />
              Nodestone
            </div>
            <p style={{ maxWidth: "34ch", fontSize: 13.5, color: "var(--fg-3)" }}>
              Cloud, infrastructure, automation, applications, integrations and software delivery.
            </p>
          </div>
          <div>
            <h5>Services</h5>
            <div className="footer-links">
              {N.services.slice(0, 4).map((s) =>
              <a key={s.slug} href={"/services/" + s.slug} onClick={(e) => go(e, "/services/" + s.slug)}>{s.label}</a>
              )}
              <a href="/services" onClick={(e) => go(e, "/services")} style={{ color: "var(--fg-4)" }}>See all →</a>
            </div>
          </div>
          <div>
            <h5>Company</h5>
            <div className="footer-links">
              <a href="/about" onClick={(e) => go(e, "/about")}>Who we are</a>
              <a href="/workshop" onClick={(e) => go(e, "/workshop")}>Workshop</a>
              <a href="/contact" onClick={(e) => go(e, "/contact")}>Contact</a>
            </div>
          </div>
          <div>
            <h5>Get in touch</h5>
            <div className="footer-links">
              <a href={"mailto:" + N.brand.contactEmail}>{N.brand.contactEmail}</a>
              <span style={{ color: "var(--fg-4)" }}>{N.brand.location}</span>
            </div>
          </div>
          <div>
            <h5>Legal</h5>
            <div className="footer-links">
              <a href="/terms-and-conditions" onClick={(e) => go(e, "/terms-and-conditions")}>Terms and Conditions</a>
              <a href="/privacy" onClick={(e) => go(e, "/privacy")}>Privacy</a>
              <a href="/cookies" onClick={(e) => go(e, "/cookies")}>Cookies</a>
              <button type="button" onClick={() => window.NodestoneOpenCookiePreferences?.()}>Cookie preferences</button>
            </div>
          </div>
        </div>

        <div className="footer-bottom">
          <span className="footer-legal">© {new Date().getFullYear()} {N.brand.company}</span>
          <span className="mono">v0.7 · In development</span>
        </div>
      </div>
    </footer>);

}

window.HomePage = HomePage;
window.ServicesPage = ServicesPage;
window.ServiceDetailPage = ServiceDetailPage;
window.AboutPage = AboutPage;
window.ProductsPage = ProductsPage;
window.ContactPage = ContactPage;
window.PrivacyPage = PrivacyPage;
window.CookiesPage = CookiesPage;
window.TermsPage = TermsPage;
window.Footer = Footer;
window.Hero = Hero;
window.Reveal = Reveal;
window.ClosingCta = ClosingCta;
