PageSourceSearch

https://bolt.new/careers

html bolt.new collected 2026-09-24 15:19:13 UTC 221,126 bytes, 2,896 lines download raw bytes

1<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <head><meta charset="UTF-8"><meta name="description" content="Bolt turns a prompt into a real, deployable app. See open roles across engineering, design, marketing, sales and customer experience."><meta name="viewport" content="width=device-width, initial-scale=1.0"><!-- Open Graph / Facebook --><meta property="og:type" content="website"><meta property="og:url" content="https://bolt.new/careers"><meta property="og:title" content="Careers at Bolt"><meta property="og:description" content="Bolt turns a prompt into a real, deployable app. See open roles across engineering, design, marketing, sales and customer experience."><meta property="og:image" content="https://bolt.new/public-page-assets/og-image-default.jpg"><!-- Twitter --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:url" content="https://bolt.new/careers"><meta name="twitter:title" content="Careers at Bolt"><meta name="twitter:description" content="Bolt turns a prompt into a real, deployable app. See open roles across engineering, design, marketing, sales and customer experience."><meta name="twitter:image" content="https://bolt.new/public-page-assets/og-image-default.jpg"><title>Careers at Bolt</title>
1<script>(function(){const bootstrap = {"allowedHosts":["bolt.new"],"writeKey":"ta1kDK49qdEDEfd8KYxI37mW0GPkLKn1","environment":"production","disabled":false,"page":{"pageType":"other","contentId":"careers","cmsCollection":"careers","dataLayer":{}}};
2const anonymousIdCookie = "ajs_anonymous_id=__ANONYMOUS_ID__; Path=/; Secure; SameSite=Strict; Max-Age=31536000";
3const anonymousIdCookieWithDomain = "ajs_anonymous_id=__ANONYMOUS_ID__; Domain=.bolt.new; Path=/; Secure; SameSite=Strict; Max-Age=31536000";
4const ANONYMOUS_ID_COOKIE_PLACEHOLDER = "__ANONYMOUS_ID__";
5
6  (function () {
7    if (
8      bootstrap.disabled ||
9      (bootstrap.environment !== "development" && bootstrap.allowedHosts.indexOf(location.hostname) === -1)
10    ) return;
11
12    var anonymousId;
13    var cookies = document.cookie ? document.cookie.split(";") : [];
14
15    for (var index = 0; index < cookies.length; index += 1) {
16      var parts = cookies[index].trim().split("=");
17      if (parts.shift() === "ajs_anonymous_id") {
18        anonymousId = parts.join("=") || undefined;
19        break;
20      }
21    }
22
23    if (!anonymousId && typeof window.crypto.randomUUID === "function") {
24      anonymousId = window.crypto.randomUUID();
25    }
26
27    if (anonymousId) {
28      var template = location.hostname === "bolt.new" ? anonymousIdCookieWithDomain : anonymousIdCookie;
29      document.cookie = template.split(ANONYMOUS_ID_COOKIE_PLACEHOLDER).join(anonymousId);
30
31      bootstrap.anonymousId = anonymousId;
32    }
33
34    window.__boltPublicPageAnalytics = bootstrap;
35  })();
36})();</script>
36 
36<script type="module" src="/public-page-assets/astro/HeadScripts.astro_astro_type_script_index_0_lang.Pz2Qz10j.js"></script>
36 
36<script src="https://redline.agence.ink/redline.js" data-key="b54541e1a49f49e99964016d3f5c26e0" defer></script>
36<link rel="canonical" href="https://bolt.new/careers"><!-- Served from /public-page-assets/ because that prefix is proxied to this
37         deployment under bolt.new; a root-relative /favicon.ico is intercepted
38         by bolt.new and 302s to the app shell, so the icon never loads. --><link rel="icon" type="image/x-icon" href="/public-page-assets/favicon.ico"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Display:wght@600&family=Silkscreen:wght@400;700&display=swap"><link id="google-fonts" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Display:wght@600&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" onerror="this.onerror=null;document.getElementById('backup-fonts').disabled=false;"><noscript><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Inter+Display:wght@600&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet"></noscript><!-- Google Tag Manager -->
38<script>
vendor: 455 bytes, lines 38-48
38
39      (function (w, d, s, l, i) {
40        w[l] = w[l] || [];
41        w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
42        var f = d.getElementsByTagName(s)[0],
43          j = d.createElement(s),
44          dl = l != "dataLayer" ? "&l=" + l : "";
45        j.async = true;
46        j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
47        f.parentNode.insertBefore(j, f);
48      })(window, document, "script", "dataLayer", "
48GTM-58KJM59B
vendor: 8 bytes, lines 48-49
48");
49    
49</script>
49<!-- Backup font loading via alternative CDN --><link id="backup-fonts" href="https://fonts.bunny.net/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" disabled onerror="this.onerror=null;document.documentElement.classList.add('fonts-failed');"><!-- Fallback font loading with local fonts -->
49<script type="module">(function(){let t=!1,d=!1;function a(){document.documentElement.classList.add("fonts-loaded")}function o(){console.warn("All font CDNs failed, using system fonts"),document.documentElement.classList.add("fonts-failed")}function s(){const n=document.getElementById("google-fonts"),e=document.getElementById("backup-fonts");if(!n)return;const l=setTimeout(()=>{if(!t&&e){console.warn("Google Fonts timeout, trying backup CDN"),e.disabled=!1;const i=setTimeout(()=>{d||o()},3e3);e.addEventListener("load",()=>{clearTimeout(i),d=!0,a()}),e.addEventListener("error",()=>{clearTimeout(i),o()})}else t||o()},3e3);n.addEventListener("load",()=>{clearTimeout(l),t=!0,a()}),n.addEventListener("error",()=>{clearTimeout(l),e?(console.warn("Google Fonts failed, trying backup CDN"),e.disabled=!1):o()})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",s):s()})();</script>
49<script type="application/ld+json">{"@context":"https://schema.org","@type":"Organization","@id":"https://bolt.new/#organization","name":"Bolt.new","alternateName":"Bolt","url":"https://bolt.new/","logo":{"@type":"ImageObject","url":"https://bolt.new/public-page-assets/bolt-logo.png"},"sameAs":["https://x.com/boltdotnew","https://www.linkedin.com/company/boltdotnew/","https://github.com/stackblitz","https://discord.com/invite/stackblitz","https://www.reddit.com/r/boltnewbuilders/","https://www.youtube.com/@BoltDotNew","https://www.instagram.com/boltdotnew/"]}</script>
vendor: 1 bytes, line 49
49
50<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://bolt.new/"},{"@type":"ListItem","position":2,"name":"Careers at Bolt","item":"https://bolt.new/careers"}]}</script>
50 
50<script src="/public-page-assets/scripts/perf-helpers.js?v=176ysgd"></script>
50 <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&family=Silkscreen:wght@400;700&display=swap"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'"><noscript><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400;1,500&family=Silkscreen:wght@400;700&display=swap" rel="stylesheet"></noscript>  <link rel="preconnect" href="https://boards-api.greenhouse.io"> 
50<script type="application/ld+json">{"@type":"JobPosting","title":"Community Manager","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team that brought you WebContainers, the first-of-its-kind technology that made it possible to run Node.js right inside your browser. That breakthrough kicked off our journey in 2019, and it’s what powers the blazing-fast online IDE used by over 1 million developers every month. But we didn’t stop there. We doubled down on everything we learned and built Bolt.new — the fastest way to go from idea to production without writing traditional code. It’s a next-gen, AI-powered app builder that helps you create, edit, and deploy full-stack web and mobile apps instantly, right in your browser. No installs. No setup. Just smart automation and instant dev environments that let you move at the speed of thought. We’re a fully remote team, globally distributed, deeply collaborative, and seriously passionate about building the future of software development. This is your chance to join a small team with a big vision. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible, we’d love to meet you. ✨ About This Opportunity We're looking for a Community Manager to own and grow both our B2B and B2C communities. You'll set the standard for how we run community at Bolt, from how it's structured to the quality of conversations, content, and value users get from it. This role sits at the intersection of community, product, and education. You'll be in the community daily: responding, moderating, shaping conversations, and pulling out the insights that matter. You'll turn that into clear signals for the business, while making sure our community efforts directly drive engagement, retention, and long-term customer value. You'll have a direct line of influence on product direction and user experience through what you're seeing and hearing on the ground. A major part of this role is building and running our community ambassador program. You'll identify high-value contributors, recruit and nurture them, set the bar for what great contribution looks like, and create the structures that turn engaged users into genuine advocates. This program will be one of your most visible outputs and a key driver of community quality at scale. You'll also shape how community shows up inside the product itself, sourcing and curating the build artifacts, templates, and recipes that help users start faster and ship with confidence. This is a high-ownership, hands-on role. You'll build the strategy and execute it: writing the posts, running the programs, helping moderate the channels, and doing the unglamorous work necessary to scale the community. If you're looking for a role where you set direction and hand off execution, this isn't it. 🛠️ How You'll Contribute&nbsp; Design, launch, and run the community ambassador program end-to-end: recruiting contributors, setting expectations, recognizing meaningful participation, and keeping quality high Spot power users early, build direct relationships with them, and create clear pathways from engaged user to ambassador and advocate Set and maintain the standard for what \"great contribution\" looks like across the community Curate and grow a library of high-quality community build artifacts: starter apps, prompt packs, templates, etc.. Maintain the quality bar and make sure the best stuff is easy to find. Build and nurture a creator loop where community members are incentivized to build reusable assets others can learn from, remix, and ship with Own the community ecosystem end-to-end. Structure it, help moderate it, set the tone, and maintain the quality bar day to day Build and evolve channel architecture to support different user segments and use cases, and actively manage those spaces, not just design them Run targeted engagement programs and playbooks that drive real participation Be in the weeds on product feedback: triaging bugs, feature requests, and friction points with strong signal-to-noise judgment and clear context before surfacing to Product and Engineering Represent the voice of the community in cross-functional conversations with Product, Engineering, and CX, and follow through to make sure it lands Partner with CX Engineering to close the loop so users see their feedback reflected in product improvements Identify what users are struggling with and define community-driven education priorities based on those real patterns Work with CX Enablement to turn those needs into scalable programs, content, and resources Curate and adapt content across skill levels and use cases: writing, editing, and reworking material to make sure it's actually useful Track community health and engagement metrics, and use that data to make decisions, not just report on it Build the systems, playbooks, and processes that let all of this scale as the product and user base grow 💡 Qualifications Proven track record in building, scaling, and nurturing both B2B and B2C online communities. You haven't just managed communities, you've made th","datePosted":"2026-04-22","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io
50/stackblitz/jobs/4223647009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4223647009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
vendor: 1 bytes, line 50
50
51<script type="application/ld+json">{"@type":"JobPosting","title":"Engineering Manager - Front-End  (UI/UX)","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team behind WebContainers, the groundbreaking technology that made it possible to run Node.js right in your browser. No installs, no setup, just instant dev environments. That innovation kickstarted our journey in 2019 and powers the blazing-fast online IDE used by over a million developers every month. But we didn’t stop there. We took everything we learned from building WebContainers and used it to create Bolt.new. The fastest way to go from idea to production without writing traditional code. Think of it as the Canva or Figma for full-stack applications: a next-gen, AI-powered builder that lets you create, edit, and deploy web and mobile apps instantly, right in your browser. WebContainers make it possible. Bolt.new brings it to life. Together, they reimagine what it means to build software lowering the barrier to entry, speeding up workflows, and unlocking creativity for the next generation of builders. We’re a globally distributed, fully remote team of passionate engineers, designers, and creatives building the future of software development. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible we’d love to meet you. ✨ About This Opportunity A hands-on technical leader who balances coding with leadership. You'll own frontend architecture, code quality, and team growth - spending significant time writing code while developing engineers and coordinating across teams. 🛠️ How You'll Contribute&nbsp; Technical Leadership Own UI architecture: component patterns, state management, performance, and frontend infrastructure Write code regularly on complex features, migrations, and architectural improvements Review all UI PRs and maintain quality standards across the domain Drive technical initiatives and manage technical debt strategically Coordinate with other Team Leads on cross-domain architectural decisions Team Development Conduct performance reviews and own career development for UI team members Hold regular 1:1s to support growth, remove blockers, and provide feedback Make hiring decisions and address performance issues proactively Build a high-performing, autonomous team culture Coordination Assign team members across features based on skills, capacity, and priorities Participate in coordination meetings with other Team Leads Regular 1:1s with Head of Engineering to align on priorities and resources Run weekly UI team meetings for knowledge sharing AI-Native Development Champion AI coding tools and model effective usage Build AI-powered UI features and agent interaction patterns Dogfood Bolt.new daily and drive product improvements 💡 Qualifications Technical 10+ years frontend development, 3+ years leadership Expert in React, TypeScript, and modern frontend architecture Strong HTML, CSS (Tailwind), performance optimization Experience with design systems, state management, and complex UI flows Track record of architectural decisions balancing speed and maintainability Daily user of AI coding assistants (Cursor, Copilot, Claude) Experience building AI-integrated applications and interfaces Leadership Performance reviews, career development, and difficult conversations Growing engineers to senior/staff+ levels Hiring frontend engineers Building remote team culture Coaching mindset with balance of individual and business needs Skills Strong UX sensibilities and eye for detail (spacing, typography, animations) Experience with Figma/Sketch and translating designs to pixel-perfect interfaces Excellent communication with technical and non-technical stakeholders Pragmatic mindset: ship MVPs, bias toward action, match quality to business need Business thinking: tie technical work to clear outcomes Collaborative: knowledge sharing and unblocking others is core work 🎯 Bonus Points&nbsp; Web Workers, Web Components, Remix, browser dev environments Developer tooling or design tools experience Design engineering background Open-source contributions to UI libraries WebAssembly or advanced browser APIs 📌 A Few Notes You do not need a college degree to apply You do not need to be located in the U.S. — we’re remote-friendly You do not need to meet every qualification listed above","datePosted":"2026-02-06","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io
51/stackblitz/jobs/4119868009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4119868009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
vendor: 1 bytes, line 51
51
52<script type="application/ld+json">{"@type":"JobPosting","title":"Head of Finance","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team behind WebContainers, the groundbreaking technology that made it possible to run Node.js right in your browser. No installs, no setup, just instant dev environments. That innovation kickstarted our journey in 2019 and powers the blazing-fast online IDE used by over a million developers every month. But we didn’t stop there. We took everything we learned from building WebContainers and used it to create Bolt.new. The fastest way to go from idea to production without writing traditional code. Think of it as the Canva or Figma for full-stack applications: a next-gen, AI-powered builder that lets you create, edit, and deploy web and mobile apps instantly, right in your browser. WebContainers make it possible. Bolt.new brings it to life. Together, they reimagine what it means to build software lowering the barrier to entry, speeding up workflows, and unlocking creativity for the next generation of builders. We’re a globally distributed, fully remote team of passionate engineers, designers, and creatives building the future of software development. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible we’d love to meet you. ✨ About This Opportunity You'll be Bolt's 1st full-time finance leader, working directly with our COO to build out the function. This is a foundational role: you'll shape and own our processes for FP&amp;A and board-level reporting, quote-to-cash, deal desk, procurement, and sales compensation, and you'll partner with Product and Growth on pricing &amp; packaging. Finance at Bolt is not a back-office function. You'll get operationally involved in every department of the business, and because we're an AI-native company, you'll build agents to handle the repetitive work so you can spend your time on the decisions that move the numbers.&nbsp; You will be a key partner in building the operating backbone for Bolt's next stage of growth. 🛠️ How You'll Contribute&nbsp; Close &amp; Reporting: Run monthly/quarterly close, consolidations, and reconciliations; deliver a fast, accurate investor and board reporting pack (ARR/NRR, retention, cash). FP&amp;A Budgeting &amp; Forecasting: Establish quarterly budgets by team; drive rolling forecast and unit economic analysis; monitor plan vs. actual for the full P&amp;L. Revenue Recognition &amp; Controls: Author ASC 606 policies, review deals/terms, maintain control evidence; support both external audits and SOC 2 readiness. Quote-to-Cash: Define and maintain order flows, invoicing, collections, cash app, and credit risk processes; reduce DSO and exceptions across Rillet ⇄ Stripe ⇄ Snowflake ⇄ HubSpot. Pricing &amp; Packaging: Partner with Product and GTM to design, test, and measure monetization strategies; set up an ongoing experimentation loop. Deal Desk: Own guardrails and discount policy for sales-assisted deal
52s; lead non-standard approval processes with Product, GTM, and Legal. Compliance &amp; Audit: Support external audits and SOC 2 readiness led by the CTO; ensure consistent process hygiene. Primary Tools: Rillet (ERP), Stripe (billing/payments), Snowflake (data—via data team BI tools), HubSpot (CRM). 💡 Qualifications 8+ years in SaaS finance/accounting with end-to-end Q2C and revenue recognition ownership. Experience building/owning Deal Desk and sales compensation programs. Strong process and policy writer; effective operator across Product &amp; GTM CPA preferred, but not required Demonstrated ability to improve close time, forecasting, and DSO. Strong verbal and written English communication skills are required, as this role involves frequent collaboration with team members, stakeholders, and customers where English is the primary working language. 🎯 Bonus Points&nbsp; Experience in PLG monetization experiments. Familiarity with SOC 2 audit processes and evidence management. Hands-on integration experience among ERP, billing, data, and CRM systems. 📌 A Few Notes You do not need a college degree to apply You do not need to be located in the U.S. — we’re remote-friendly You do not need to meet every qualification listed above","datePosted":"2026-06-17","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io/stackblitz/jobs/4016046009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4016046009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
vendor: 1 bytes, line 52
52
53<script type="application/ld+json">{"@type":"JobPosting","title":"Senior Applied AI Engineer","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team that brought you WebContainers, the first-of-its-kind technology that made it possible to run Node.js right inside your browser. That breakthrough kicked off our journey in 2019, and it’s what powers the blazing-fast online IDE used by over 1 million developers every month. But we didn’t stop there. We doubled down on everything we learned and built&nbsp; Bolt.new — the fastest way to go from idea to production without writing traditional code. It’s a next-gen, AI-powered app builder that helps you create, edit, and deploy full-stack web and mobile apps instantly, right in your browser. No installs. No setup. Just smart automation and instant dev environments that let you move at the speed of thought. We’re a fully remote team, globally distributed, deeply collaborative, and seriously passionate about building the future of software development. This is your chance to join a small team with a big vision. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible, we’d love to meet you. ✨ About This Opportunity As a Senior Engineer on the AI team, you’ll build and improve the core coding AI agents that power Bolt.new. You’ll collaborate directly with product, design, engineering, and data to solve key agent challenges: evaluation harnesses, context compression, maintaining context across large codebases, orchestrating intuitive multi-step workflows, and tackling everything from simple UI tweaks to complex architectural decisions. This isn't API integration work. You'll be contributing to how AI reasons about and generates full-stack applications, working alongside experienced engineers in a problem space where the patterns are still being invented. Your work ships to millions of users building real products with Bolt.new every day. 🛠️ How You'll Contribute&nbsp; Build and ship state-of-the-art coding agents for Bolt.new (long-running workflows, context compression, agent loops, model routing). Own agent capabilities end-to-end: planning, tool use, co
53de generation/editing, and maintaining context across large codebases and multi-turn sessions. Integrate and evaluate multiple LLM providers (OpenAI, Anthropic, Google): benchmark quality/cost/latency; choose the right model for each task; and adopt new model features. Design safe, reliable tool-use and workflow orchestration (e.g., Vercel AI SDK, LangGraph): structured actions, guardrails, and production observability. Partner closely with product/design/engineering/data to deliver robust AI features; mentor and raise the bar for peers. Build and maintain evaluation harnesses and datasets; analyze failure modes and user feedback to continuously improve quality and reliability. 💡 Qualifications LLM Experience: Have shipped LLM agents to production environments used by thousands of users. Prompt Engineering: Deep understanding of prompt engineering best practices to guide LLM behavior. Able to craft, refine, and optimize prompts for different tasks and models. Software Engineering Skills: Solid software engineering fundamentals with experience in building production-ready systems. Autonomous Execution : Ability to deliver high-quality work with minimal oversight. Comfortable owning tasks end-to-e
53nd, managing complexity, and driving projects to completion within your functional area. Problem-Solving : Strong analytical and problem-solving skills with the ability to debug complex AI behaviors and bring clarity to ambiguous tasks. Data-Driven Mindset : Comfortable collecting, curating, and analyzing data to inform decisions. Able to build datasets, identify patterns in agent behavior, and translate findings into actionable improvements. Strong verbal and written English communication skills are required, as this role involves frequent collaboration with team members, stakeholders, and customers where English is the primary working language. 🎯 Bonus Points&nbsp; Machine Learning Background : Understanding of ML fundamentals and experience with model evaluation metrics. Open Source Contributions: Experience contributing to or maintaining open-source AI/ML projects. Research Background: Experience reading and implementing techniques from AI/ML research papers. 📌 A Few Notes You do not need a college degree to apply You do not need to be located in the U.S. — we’re remote-friendly You do&nbsp; not need to meet every qualification listed above","datePosted":"2025-06-25","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io/stackblitz/jobs/4005254009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4005254009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
vendor: 1 bytes, line 53
53
54<script type="application/ld+json">{"@type":"JobPosting","title":"Senior Software Engineer - Rails","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team that brought you WebContainers, the first-of-its-kind technology that made it possible to run Node.js right inside your browser. That breakthrough kicked off our journey in 2019, and it’s what powers the blazing-fast online IDE used by over 1 million developers every month. But we didn’t stop there. We doubled down on everything we learned and built Bolt.new — the fastest way to go from idea to production without writing traditional code. It’s a next-gen, AI-powered app builder that helps you create, edit, and deploy full-stack web and mobile apps instantly, right in your browser. No installs. No setup. Just smart automation and instant dev environments that let you move at the speed of thought. We’re a fully remote team, globally distributed, deeply collaborative, and seriously passionate about building the future of software development. This is your chance to join a small team with a big vision. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible, we’d love to meet you. ✨ About This Opportunity We’re looking for a Senior Ruby on Rails Engineer &nbsp; to help build the backend systems that power StackBlitz.com and Bolt.new. In this role, you’ll work on platform features, developer infrastructure, and integrations that enable millions of developers around the world to build software entirely in the browser. You’ll be part of a small, ambitious team focused on making software development more accessible to everyone. If you enjoy building robust systems, shipping high-quality code, and collaborating across teams — this could be the role for you. 🛠️ How You'll Contribute&nbsp; Improve the scalability, reliability, and performance of our Ruby on Rails application Design and optimize data storage using PostgreSQL and object storage services Build features for teams, organizations, and developer communities Develop integrations with external platforms to extend our product ecosystem Help review and shape code from peers to ensure quality, security, and best practices 💡 Qualifications 5+ years of professional experience with Ruby on Rails 5+ years of experience with PostgreSQL (or similar relational databases) Experience with AWS and modern cloud infrastructure Familiarity with CSS, SASS, HTML, and other templating/styling languages Experience building web applications with REST or GraphQL APIs Production experience with Node.js and TypeScript; this role works across both the Rails and Node/TypeScript stacks Strong communication skills — especially when explaining technical topics Strong verbal and written English communication skills are required, as this role involves frequent collaboration with team members, stakeholders, and customers where English is the primary working language. 🎯 Bonus Points&nbsp; Familiarity with Cloudflare Workers or edge infrastructure Interest in working with AI or LLMs Experience integrating with GitHub APIs or other version control systems 📌 A Few Notes You do not need a college degree to apply You do not need to be located in the U.S. — we’re remote-friendly You do&nbsp; not need to meet every qualification listed above. If this role excites you, we want to hear from you.","datePosted":"2025-06-25","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io/stackblitz/jobs/4006004009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4006004009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
vendor: 1 bytes, line 54
54
55<script type="application/ld+json">{"@type":"JobPosting","title":"Staff Applied AI Engineer","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team that brought you WebContainers, the first-of-its-kind technology that made it possible to run Node.js right inside your browser. That breakthrough kicked off our journey in 2019, and it’s what powers the blazing-fast online IDE used by over 1 million developers every month. But we didn’t stop there. We doubled down on everything we learned and built&nbsp; Bolt.new — the fastest way to go from idea to production without writing traditional code. It’s a next-gen, AI-powered app builder that helps you create, edit, and deploy full-stack web and mobile apps instantly, right in your browser. No installs. No setup. Just smart automation and instant dev environments that let you move at the speed of thought. We’re a fully remote team, globally distributed, deeply collaborative, and seriously passionate about building the future of software development. This is your chance to join a small team with a big vision. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible, we’d love to meet you. ✨ About This Opportunity As a Staff Engineer on the AI team, you'll lead the technical direction of the AI agents that turn natural language into production-ready applications. This means shaping how we work with LLMs to solve our hardest problems: maintaining context across large codebases, orchestrating multi-step workflows that feel intuitive, and handling everything from simple UI tweaks to complex architectural decisions. This isn't API integration work. You'll define the patterns and systems that govern how AI reasons about and generates full-stack applications, driving initiatives across multiple teams and influencing our broader AI strategy. Your work shapes the experience of millions of users building real products with Bolt every day. 🛠️ How You'll Contribute&nbsp; Define AI Agent Architecture: Set technical direction for how agents manage context, orchestrate workflows, and scale. Lead multi-model strategy: Build evals and selection criteria across providers (OpenAI, Anthropic, Google); partner with provider teams to test new capabilities. Build tool-use &amp; workflow foundations: Design safe, reliable interfaces for tool calling (search, queries, domain actions); evaluate frameworks (e.g., Vercel AI SDK, LangGraph) and set org best practices. Drive cross-team execution: Align product/design/engineering, resolve tradeoffs, and mentor engineers to raise AI engineering standards. Establish data &amp; evaluation standards: Own dataset methodology and the eval harness; turn failure modes and conversation insights into measurable improvements. Drive Research and Innovation: Experiment with prompting, context handling, and post-training; share learnings externally when appropriate. 💡 Qualifications Deep LLM experience: Built and scaled production LLM systems; strong grasp of capabilities, limits, and emergent behavior. Prompt engineering: Sets best practices and mentors across models and use cases. Software engineering: Strong fundamentals; designs scalable systems and makes pragmatic architectural calls. Strategic execution: Drives ambiguous, high-scope work end to end; influences across teams. Systems thinking: Spots process/communication/technical debt and improves team velocity. Model &amp; agent literacy: Tracks coding-agent/LLM advances; understands model tradeoffs and the agent lifecycle. Data-driven leadership: Builds data collection + eval harnesses; turns insights into measurable improvements. Strong verbal and written English communication skills are required, as this role involves frequent collaboration with team members, stakeholders, and customers where English is the primary working language. 🎯 Bonus Points&nbsp; Fine-tuning and alignment o f: LLMs (SFT, RLHF/RLAIF, DPO/ORPO) Machine Learning Background: Understanding of ML fundamentals and experience with model evaluation metrics. Open Source Contributions: Experience contributing to or maintaining open-source AI/ML projects. Research Backgroun
55d: Experience reading and implementing techniques from AI/ML research papers. External Presence: Experience speaking at conferences, publishing technical content, or representing an organization in industry forums. 📌 A Few Notes You do not need a college degree to apply You do not need to be located in the U.S. — we’re remote-friendly You do&nbsp; not need to meet every qualification listed above","datePosted":"2026-01-30","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io/stackblitz/jobs/4111216009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4111216009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
vendor: 1 bytes, line 55
55
56<script type="application/ld+json">{"@type":"JobPosting","title":"Staff Product Manager","description":"🚀 About Us We’re Bolt.new by StackBlitz! We’re the team that brought you WebContainers, the first-of-its-kind technology that made it possible to run Node.js right inside your browser. That breakthrough kicked off our journey in 2019, and it’s what powers the blazing-fast online IDE used by over 1 million developers every month. But we didn’t stop there. We doubled down on everything we learned and built Bolt.new — the fastest way to go from idea to production without writing traditional code. It’s a next-gen, AI-powered app builder that helps you create, edit, and deploy full-stack web and mobile apps instantly, right in your browser. No installs. No setup. Just smart automation and instant dev environments that let you move at the speed of thought. We’re a fully remote team, globally distributed, deeply collaborative, and seriously passionate about building the future of software development. This is your chance to join a small team with a big vision. If you love shipping fast, solving real problems, and pushing the boundaries of what’s possible, we’d love to meet you. ✨ About This Opportunity Reports to Head of Product Bolt is building the collaboration platform for the agentic era. Not a coding tool, not an AI wrapper, but a place where anyone with taste, judgment, and a problem worth solving can direct a team of AI agents to build real software and ship it. We're one of the fastest-growing products in software history, the scope ahead of us is enormous, and everyone we hire carries real weight. We're looking for a senior product IC to work at the system level. That means no one hands you a roadmap. You look across the whole product, work out where the real problems are, and go and fix them. Some weeks, that's chasing down something quietly eating retention. Some weeks, it's untangling a journey that sprawls across three surfaces no one quite owns, or making the call on how a new capability should behave everywhere it shows up. You'll work alongside a small product team that holds a high bar and moves quickly. This role suits someone who's done senior IC work at a product-led company, has genuine opinions about what makes software feel right, and does their best work when they're given room rather than a plan. If you're after a role where someone else defines the problems and hands you the steps, this isn't the one. 🛠️ How You'll Contribute&nbsp; Define product strategy for ambiguous, high-impact areas, making clear prioritization calls based on customer insight, product quality, business impact, and technical feasibility. Own complex, cross-surface initiatives from first diagnosis all the way to shipped. You own the work start to finish, not just scope it and pass it along. Live in the product the way a real user would, actually trying to build things, so you catch what's broken before customers do. Co-own the metrics that matter for your area, from activation and engagement through to retention and expansion, and use them to focus the team on the highest-leverage work. Lead discovery across users, customers, CX, Sales, and internal teams, turning qualitative signal, product usage data, and market context into sharp product judgment. Partner cross-functionally to navigate hard tradeoffs, create alignment, and ship high-impact work. Be responsible for the consistent look and feel of the product and ensure that it delivers value to different user groups, regardless of their technical abilities and use case. Turn what you're seeing into clear, written thinking the rest of the team can pick up and act on. Lift the standard of product thinking across the team, mostly by example. 💡 Qualifications 7+ years in product, with at least a few of those spent operating at this level of scope and ambiguity. You've done senior IC product work in a product-led company and know how to create leverage without needing a large team or a perfect plan. You understand product-led growth. You can trace the line from a single product lever, activation, onboarding, or the right feature in front of the right user, all the way through to retention and revenue, and you reach for those levers by instinct. You have strong product judgment. You can tell when something feels off, understand why, and turn that instinct into a clear product direction. You're comfortable in the technical weeds. You don't need to write the code, but you do need to hold an honest conversation with an engineer about tradeoffs. Familiarity with developer tools and AI products is a big plus. You take real ownership. You move with urgency and hold the bar high without waiting for permission or a lot of structure around you. Strong verbal and written English communication skills are required, as this role involves frequent collaboration with team members, stakeholders, and customers, with English as the primary working language. 🎯 Bonus Points You've built developer tools, AI products, or other technical products for a technical audience. You can still build","datePosted":"2026-06-03","employmentType":"FULL_TIME","directApply":false,"url":"https://job-boards.greenhouse.io
56/stackblitz/jobs/4260461009","identifier":{"@type":"PropertyValue","name":"Bolt.new","value":"4260461009"},"hiringOrganization":{"@id":"https://bolt.new/#organization"},"jobLocationType":"TELECOMMUTE"}</script>
56 <link rel="stylesheet" href="/public-page-assets/astro/blog.Dpb1UJgl.css">
57<style>.bolt-auth-frame{position:fixed;inset:0;width:100vw;max-width:none;height:100vh;height:100dvh;max-height:none;margin:0;padding:0;overflow:hidden;border:0;background:transparent;color-scheme:dark}.bolt-auth-frame:not([open]){display:block!important;position:fixed;inset:0;opacity:0;visibility:hidden;pointer-events:none}.bolt-auth-frame::backdrop{background:transparent}.bolt-auth-frame__iframe{display:block;width:100%;height:100%;border:0;background:transparent;color-scheme:dark}.bolt-auth-frame__iframe--pending{opacity:0}.bolt-auth-frame__pending{position:absolute;inset:0;display:grid;place-items:center;background:#00000073}.bolt-auth-frame__pending-spinner{width:32px;height:32px;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;animation:bolt-auth-frame-spin .7s linear infinite}@keyframes bolt-auth-frame-spin{to{transform:rotate(360deg)}}
58</style>
59<link rel="stylesheet" href="/public-page-assets/astro/blog.BKpcLbu-.css"></head> <body data-astro-cid-sckkx6r4> <!-- Google Tag Manager (noscript) --> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-58KJM59B" height="0" width="0" style="display:none;visibility:hidden" data-astro-cid-sckkx6r4></iframe></noscript>   <div class="careers-page min-h-screen flex flex-col"> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style>
59<script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event("astro:load"));})();</script>
59<script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script>
59<astro-island uid="ZCajf6" prefix="r3" component-url="/public-page-assets/astro/Header.Dv9qpu1R.js" component-export="Header" renderer-url="/public-page-assets/astro/client.BvbZLJKQ.js" props="{}" ssr client="load" opts="{&quot;name&quot;:&quot;Header&quot;,&quot;value&quot;:true}" await-children><div class="mkt-nav-sentinel" aria-hidden="true"></div><header class="mkt-nav-shell"><div><nav aria-label="Primary" data-orientation="horizontal" dir="ltr" class="mkt-nav mkt-nav--solid"><div class="mkt-nav-inner"><button type="button" class="mkt-nav-back" data-mobile-back="true" aria-label="Back to main menu" inert=""><svg viewBox="0 0 24 24" fill="none" class="mkt-nav-back-icon" aria-hidden="true"><path d="M15.75 19.5 8.25 12l7.5-7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>Back</button><a href="https://bolt.new" class="mkt-nav-logo" aria-label="bolt.new home"><svg viewBox="0 0 85 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M26.1121 20.9491C21.0582 20.9491 18.4869 17.9995 18.4869 14.3126C18.4869 9.9472 21.9744 5.99476 27.0283 5.99476C32.0822 5.99476 34.6535 8.94434 34.6535 12.6313C34.6535 16.9967 31.166 20.9491 26.1121 20.9491ZM26.319 16.2593C28.0923 16.2593 29.2154 14.6665 29.2154 12.8968C29.2154 11.481 28.2992 10.6846 26.8214 10.6846C25.0481 10.6846 23.925 12.2774 23.925 14.0471C23.925 15.4629 24.8412 16.2593 26.319 16.2593Z" fill="#fff"></path><path d="M40.0368 20.5952H34.717L39.0615 0.921487H44.3814L40.0368 20.5952Z" fill="#fff"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M10.4272 20.9491C8.80168 20.9491 7.20572 20.3592 6.28951 19.0909L5.96635 20.5867L0 23.75L0.644095 20.5867L4.9891 0.921487H10.309L8.77213 7.853C10.0134 6.49619 11.1661 5.99476 12.6438 5.99476C15.8358 5.99476 17.9637 8.08896 17.9637 11.9234C17.9637 15.8758 15.5107 20.9491 10.4272 20.9491ZM12.4665 13.0443C12.4665 14.873 11.1661 16.2593 9.48145 16.2593C8.53569 16.2593 7.6786 15.9053 7.11705 15.2859L7.94459 11.658C8.56524 11.0385 9.27456 10.6846 10.1021 10.6846C11.373 10.6846 12.4665 11.6285 12.4665 13.0443Z" fill="#fff"></path><path d="M49.9163 20.9491C46.8426 20.9491 44.6555 19.8283 44.6555 17.3506C44.6555 17.1442 44.6851 16.6427 44.7737 16.2298L45.9264 10.9501H43.562L44.5964 6.34871H46.9608L47.8179 2.45527L53.7735 0L53.1378 2.46127L52.2807 6.34871H55.1771L54.1426 10.9501H51.2462L50.4778 14.4306C50.4187 14.696 50.3892 14.991 50.3892 15.109C50.3892 15.7874 50.8029 16.2593 51.7191 16.2593C51.9851 16.2593 52.3693 16.1708 52.458 16.1118V20.3592C51.8965 20.7427 50.8916 20.9491 49.9163 20.9491Z" fill="#fff"></path><path d="M56.461 21.0087C55.7527 21.0087 55.1886 20.4196 55.1886 19.7127C55.1886 18.8879 55.897 18.1941 56.7234 18.1941C57.4317 18.1941 57.9958 18.7832 57.9958 19.4901C57.9958 20.3149 57.2875 21.0087 56.461 21.0087Z" fill="#fff"></path><path d="M65.3576 20.8386H62.9964L63.7309 17.5133C63.7572 17.3955 63.8097 17.186 63.8097 17.0289C63.8097 16.5969 63.4292 16.4398 63.0488 16.4398C62.5372 16.4398 62.183 16.7016 61.9338 16.9242L61.068 20.8386H58.7068L60.1104 14.5153H62.4716L62.3011 15.2354C62.7602 14.8164 63.3374 14.3582 64.295 14.3582C65.6199 14.3582 66.3807 15.0783 66.3807 16.0209C66.3807 16.1256 66.3414 16.4136 66.3151 16.5314L65.3576 20.8386Z" fill="#fff"></path><path d="M70.256 20.9956C68.2096 20.9956 66.7929 19.9745 66.7929 18.1155C66.7929 16.0994 68.3408 14.3582 70.5577 14.3582C72.1319 14.3582 73.5223 15.3139 73.5223 17.2384C73.5223 17.6704 73.4436 18.1679 73.3781 18.4035H69.1279V18.4166C69.1279 18.5083 69.5083 19.2021 70.4265 19.2021C70.925 19.2021 71.5153 19.0843 71.8039 18.8879L72.5254 20.4196C71.8826 20.8255 71.0169 20.9956 70.256 20.9956ZM69.3509 16.9111H71.4366V16.8718C71.4366 16.6885 71.2136 16.1518 70.4659 16.1518C69.8363 16.1518 69.4296 16.6362 69.3509 16.9111Z" fill="#fff"></path><path d="M81.7599 20.8386H79.1888L78.9265 17.3038L77.1293 20.8386H74.5582L74.1254 14.5153H76.4866L76.5521 17.9977L78.4411 14.5153H80.5531L80.881 17.9977L82.4945 14.5153H85L81.7599 20.8386Z" fill="#fff"></path></svg></a><div class="mkt-nav-list"><div style="position:relative"><ul data-orientation="horizontal" class="mkt-nav-items" dir="ltr"><li><button id="radix-:r3R6:-trigger-solutions" data-state="closed" aria-expanded="false" class="mkt-nav-link" data-radix-collection-item="">Solutions<svg viewBox="0 0 24 24" fill="none" class="mkt-nav-chev" aria-hidden="true"><path d="m19.5 8.25-7.5 7.5-7.5-7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></li><li><button id="radix-:r3R6:-trigger-resources" data-state="closed" aria-expanded="false" class="mkt-nav-link" data-radix-collection-item="">Resources<svg viewBox="0 0 24 24" fill="none" class="mkt-nav-chev" aria-hidden="true"><path d="m19.5 8.25-7.5 7.5-7.5-7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></button></li><li><a href="https://bolt.new/careers" class="mkt-nav-link" data-radix-collection-item="">Careers</a></li><li><a href="https://bolt.new/pricing" class="mkt-nav-link" data-radix-collection-item="">Pricing</a></li></ul></div></div><div class="mkt-nav-right"><div class="mkt-nav-actions"><button type="button" class="mkt-nav-signin" data-bolt-auth="signin" data-bolt-auth-trigger="header_sign_in">Sign in</button><button type="button" class="mkt-nav-cta hero-btn-primary" data-bolt-auth="signup" data-bolt-auth-trigger="header_get_started"><div class="btn-bg-hover"></div><div class="btn-text-wrap"><div class="btn-text-inner"><span>Get Started</span><span>Get Started</span></div></div></button></div><button type="button" class="mkt-nav-burger" aria-label="Open menu" aria-expanded="false" aria-controls="mkt-nav-mobile"><span></span><span></span><span></span></button></div></div><div class="mkt-nav-viewport-wrap"></div></nav><div class="mkt-nav-scrim" aria-hidden="true"></div><div class="mkt-nav-mobile" id="mkt-nav-mobile" aria-hidden="true" inert=""><div class="mkt-nav-mobile-body"><div class="mkt-nav-mobile-view is-active" data-view="home"><button class="mkt-nav-mobile-item" data-drill="solutions">Solutions<svg viewBox="0 0 24 24" fill="none" class="mkt-nav-mobile-chev" aria-hidden="true"><path d="m8.25 4.5 7.5 7.5-7.5 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><button class="mkt-nav-mobile-item" data-drill="resources">Resources<svg viewBox="0 0 24 24" fill="none" class="mkt-nav-mobile-chev" aria-hidden="true"><path d="m8.25 4.5 7.5 7.5-7.5 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></button><a class="mkt-nav-mobile-item" href="https://bolt.new/careers">Careers</a><a class="mkt-nav-mobile-item" href="https://bolt.new/pricing">Pricing</a></div><div class="mkt-nav-mobile-view" data-view="solutions" inert=""><a href="https://bolt.new/solutions/ai-for-small-business" class="mkt-nav-mobile-feature"><p class="mkt-nav-mobile-feature-title">AI for Small Business<!-- --> →</p><p class="mkt-nav-mobile-feature-desc">Custom apps for your business, built in an afternoon — no code, no developer, no $100K project.</p></a><div class="contents"><p class="mkt-nav-mobile-heading">Top Use Cases</p><div class="mkt-nav-mobile-links"><a href="https://bolt.new/use-cases/ai-app-builder">
59AI App Builder</a><a href="https://bolt.new/use-cases/ai-website-builder">AI Website Builder</a><a href="https://bolt.new/use-cases/custom-crm-software">SaaS Dev</a><a href="https://bolt.new/use-cases/ai-prototyping-tools">Prototyping</a></div></div><div class="contents"><p class="mkt-nav-mobile-heading">Business Type</p><div class="mkt-nav-mobile-links"><a href="https://bolt.new/solutions/ai-for-small-business">Small Business</a><a href="https://bolt.new/enterprise">Enterprise</a></div></div></div><div class="mkt-nav-mobile-view" data-view="resources" inert=""><a href="https://bolt.new/resources/templates" class="mkt-nav-mobile-feature"><p class="mkt-nav-mobile-feature-title">Bolt Templates<!-- --> →</p><p class="mkt-nav-mobile-feature-desc">Production-ready starting points — dashboards, storefronts, portfolios, and more. Fork one and make it yours.</p></a><div class="contents"><p class="mkt-nav-mobile-heading">Support</p><div class="mkt-nav-mobile-links"><a href="https://discord.com/invite/stackblitz" target="_blank" rel="noopener">Community</a><a href="https://support.bolt.new" target="_blank" rel="noopener">Help Center</a><a href="https://bolt.new/platform/security">Security &amp; Trust</a></div></div><div class="contents"><p class="mkt-nav-mobile-heading">Build and Learn</p><div class="mkt-nav-mobile-links"><a href="https://bolt.new/blog">Blog</a><a href="https://bolt.new/resources/templates">Templates</a></div></div></div></div><div class="mkt-nav-mobile-footer"><button type="button" class="hero-btn-primary" data-bolt-auth="signup" data-bolt-auth-trigger="header_get_started"><div class="btn-bg-hover"></div><div class="btn-text-wrap"><div class="btn-text-inner"><span>Get Started</span><span>Get Started</span></div></div></button><button type="button" class="mkt-nav-mobile-signin" data-bolt-auth="signin" data-bolt-auth-trigger="header_sign_in">Sign in</button></div></div></div></header><!--astro:end--></astro-island> <main class="careers-main"><!-- ============================================================================
60     CAREERS — page body.
61     Ported from the garyliusf/Enterprise sandbox (marketing/careers.html).
62
63     Self-contained by design, matching get-started-body.html: this repo has no
64     shared-components.css/js, so both are inlined below rather than linked.
65     Order is deliberate and mirrors the sandbox — page CSS first, then markup,
66     then the shared component CSS (it must win the cascade), then page JS, then
67     the shared JS the page calls into.
68
69     Nav and footer are intentionally absent: Layout.astro supplies Header and
70     Footer. Page-level background/font/overflow live on .careers-main rather
71     than body, so nothing here can affect the global chrome.
72     ============================================================================ -->
73
74<style>
75  /* Page-level rules scoped to the wrapper — body belongs to Layout.astro. */
76  .careers-main {
77    background: #000;
78    font-family: 'Inter', sans-serif;
79    /* clip NOT hidden: hidden makes this a scroll container and breaks
80       position:sticky on the site header. */
81    overflow-x: hidden;
82    overflow-x: clip;
83  }
84  html { scroll-behavior: smooth; }   /* the hero CTA jumps to #open-roles */
85</style>
86
87<style>
88  .sr-only {
89    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
90    overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
91  }
92
93  /* ─── SECTION SCAFFOLDING ─────────────────────────────────────────────── */
94  .section { width: 100%; padding: clamp(80px, 12vh, 140px) 0; position: relative; }
95  .section-inner { width: min(1200px, calc(100vw - 160px)); margin: 0 auto; position: relative; z-index: 2; }
96  .section-header { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; margin-bottom: 56px; }
97  .section-header--center { align-items: center; text-align: center; margin-left: auto; margin-right: auto; max-width: 760px; }
98  /* 640px clipped one-liners into two by a few characters; there's room. */
99  /* 640px = the solutions.html standard measure; 780 ran long enough to
100     orphan single words on two-line subs */
101  .section-sub { font-size: clamp(15px, 1.1vw, 17px); line-height: 1.65; color: rgba(255,255,255,0.5); max-width: 640px; margin: 0; }
102  .section-header--center .section-sub { max-width: 620px; }
103
104  /* ─── HERO — full-bleed video ─────────────────────────────────────────── */
105  /* Short variant per CLAUDE.md (min(65vh,720px)): the hero is now H1 + sub +
106     CTAs only — the film moved out from under it into its own section, so the
107     tall 80vh variant would leave dead space above the fold. */
108  .hero-section {
109    position: relative; width: 100%;
110    /* No min-height: with this hero's short stack (H1 + sub + one CTA +
111       meta), reserving 65vh parks slack somewhere — centered it pushed the
112       H1 below every sibling page's, and top-pinned it opened a tall dead
113       gap between the meta line and the film card. The film sits flush
114       under and continues the hero visually, so the section hugs its
115       content: H1 rides the shared top-pad clamp, and the content-to-film
116       gap is exactly the bottom pad at every viewport (76-108px; Gary
117       sized it by eye, 2026-08-24). (Mobile already
118       sizes to content for the same reason.) */
119    display: flex; flex-direction: column; justify-content: flex-start;
120    overflow: hidden; background: #000;
121    padding: clamp(90px, calc(24px + 8.5vh), 148px) 0 clamp(76px, calc(6vh + 20px), 108px);
122  }
123  #hero-pixel-canvas { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; opacity: 0.5; }
124  .hero-inner {
125    position: relative; z-index: 2;
126    width: min(1200px, calc(100vw - 160px)); margin: 0 auto;
127    display: flex; flex-direction: column; align-items: center; text-align: center;
128    gap: 20px;
129  }
130  .hero-h1 {
131    font-size: 66px; font-weight: 500; color: #fff; line-height: 1.1;
132    letter-spacing: normal; margin: 0; max-width: 900px;
133  }
134  /* Keyword shimmer — .hero-h1-kw gradient from marketing/templates/index.html
135     (white -> #1488FC -> #7FDDC1 -> white). The sweep is compressed to the
136     first 28% of the 2.9s cycle (~0.8s) so each phrase flashes briefly instead
137     of glowing for most of the cycle — the colored band is ~70% of the element
138     wide, so with the original 83% sweep both phrases sat lit simultaneously
139     and the follow delay was invisible. The second phrase trails by 0.6s:
140     "Reimagine" flashes, then "Bolt.new" catches it, then a quiet beat
141     before the loop repeats. The static background-position parks the band
142     past the text so the follow phrase rests white during its pre-delay. */
143  .hero-h1-kw {
144    background: linear-gradient(105deg, #fff 0%, #fff 34%, #1488FC 45%, #7FDDC1 55%, #fff 66%, #fff 100%);
145    background-size: 220% 100%;
146    background-position: -20% 50%;
147    -webkit-background-clip: text; background-clip: text;
148    -webkit-text-fill-color: transparent; color: transparent;
149    animation: kwShimmer 2.9s cubic-bezier(0.45,0,0.25,1) infinite;
150  }
151  .hero-h1-kw--follow { animation-delay: 0.6s; }
152  @keyframes kwShimmer { 0% { background-position: 120% 50%; } 28%, 100% { background-position: -20% 50%; } }
153  @media (prefers-reduced-motion: reduce) {
154    .hero-h1-kw { animation: none; -webkit-text-fill-color: #fff; color: #fff; }
155  }
156
157  .hero-subtitle {
158    font-size: clamp(16px, 1.4vw, 20px); font-weight: 400; color: #ABABAB;
159    line-height: 1.4; margin: 0; max-width: 620px;
160  }
161  .hero-btn-group { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
162  .hero-btn-group .hero-btn-primary,
163  .hero-btn-group .hero-btn-ghost { min-width: var(--cta-min-w, 180px); }
164  /* Live role counter — real data from the Greenhouse board, not a claim */
165  .hero-meta {
166    display: flex; align-items: center; justify-content: center; gap: 10px;
167    font-size: 12px; color: rgba(255,255,255,0.45); margin-top: 4px;
168    font-family: 'Inter', sans-serif; font-weight: 500;
169    letter-spacing: 0.9px; text-transform: uppercase;
170    min-height: 16px;
171  }
172  .hero-meta-dot {
173    width: 6px; height: 6px; border-radius: 50%; background: #29C57A;
174    box-shadow: 0 0 0 3px rgba(41,197,122,0.18); flex-shrink: 0;
175  }
176
177  /* ─── MISSION ─────────────────────────────────────────────────────────── */
178
179  /* ─── VALUES ──────────────────────────────────────────────────────────── */
180  .values-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
181  /* 5 values in a 3-col grid: last two centre on the second row via a 6-col span trick */
182  .values-grid { grid-template-columns: repeat(6, 1fr); }
183  .value-card { grid-column: span 2; }
184  .value-card:nth-child(4) { grid-column: 2 / span 2; }
185  .value-card:nth-child(5) { grid-column: 4 / span 2; }
186  .value-card {
187    border: 1px solid rgba(255,255,255,0.10); border-radius: 2px;
188    padding: 24px 24px 28px;
189    background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.005) 100%);
190    transition: border-color 0.3s ease;
191    display: flex; flex-direction: column; gap: 14px;
192  }
193  .value-card:hover { border-color: rgba(255,255,255,0.22); }
194
195  /* ─── CARD HOVER PIXEL FIELD ──────────────────────────────────────────── */
196  /* Canonical component, ported from solutions/ai-for-real-estate's
197     .template-hover-canvas. Canvas is injected by JS at z-index 0; card
198     content must sit above it, hence the :not() child rules. */
199  .card-hover-canvas {
200    position: absolute; inset: 0; width: 100%; height: 100%;
201    pointer-events: none; opacity: 0; transition: opacity 0.45s ease; z-index: 0;
202  }
203  .value-card { position: relative; overflow: hidden; }
204  .value-card:hover .card-hover-canvas { opacity: 1; }
205  .value-card > *:not(.card-hover-canvas) { position: relative; z-index: 1; }
206  .value-num {
207    font-family: 'Silkscreen', monospace; font-size: 11px; letter-spacing: 1.5px;
208    color: #1488FC;
209  }
210  .value-title { font-size: 18px; font-weight: 600; color: #fff; line-height: 1.35; letter-spacing: -0.2px; margin: 0; }
211  .value-desc { font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.6; margin: 0; }
212
213  /* ─── FILM ────────────────────────────────────────────────────────────── */
214  /* Flush under the hero — no top padding, so the player reads as part of the
215     hero block rather than as a separate band. */
216  .film-section { width: 100%; padding: 0 0 clamp(80px, 12vh, 140px); position: relative; }
217  .film-card {
218    position: relative; width: 100%; line-height: 0;
219    border: 1px solid rgba(255,255,255,0.10); border-radius: 2px;
220    overflow: hidden; background: #050505;
221  }
222  .film-card { cursor: pointer; }
223  .film-card .hero-video-area { border-radius: 2px; }
224  .film-poster-img {
225    position: absolute; inset: 0; width: 100%; height: 100%;
226    object-fit: cover; display: block; z-index: 1;
227    transition: opacity 0.6s ease;
228  }
229  /* Ambient loop above the poster, below the vignette and play button. Fades
230     up once it's actually playing so there's no black gap while it buffers. */
231  .film-preview {
232    position: absolute; inset: 0; width: 100%; height: 100%;
233    object-fit: cover; display: block; z-index: 1;
234    opacity: 0; transition: opacity 0.6s ease; pointer-events: none;
235  }
236  .film-card.is-previewing .film-preview { opacity: 1; }
237  .film-card.is-previewing .film-poster-img { opacity: 0; }
238
239  /* ─── TEAM GALLERY (floating stagger) ─────────────────────────────────── */
240  /* Chose a staggered grid over a slider: the film directly above already
241     autoplays and six pixel canvases are animating, so another moving rail
242     competes; and a rail would hide 6 of 10 photos behind interaction nobody
243     performs on a careers page. Rhythm alternates 2 / 3 / 2 / 3 across a 6-col
244     grid with per-item vertical offsets, so ~2-3 read at a time and the block
245     floats on the page instead of sitting in a frame. */
246  /* Bottom pad matches the values section's bottom pad above (same clamp), so
247     the seams either side of the gallery stay equal at every viewport height.
248     At 8vh the gap below measured 142px against 176px above. */
249  .teamshot-section { position: relative; padding: clamp(24px, 4vh, 48px) 0 clamp(80px, 12vh, 140px); overflow: hidden; }
250  /* padded frame gives the pixel border a band to occupy around the grid */
251  .teamshot-frame { position: relative; padding: clamp(18px, 2.6vw, 40px); }
252  #teamshot-pixels { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; }
253  @media (prefers-reduced-motion: reduce) { #teamshot-pixels { display: none; } }
254
255  /* Uniform 5x2 grid: equal tiles on a shared baseline. The staggered version
256     (unequal spans + per-item vertical offsets) read as messy rather than
257     editorial, so alignment does the work and the photos stay the subject.
258     Ten images divide evenly into 5 columns — 3-up would leave a ragged row. */
259  .gallery {
260    position: relative; z-index: 1;
261    display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px;
262  }
263  .gallery-item { margin: 0; border-radius: 2px; overflow: hidden; background: #0a0a0a; line-height: 0; }
264  .gallery-item img {
265    /* 5:4 rather than the source 3:2 — taller, boxier tiles. object-fit cover
266       crops ~17% off the sides of the 900x600 originals; square would take a
267       third, which is why this stops at 5:4. */
268    display: block; width: 100%; height: auto; aspect-ratio: 5 / 4; object-fit: cover;
269    opacity: 0.85; transition: opacity 0.35s ease, transform 0.5s cubic-bezier(0.22,1,0.36,1);
270  }
271  .gallery-item:hover img { opacity: 1; transform: scale(1.03); }
272
273  /* ─── GALLERY LIGHTBOX ────────────────────────────────────────────────── */
274  .gallery-item { cursor: pointer; }
275  .gallery-item:focus-visible { outline: 2px solid #1488FC; outline-offset: 3px; }
276  .lightbox {
277    position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,0.94);
278    display: flex; align-items: center; justify-content: center; gap: clamp(8px, 2vw, 28px);
279    padding: clamp(16px, 4vw, 48px);
280    /* Closed state is visibility, not display: display can't be transitioned.
281       The 0s visibility transition is DELAYED by the fade duration on close so
282       the element stays hittable/visible while it fades, then snaps out of the
283       a11y tree and pointer flow. On open the delay is 0 so it appears at once. */
284    opacity: 0; visibility: hidden;
285    transition: opacity 0.26s ease, visibility 0s linear 0.26s;
286  }
287  .lightbox.is-open { opacity: 1; visibility: visible; transition: opacity 0.26s ease, visibility 0s; }
288  .lightbox-stage {
289    margin: 0; display: flex; flex-direction: column; align-items: center; gap: 14px; min-width: 0;
290    /* the stage lifts and settles rather than the whole overlay scaling, so the
291       backdrop fade stays flat and only the photo moves */
292    opacity: 0; transform: scale(0.975) translateY(8px);
293    transition: opacity 0.26s ease, transform 0.36s cubic-bezier(0.22, 1, 0.36, 1);
294  }
295  .lightbox.is-open .lightbox-stage { opacity: 1; transform: none; }
296  .lightbox-btn { opacity: 0; transition: opacity 0.3s ease 0.06s, background 0.25s ease, border-color 0.25s ease; }
297  .lightbox.is-open .lightbox-btn { opacity: 1; }
298  @media (prefers-reduced-motion: reduce) {
299    .lightbox, .lightbox-stage, .lightbox-btn { transition-duration: 0.01s; }
300    .lightbox-stage { transform: none; }
301    .lightbox-stage img { animation-duration: 0.01s !important; }
302  }
303  .lightbox-stage img {
304    display: block; max-width: min(1100px, 100%); max-height: 78vh;
305    width: auto; height: auto; border-radius: 2px;
306  }
307  /* Stepping between photos: the incoming shot drifts in from the side you're
308     heading toward (opening the lightbox doesn't animate — the stage itself
309     already scales in). JS re-adds the class per click; the reflow it forces
310     restarts the animation when the direction repeats. */
311  .lightbox-stage img.lb-anim-next { animation: lbInNext 0.32s cubic-bezier(0.25,0.46,0.45,0.94); }
312  .lightbox-stage img.lb-anim-prev { animation: lbInPrev 0.32s cubic-bezier(0.25,0.46,0.45,0.94); }
313  @keyframes lbInNext { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } }
314  @keyframes lbInPrev { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: none; } }
315  .lightbox-count {
316    font-family: 'Silkscreen', monospace; font-size: 11px; letter-spacing: 2px;
317    color: rgba(255,255,255,0.45);
318  }
319  .lightbox-btn {
320    flex-shrink: 0; width: 44px; height: 44px; border-radius: 2px; cursor: pointer;
321    background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14);
322    color: #fff; display: flex; align-items: center; justify-content: center;
323    transition: background 0.25s ease, border-color 0.25s ease;
324  }
325  .lightbox-btn:hover { background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.3); }
326  .lightbox-btn svg { width: 20px; height: 20px; }
327  .lightbox-close { position: absolute; top: clamp(16px, 3vw, 32px); right: clamp(16px, 3vw, 32px); }
328  @media (max-width: 768px) {
329    /* arrows move below the image so the photo gets the full width */
330    /* Wrapped rows defaulted to align-content: stretch, which pushed the
331       arrows to the bottom of the viewport, far from the photo. Centre the
332       rows and set an explicit row gap so the controls sit just under it. */
333    .lightbox { flex-wrap: wrap; align-content: center; row-gap: 31px; }
334    .lightbox-stage { order: 1; flex: 1 0 100%; margin-bottom: 0; }
335    .lightbox-prev { order: 2; } .lightbox-next { order: 3; }
336    .lightbox-stage img { max-height: 62vh; }
337  }
338
339  /* ─── TESTIMONIALS (vertical cards) ───────────────────────────────────── */
340  /* Portrait card: grayscale photo on top, copy beneath. Two per row, so the
341     set grows sideways then wraps instead of adding a full-width band per
342     person. Photo sits above via order:-1 — markup keeps the quote first so
343     it still reads correctly without CSS and to screen readers. */
344  .testi-section { padding: clamp(24px, 4vh, 48px) 0 clamp(80px, 12vh, 140px); }
345  /* Four testimonials now — a scroll-snap slider instead of the old 3-col
346     grid. ~3 cards + a visible sliver of the 4th fit the row; the sliver, a
347     subtle right-edge fade, and the arrows carry the overflow cue
348     (a heavier full-black 90px fade was tried and rejected).
349     stretch, not start: `start` sized each card to its own quote, so a longer
350     one made a taller card and the attributions sat at different heights.
351     Stretching equalises the row; .testi-content's flex:1 plus .testi-attr's
352     margin-top:auto then pins every name to the same baseline. */
353  /* isolation + the translateZ on ::after below: Safari promotes the
354     overflow-scroller's content (the filtered photos especially) to its own
355     compositing layer, which paints ABOVE the fade overlay despite z-index —
356     the text dimmed but the photo ran bright to the clip edge. Isolating the
357     stacking context and giving the overlay its own layer restores the
358     spec order in WebKit; no effect in Chrome. */
359  .testi-slider { position: relative; isolation: isolate; }
360  /* No snap on desktop: with only a sliver of the 4th card overflowing, the
361     nearest mandatory snap point is past the scroll range and Chrome bounces
362     every scroll back to 0. Mobile (85% cards, real snap points) re-enables
363     it below. */
364  .testi-stack {
365    display: flex; gap: 20px; align-items: stretch;
366    overflow-x: auto;
367    scrollbar-width: none; -ms-overflow-style: none;
368  }
369  .testi-stack::-webkit-scrollbar { display: none; }
370  .testi-stack .testi-panel { flex: 0 0 calc((100% - 60px) / 3.35); scroll-snap-align: start; }
371  /* Subtle right-edge fade while content remains off-screen. Eased ramp:
372     stays light for most of its span and only plunges in the last ~20%, so
373     the sliver reads as gently dimmed rather than swallowed — but it MUST
374     land on solid #000: the container clips the sliver at its right edge,
375     and any fade that stops short leaves a visible seam where the dimmed
376     card meets the pure-black page. JS drops it (is-end) once the slider
377     is scrolled all the way. */
378  .testi-slider::after {
379    content: ''; position: absolute; top: 0; right: 0; bottom: 0; width: 72px;
380    background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.22) 50%, rgba(0,0,0,0.6) 82%, #000);
381    pointer-events: none; z-index: 3; transform: translateZ(0);
382    opacity: 1; transition: opacity 0.3s ease;
383  }
384  .testi-slider.is-end::after { opacity: 0; }
385  /* prev/next — ghost-button DNA (0.22 border → 0.4 + white on hover) */
386  .testi-nav { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; }
387  .testi-arrow {
388    width: 38px; height: 38px; border-radius: 2px;
389    border: 1px solid rgba(255,255,255,0.22); background: transparent;
390    color: rgba(255,255,255,0.8); display: flex; align-items: center; justify-content: center;
391    cursor: pointer; padding: 0;
392    transition: border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
393  }
394  .testi-arrow:hover { border-color: rgba(255,255,255,0.4); color: #fff; }
395  .testi-arrow[disabled] { opacity: 0.35; cursor: default; pointer-events: none; }
396  .testi-arrow svg { width: 16px; height: 16px; display: block; }
397  .testi-panel {
398    position: relative; overflow: hidden;
399    background: #0d0d0d; border: 1px solid rgba(255,255,255,0.07); border-radius: 2px;
400    display: flex; flex-direction: column;
401  }
402  .testi-panel canvas.testi-pixels {
403    position: absolute; inset: 0; width: 100%; height: 100%;
404    pointer-events: none; z-index: 0; opacity: 0.65;
405  }
406  .testi-photo { position: relative; overflow: hidden; background: #0a0a0a; order: -1; aspect-ratio: 3 / 2; }
407  .testi-photo img {
408    width: 100%; height: 100%; object-fit: cover; object-position: center 22%;
409    filter: grayscale(100%) contrast(1.05); display: block;
410    transition: filter 0.4s ease;
411  }
412  /* hover reveals the colour photo — hover anywhere on the card, not just
413     the image, so the moment isn't lost to a small target */
414  .testi-panel:hover .testi-photo img { filter: grayscale(0%) contrast(1); }
415  /* scrim fades the photo's lower edge into the copy below it */
416  .testi-photo::after {
417    content: ''; position: absolute; inset: 0;
418    background: linear-gradient(to bottom, transparent 55%, rgba(13,13,13,0.85) 100%);
419  }
420  /* Portrait not on disk yet: drop the photo and let the copy stand alone
421     rather than leaving a dead block. Self-heals when the file exists. */
422  .testi-photo.is-empty { display: none; }
423  .testi-content {
424    padding: 30px 32px 32px; display: flex; flex-direction: column;
425    gap: 20px; position: relative; z-index: 2; flex: 1;
426  }
427  /* .testi-panel prefix is required, not cosmetic: shared-components.css loads
428     at end of body and its 0-1-0 .section-eyebrow rule would otherwise beat a
429     bare .testi-eyebrow and force this back to 16px. */
430  /* Grey, not brand blue. CLAUDE.md's "eyebrows are always #1488FC" governs
431     SECTION eyebrows; this is a card label naming the team, so it takes the
432     same treatment as solutions.html's .int-tile-tag. The .testi-panel prefix
433     is required — shared-components.css loads after this and its
434     .section-eyebrow rule would otherwise force it back to blue. */
435  .testi-panel .testi-eyebrow { font-size: 13px; margin: 0; color: rgba(255,255,255,0.4); }
436  .testi-quote {
437    font-family: 'Cormorant Garamond', Georgia, serif;
438    /* #ABABAB matches .testi-name — the documented body-text grey */
439    font-size: clamp(16px, 1.5vw, 18px); font-weight: 300; color: #ABABAB;
440    line-height: 1.5; font-style: normal; margin: 0;
441  }
442  .testi-attr { display: flex; flex-direction: column; gap: 3px; margin-top: auto; padding-top: 8px; }
443  /* documented greys: #ABABAB body text, 0.5 for the secondary line */
444  .testi-name { font-size: 14px; font-weight: 500; color: #ABABAB; }
445  .testi-role { font-size: 13px; color: rgba(255,255,255,0.5); }
446
447  /* ─── PERKS + COMP ────────────────────────────────────────────────────── */
448  .perks-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 24px; }
449  /* Exact .unlock-card spec from solutions.html: no border, flat dark panel
450     that stands off the black page. Border-color hover is gone with the
451     border — the flashlight is the hover state. */
452  .perk-card {
453    border: none; border-radius: 2px; padding: 24px 24px 28px;
454    background:
455      linear-gradient(180deg, rgba(255,255,255,0.045) 0%, rgba(255,255,255,0.012) 34%, rgba(255,255,255,0) 62%),
456      #09090b;
457    display: flex; flex-direction: column; gap: 12px; min-height: 180px;
458    position: relative; overflow: hidden;
459  }
460  /* Flashlight — soft radial glow follows the cursor (JS sets --mx/--my).
461     Same values as solutions.html's .unlock-card. */
462  .perk-card::before {
463    content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
464    background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%), rgba(150,185,255,0.14), rgba(150,185,255,0) 60%);
465    opacity: 0; transition: opacity 0.35s ease;
466  }
467  .perk-card:hover::before { opacity: 1; }
468  .perk-card > * { position: relative; z-index: 1; }
469  /* Icon tile matches the canonical spec used by .builtin-card-icon /
470     .trust-check-icon — grey glyph, not brand blue. */
471  .perk-icon {
472    width: 44px; height: 44px; border-radius: 2px; flex-shrink: 0; margin-bottom: 6px;
473    background: rgba(255,255,255,0.05);
474    border: 1px solid rgba(255,255,255,0.08); display: inline-flex; align-items: center; justify-content: center;
475    color: rgba(255,255,255,0.85);
476  }
477  .perk-icon svg { width: 20px; height: 20px; }
478  .perk-title { font-size: 18px; font-weight: 600; color: #fff; line-height: 1.35; letter-spacing: -0.2px; margin: 0; }
479  .perk-desc { font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.55; margin: 0; }
480
481
482  /* ─── OPEN ROLES ──────────────────────────────────────────────────────── */
483  .roles-controls { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 52px; }
484  .roles-select {
485    flex: 0 0 auto; align-self: flex-start;
486    height: 44px; padding: 0 36px 0 14px; background: rgba(255,255,255,0.04);
487    border: 1px solid rgba(255,255,255,0.12); border-radius: 2px; color: #fff;
488    font-family: 'Inter', sans-serif; font-size: 15px; cursor: pointer;
489    appearance: none;
490    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
491    background-repeat: no-repeat; background-position: right 12px center; background-size: 16px;
492  }
493  .roles-select:focus { outline: none; border-color: rgba(20,136,252,0.6); }
494  .roles-select option { background: #111; color: #fff; }
495
496  .roles-pills-wrap { position: relative; flex: 1 1 auto; min-width: 0; }
497  /* Right-edge fade — signals more team filters off-screen when the row
498     overflows (same affordance as the blog filter bar / platform tab bar).
499     JS toggles .at-end when scrolled fully right. */
500  .roles-pills-wrap::after {
501    content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 72px;
502    pointer-events: none; z-index: 2; opacity: 0; transition: opacity 0.25s ease;
503    background: linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.55) 35%, rgba(0,0,0,0.88) 65%, #000 88%);
504  }
505  .roles-pills-wrap.is-scrollable::after { opacity: 1; }
506  .roles-pills-wrap.at-end::after { opacity: 0; }
507  .roles-pills {
508    display: flex; gap: 8px; overflow-x: auto; overflow-y: hidden;
509    touch-action: pan-x; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch;
510    /* Scrollbar only when the row ACTUALLY overflows (Gary, 2026-08-24):
511       the base hides it — the 14px pill font means the full team list fits
512       at desktop width — and the .is-scrollable class (set by the same JS
513       that drives the edge fade) restores the styled 6px bar + its gutter
514       on narrower screens where the row really scrolls. Class-driven, not a
515       viewport breakpoint, so any width that overflows gets the bar.
516       scrollbar-width flips to auto there because Chrome 121+ ignores
517       ::-webkit-scrollbar styling while a non-initial standard scrollbar
518       property is in effect. 6px is a floor — below ~4px Chrome reverts to
519       an overlay bar that reserves no space. */
520    scrollbar-width: none; -ms-overflow-style: none;
521  }
522  .roles-pills::-webkit-scrollbar { display: none; }
523  .roles-pills-wrap.is-scrollable .roles-pills { scrollbar-width: auto; padding-bottom: 10px; }
524  .roles-pills-wrap.is-scrollable .roles-pills::-webkit-scrollbar { display: block; height: 6px; }
525  .roles-pills-wrap.is-scrollable .roles-pills::-webkit-scrollbar-track { background: rgba(255,255,255,0.045); border-radius: 999px; }
526  .roles-pills-wrap.is-scrollable .roles-pills::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.20); border-radius: 999px; }
527  .roles-pills-wrap.is-scrollable .roles-pills:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.34); }
528  .roles-pills-wrap.is-scrollable::after { bottom: 10px; }
529  .role-pill {
530    flex: 0 0 auto; height: 44px; padding: 0 18px; border-radius: 2px;
531    display: inline-flex; align-items: center;
532    background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.10);
533    color: rgba(255,255,255,0.6); font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 500;
534    cursor: pointer; transition: all 0.22s ease; white-space: nowrap;
535  }
536  .role-pill:hover { color: #fff; border-color: rgba(255,255,255,0.28); }
537  .role-pill.is-active { background: #1488FC; border-color: #1488FC; color: #fff; }
538  .role-pill-count { opacity: 0.6; margin-left: 6px; font-variant-numeric: tabular-nums; }
539
540  .roles-group { margin-bottom: 40px; }
541  .roles-group:last-child { margin-bottom: 0; }
542  .roles-group-label {
543    font-family: 'Silkscreen', monospace; font-size: 11px; letter-spacing: 2px;
544    text-transform: uppercase; color: rgba(255,255,255,0.4); margin: 0 0 14px;
545  }
546  .roles-list { border: 1px solid rgba(255,255,255,0.10); border-radius: 2px; overflow: hidden; }
547  .role-row {
548    display: grid; grid-template-columns: 1fr auto auto; gap: 20px; align-items: center;
549    padding: 14px 16px 14px 24px; border-bottom: 1px solid rgba(255,255,255,0.10);
550    text-decoration: none; transition: background 0.25s ease;
551  }
552  .role-row:last-child { border-bottom: none; }
553  .role-row:hover { background: rgba(20,136,252,0.05); }
554  .role-title { font-size: 17px; font-weight: 500; color: #fff; letter-spacing: -0.2px; line-height: 1.35; }
555  .role-row:hover .role-title { color: #fff; }
556  .role-loc {
557    font-size: 15px; color: rgba(255,255,255,0.55); white-space: nowrap;
558    display: inline-flex; align-items: center; gap: 7px;
559  }
560  .role-loc svg { width: 16px; height: 16px; flex-shrink: 0; opacity: 0.7; }
561  /* Standard 52px ghost CTA. It's a <span>, not an <a>: the whole row is
562     already the link, and nesting an anchor inside one is invali
562d. The row's
563     hover drives the button's states so they fire from anywhere on the row,
564     not only when the cursor is over the button itself. */
565  .role-apply { flex-shrink: 0; min-width: 150px; padding: 0 20px; }
566  .role-row:hover .role-apply { border-color: rgba(255,255,255,0.4); color: #fff; }
567  .role-row:hover .role-apply .btn-text-inner { transform: translateY(-50%); }
568
569  .roles-state { padding: 44px 24px; text-align: center; color: rgba(255,255,255,0.4); font-size: 15px; }
570  .roles-state a { color: #1488FC; }
571  /* Greenhouse embed fallback — only shown if the JSON board can't be reached */
572  #grnhse_app { margin-top: 8px; }
573
574  .roles-note { margin-top: 28px; font-size: 14px; color: rgba(255,255,255,0.45); line-height: 1.65; }
575  .roles-note a { color: #1488FC; text-decoration: none; }
576  .roles-note a:hover { text-decoration: underline; }
577
578  /* ─── ANIMATED PIXEL DIVIDER (verbatim solutions / security-agent) ─────── */
579  .section-divider {
580    position: relative; height: 210px;
581    width: min(1200px, calc(100vw - 160px));
582    /* negative margins bleed the strip into both neighbouring sections */
583    margin: -100px auto -160px;
584    overflow: hidden; pointer-events: none; z-index: 1;
585    -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 30%, #000 76%, transparent 100%);
586            mask-image: linear-gradient(to bottom, transparent 0%, #000 30%, #000 76%, transparent 100%);
587  }
588  .section-divider canvas { display: block; width: 100%; height: 100%; }
589  @media (max-width: 1024px) { .section-divider { height: 160px; width: min(900px, calc(100vw - 80px)); margin: -80px auto -110px; } }
590  @media (max-width: 768px)  { .section-divider { height: 110px; width: calc(100vw - 32px); margin: -50px auto -60px; } }
591  @media (prefers-reduced-motion: reduce) { .section-divider { display: none; } }
592
593  /* ─── FAQ ─────────────────────────────────────────────────────────────── */
594  .ms-faq { position: relative; overflow: hidden; }
595  /* The FAQ header sits in the page's .section-header wrapper (56px bottom
596     margin) AND carries the shared .ms-faq-headline 48px bottom margin —
597     ~104px stacked where the component standard is 48px. Zero the wrapper's
598     margin here so the shared 48px is the whole gap. */
599  .ms-faq .section-header { margin-bottom: 0; }
600  #faq-pixel-canvas { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
601  .ms-faq-inner { position: relative; z-index: 1; width: min(1200px, calc(100vw - 160px)); margin: 0 auto; }
602
603  /* FAQ answer bodies. The page reset zeroes margins/padding, so lists inside
604     an answer lose their indent entirely — these rebuild the presentation
605     rather than restoring browser defaults. */
606  .ms-faq-a-inner .ms-faq-p { margin: 0 0 18px; }
607  .ms-faq-a-inner .ms-faq-p:last-child { margin-bottom: 0; }
608
609  /* Process rail — the canonical how-it-works node treatment (.how-step-node
610     from security-agent.html): 40px circle, 1px brand-blue ring on black, a
611     Silkscreen 10px numeral. Vertical rather than the component's horizontal
612     3-up, because five steps of sentence-length copy can't run across a row
613     inside an FAQ answer.
614     Progressive like the original too: opening the item draws the rail down
615     and pops the nodes in sequence, driven off the accordion's .is-open. */
616  .faq-steps { list-style: none; margin: 16px 0 22px; padding: 0; position: relative; }
617  /* static track + the fill that draws over it */
618  .faq-steps::before, .faq-steps::after {
619    content: ""; position: absolute; left: 20px; transform: translateX(-50%);
620    top: 20px; bottom: 20px; width: 1px;
621  }
622  .faq-steps::before { background: rgba(255,255,255,0.07); }
623  .faq-steps::after {
624    background: linear-gradient(180deg, rgba(20,136,252,0.25), rgba(20,136,252,0.8) 20%, rgba(20,136,252,0.8) 80%, rgba(20,136,252,0.25));
625    /* Resting state is DRAWN, not empty: the animation is an enhancement, so
626       if it never fires (no .is-open, reduced motion, JS off) the rail is
627       still there rather than invisible. */
628    transform: translateX(-50%) scaleY(1); transform-origin: top center;
629  }
630  .ms-faq-item.is-open .faq-steps::after {
631    animation: faqRailDraw 1.4s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both;
632  }
633  @keyframes faqRailDraw {
634    from { transform: translateX(-50%) scaleY(0); }
635    to   { transform: translateX(-50%) scaleY(1); }
636  }
637
638  /* grid-auto-rows: 1fr equalises every row to the tallest, so node-to-node
639     spacing is constant whether a step runs to one line or two. Sizing rows to
640     content instead left uneven gaps down the rail. */
641  .faq-steps { display: grid; grid-auto-rows: 1fr; }
642  .faq-steps li {
643    position: relative; display: grid; grid-template-columns: 40px 1fr;
644    column-gap: 18px; align-content: start; padding-bottom: 22px;
645  }
646  .faq-steps li:last-child { padding-bottom: 0; }
647
648  /* .how-step-node, verbatim values */
649  .faq-step-n {
650    width: 40px; height: 40px; border-radius: 50%; box-sizing: border-box;
651    border: 1px solid rgba(20,136,252,0.5); background: #000;
652    display: flex; align-items: center; justify-content: center;
653    font-family: 'Silkscreen', monospace; font-size: 10px; color: #1488FC;
654    position: relative; z-index: 1; flex-shrink: 0;
655    /* visible at rest — nodePop animates FROM scale(0), so a node is never
656       left invisible if the animation doesn't run */
657  }
658  .ms-faq-item.is-open .faq-steps li:nth-child(1) .faq-step-n { animation: nodePop 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.15s both; }
659  .ms-faq-item.is-open .faq-steps li:nth-child(2) .faq-step-n { animation: nodePop 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.40s both; }
660  .ms-faq-item.is-open .faq-steps li:nth-child(3) .faq-step-n { animation: nodePop 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.65s both; }
661  .ms-faq-item.is-open .faq-steps li:nth-child(4) .faq-step-n { animation: nodePop 0.45s cubic-bezier(0.34,1.56,0.64,1) 0.90s both; }
662  .ms-faq-item.is-open .faq-steps li:nth-child(5) .faq-step-n { animation: nodePop 0.45s cubic-bezier(0.34,1.56,0.64,1) 1.15s both; }
663  @keyframes nodePop { from { transform: scale(0); } to { transform: scale(1); } }
664
665  /* Optically centre the FIRST line of the step against the 40px node. */
666  .faq-step-t { padding-top: calc((40px - 1.7em) / 2); color: #fff; }
667
668  /* Each row's text reveals just behind its node — howStepIn verbatim from the
669     horizontal how-it-works timeline (solutions/_template), each text trailing
670     its nodePop by the same 0.13s the horizontal version uses. Visible at
671     rest for the same reason as the nodes: `both` only hides it while .is-open
672     drives the animation. */
673  @keyframes howStepIn { from { opacity:0; transform:translateY(14px); filter:blur(10px); } to { opacity:1; transform:translateY(0); filter:blur(0); } }
674  .ms-faq-item.is-open .faq-steps li:nth-child(1) .faq-step-t { animation: howStepIn 0.9s cubic-bezier(0.25,0.46,0.45,0.94) 0.28s both; }
675  .ms-faq-item.is-open .faq-steps li:nth-child(2) .faq-step-t { animation: howStepIn 0.9s cubic-bezier(0.25,0.46,0.45,0.94) 0.53s both; }
676  .ms-faq-item.is-open .faq-steps li:nth-child(3) .faq-step-t { animation: howStepIn 0.9s cubic-bezier(0.25,0.46,0.45,0.94) 0.78s both; }
677  .ms-faq-item.is-open .faq-steps li:nth-child(4) .faq-step-t { animation: howStepIn 0.9s cubic-bezier(0.25,0.46,0.45,0.94) 1.03s both; }
678  .ms-faq-item.is-open .faq-steps li:nth-child(5) .faq-step-t { animation: howStepIn 0.9s cubic-bezier(0.25,0.46,0.45,0.94) 1.28s both; }
679
680  @media (prefers-reduced-motion: reduce) {
681    .ms-faq-item.is-open .faq-steps li .faq-step-n,
682    .ms-faq-item.is-open .faq-steps li .faq-step-t,
683    .ms-faq-item.is-open .faq-steps::after { animation: none; }
684  }
685
686  /* ─── FOOTER ──────────────────────────────────────────────────────────── */
687  .footer-image-section { width: 100%; background: #000; }
688  .bolt-image-wrap { margin-top: 0; width: 100%; overflow: hidden; position: relative; height: 32vw; max-height: 32vw; }
689
690  /* Local H2 word-reveal — fires at ~50% viewport */
691  .reveal-h .word { display: inline-block; opacity: 0; transform: translate(-10px, -7px); filter: blur(5px); will-change: opacity, transform, filter; }
692  .reveal-h.is-revealed .word { animation: dsa-word-reveal 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards; animation-delay: calc(var(--i, 0) * 28ms); }
693  .reveal-h.is-instant .word { opacity: 1; transform: none; filter: none; animation: none; }
694
695  /* ─── RESPONSIVE ──────────────────────────────────────────────────────── */
696  @media (max-width: 1024px) {
697    .section-inner, .hero-inner, .ms-faq-inner { width: min(900px, calc(100vw - 80px)); }
698    .hero-h1 { font-size: 51px; }
699    .hero-subtitle { font-size: 18px; }
700    .values-grid { grid-template-columns: repeat(4, 1fr); }
701    .value-card { grid-column: span 2; }
702    .value-card:nth-child(4) { grid-column: 1 / span 2; }
703    .value-card:nth-child(5) { grid-column: 3 / span 2; }
704    .perks-grid { grid-template-columns: repeat(2, 1fr); }
705    .gallery { grid-template-columns: repeat(3, 1fr); gap: 10px; }
706    .testi-content { padding: 26px 26px 28px; gap: 18px; }
707    /* two cards + a sliver on tablet */
708    .testi-stack .testi-panel { flex-basis: calc((100% - 20px) / 2.25); }
709  }
710  @media (max-width: 768px) {
711    /* Truly sized to the content: the earlier 82dvh floor still reserved
712       slack, which flex-start now parks between the meta line and the film
713       card as dead space. The hero hugs its stack; the gap to the film is
714       the 56px bottom pad. */
715    .hero-section { min-height: 0; padding: 92px 0 56px; }
716    .hero-inner { width: 100%; padding: 0 16px; }
717    .hero-h1 { font-size: clamp(37px, 10.45vw, 51px); line-height: 1.1; }
718    .hero-subtitle { font-size: 16px; max-width: 100%; }
719    /* Standalone CTAs are a min-width FLOOR, never full-bleed (CLAUDE.md
720       "Standalone CTA button min-width"). The old `width:100%` here overrode
721       the shared --cta-min-w token and stretched the button edge to edge.
722       Centered, 52px, 16px label — the token supplies the 220px floor. */
723    .hero-btn-group { width: 100%; flex-direction: column; align-items: center; }
724    .hero-btn-group .hero-btn-primary,
725    .hero-btn-group .hero-btn-ghost {
726      width: auto; min-width: var(--cta-min-w); max-width: 100%;
727      height: 52px; font-size: 16px;
728    }
729    .hero-meta { font-size: 11px; letter-spacing: 0.5px; }
730    .section { padding: 48px 0; }
731    .film-section { padding: 0 0 48px; }
732    .section-inner, .ms-faq-inner { width: calc(100vw - 32px); }
733    .section-header { margin-bottom: 32px; }
734    .values-grid { grid-template-columns: 1fr; }
735    .value-card, .value-card:nth-child(4), .value-card:nth-child(5) { grid-column: auto; }
736    .gallery { grid-template-columns: repeat(2, 1fr); gap: 8px; }
737    /* one card + a sliver — the peek replaces the old stacked column, and
738       the snap points make it a natural swipe on touch */
739    .testi-stack { scroll-snap-type: x mandatory; }
740    .testi-stack .testi-panel { flex-basis: 85%; }
741    .testi-slider::after { width: 48px; }
742    .testi-content { padding: 26px 22px 28px; gap: 18px; }
743    .testi-photo { aspect-ratio: 16 / 10; }
744    .testi-panel .testi-eyebrow { font-size: 12px; }
745    .perks-grid { grid-template-columns: 1fr; }
746    .roles-controls { flex-direction: column; align-items: stretch; gap: 14px; }
747    .roles-select { flex: 0 0 auto; width: 100%; min-width: 0; height: 44px; }
748    /* Title and location share row 1; the CTA spans row 2 full-width.
749       Note this is a row action inside a list card, not a standalone hero CTA
750       — the "never full-bleed" min-width standard governs those, not this. */
751    .role-row { grid-template-columns: 1fr auto; gap: 16px 18px; padding: 22px 18px; align-items: center; }
752    .role-loc { justify-self: end; font-size: 14px; align-self: start; padding-top: 2px; }
753    /* A 52px full-width button per row ate the list on phones — eleven of them
754       is more chrome than content. Strip it back to a blue text link with an
755       arrow. The markup stays the shared ghost-button structure (the whole row
756       is still the link), so only the presentation changes: border, fill,
757       fixed height and the injected pixel-fill canvas all come off. */
758    /* .role-row prefix is required, not cosmetic: shared-components.css loads
759       at end of body, so its 0-1-0 .hero-btn-ghost rules (52px he
759ight, border,
760       0.8 white) beat a bare .role-apply and the button chrome stayed on. */
761    .role-row .role-apply {
762      grid-column: 1 / -1; justify-self: start;
763      width: auto; min-width: 0; height: auto; padding: 0;
764      border: none; background: none; overflow: visible;
765      color: #1488FC; font-size: 15px; font-weight: 500;
766      display: inline-flex; align-items: center; gap: 7px;
767    }
768    .role-row .role-apply .btn-bg-hover,
769    .role-row .role-apply .btn-pixel-canvas { display: none; }
770    .role-row .role-apply .btn-text-wrap { height: auto; overflow: visible; }
771    /* the two-span slide-up is a desktop hover affordance; there is no hover on
772       touch, so show one label only */
773    .role-row .role-apply .btn-text-inner { transform: none; }
774    .role-row .role-apply .btn-text-inner span:nth-child(2) { display: none; }
775    /* The arrow glyph sits high in its em box, so flex centring aligns the
776       boxes but not the ink — it read as floating above the word. Nudged down
777       to sit on the text's optical centre. */
778    .role-row .role-apply::after {
779      content: "\2192"; font-size: 16px; line-height: 1;
780      position: relative; top: 1px;
781    }
782    .bolt-image-wrap { width: 100vw !important; margin-left: 0 !important; height: 42vw !important; max-height: 42vw !important; }
783  }
784</style>
785
786<!-- ─── SECTION 1: HERO ───────────────────────────────────────────────────── -->
787<section class="hero-section">
788  <canvas id="hero-pixel-canvas"></canvas>
789
790  <div class="hero-inner">
791    <h1 class="hero-h1"><span class="hero-h1-kw">Reimagine</span> how software is built with <span class="hero-h1-kw hero-h1-kw--follow">Bolt.new</span></h1>
792    <p class="hero-subtitle">Join us to change how millions of people build &amp; deploy, forever.</p>
793    <div class="hero-btn-group">
794      <a href="#open-roles" class="hero-btn-primary">
795        <div class="btn-bg-hover"></div>
796        <div class="btn-text-wrap"><div class="btn-text-inner"><span>See Open Roles</span><span>See Open Roles</span></div></div>
797      </a>
798    </div>
799    <!-- Populated live from the Greenhouse board — never a stale hand-typed count -->
800    <p class="hero-meta" id="hero-role-meta"></p>
801  </div>
802</section>
803
804<!-- ─── SECTION 2: FILM ───────────────────────────────────────────────────── -->
805<!-- Sits flush under the hero: the hero runs this footage blurred back as
806     ambience, and this is the same film full-frame, with sound. -->
807<section class="film-section">
808  <div class="section-inner">
809    <!-- Ambient YouTube loop — plays muted on its own once scrolled into
810         view (a frozen poster sitting here reads as a broken embed); the
811         poster only fades after the player confirms playback, so a stalled
812         player keeps a legitimate-looking card. Clicking anywhere on the card
813         opens the full film with sound in the modal below. -->
814    <div class="film-card hero-video-card" id="hero-video-card" role="button" tabindex="0"
815         aria-label="Play the team film">
816      <div class="hero-video-area">
817        <img class="film-poster-img" id="film-preview-cover" src="/public-page-assets/careers/careers-film-poster.jpg" alt="" aria-hidden="true"
818             style="z-index:2;">
819        <!-- The ambient-loop YouTube iframe is built by JS when the card
820             approaches the viewport — NOT in markup. A parse-time iframe
821             intermittently bootstraps into YouTube's error-153 state (answers
822             the widget handshake but never loads a video); a JS-inserted one
823             bootstraps reliably, and it also means visitors who never scroll
824             here never load the third-party player at all. A YouTube iframe
825             rather than a <video>: Chrome kept pausing the muted inline
826             element within a second, and iOS blocks muted autoplay under Low
827             Power Mode; the iframe is subject to neither, which is why
828             solutions/ai-for-real-estate does it this way. -->
829        <div class="hero-video-overlay"></div>
830        <!-- No centre play button: the ambient loop plays on its own and the
831             whole card is the click target (shared module guards the missing
832             #hero-play-btn). -->
833      </div>
834    </div>
835  </div>
836</section>
837
838<!-- Clicking the card opens the film here. Same card + modal pattern as
839     solutions/ai-for-real-estate: the shared module builds the YouTube iframe
840     from #modal-video's data-youtube on open and clears it on close. -->
841<div class="video-modal" id="video-modal" style="--video-poster:url('/public-page-assets/careers/careers-film-poster.jpg')">
842  <div class="video-modal-inner">
843    <button class="video-modal-close" id="video-modal-close" aria-label="Close video">&times;</button>
844    <div id="modal-video" data-youtube="iIrbX4FB3-g"></div>
845  </div>
846</div>
847
848<!-- ─── SECTION 3: VALUES ─────────────────────────────────────────────────── -->
849<section class="section" id="values" style="padding-top:clamp(24px, 4vh, 48px);">
850  <div class="section-inner">
851    <div class="section-header section-header--center">
852      <div class="reveal-h"><h2 class="sc-section-h2">Who we are</h2></div>
853      <p class="section-sub">We&rsquo;re builders, designers, and dreamers reinventing how software is created. We move fast, win as a team, and ship bold ideas that make building faster, more joyful, and more accessible to every creator.</p>
854    </div>
855    <div class="values-grid">
856      <!-- Copy is the official values list, verbatim.
857           NOTE: flagged internally as "subject to change per Hyllarie Colyer
858           review" — re-check before this page goes live. -->
859      <div class="value-card">
860        <span class="value-num">01</span>
861        <h3 class="value-title">Extreme Ownership</h3>
862        <p class="value-desc">Ownership here isn&rsquo;t a title, it&rsquo;s a mindset, see what needs to happen, make it happen, carry it to the finish.</p>
863      </div>
864      <div class="value-card">
865        <span class="value-num">02</span>
866        <h3 class="value-title">Win Together</h3>
867        <p class="value-desc">We win together, that means jumping in wherever we&rsquo;re needed and wearing as many hats as it takes to get there.</p>
868      </div>
869      <div class="value-card">
870        <span class="value-num">03</span>
871        <h3 class="value-title">Bias for Momentum</h3>
872        <p class="value-desc">We keep the pace fast and the feedback loop tight, ship, learn, adjust, repeat.</p>
873      </div>
874      <div class="value-card">
875        <span class="value-num">04</span>
876        <h3 class="value-title">Build for Impact</h3>
877        <p class="value-desc">Impact starts with listening, we build what solves real problems and sweat every detail to get it right.</p>
878      </div>
879      <div class="value-card">
880        <span class="value-num">05</span>
881        <h3 class="value-title">All In</h3>
882        <p class="value-desc">All-in means being present when it counts and human the rest of the time, we take care of each other and respect boundaries.</p>
883      </div>
884    </div>
885  </div>
886</section>
887
888<!-- ─── SECTION 4: TEAM GALLERY ─────────────────────────────────────────────── -->
889<!-- Sits between the values and the testimonials: the values say what we're
890     like, this shows who "we" is, then the quotes are individual voices. -->
891<section class="teamshot-section">
892  <div class="section-inner">
893    <div class="teamshot-frame">
894      <canvas id="teamshot-pixels" aria-hidden="true"></canvas>
895      <div class="gallery">
896        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-01.webp" alt="Team gathered for a talk in the office" width="900" height="600" loading="lazy" decoding="async"></figure>
897        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-02.webp" alt="Three teammates working from the office couches" width="900" height="600" loading="lazy" decoding="async"></figure>
898        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-03.webp" alt="Two teammates talking at an offsite in the park" width="900" height="600" loading="lazy" decoding="async"></figure>
899        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-04.webp" alt="Teammates catching up over coffee in the office kitchen" width="900" height="600" loading="lazy" decoding="async"></figure>
900        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-05.webp" alt="The 1160 Battery Street office entrance" width="900" height="600" loading="lazy" decoding="async"></figure>
901        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-06.webp" alt="High fives at a team offsite" width="900" height="600" loading="lazy" decoding="async"></figure>
902        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-07.webp" alt="Three teammates laughing around a laptop in a meeting room" width="900" height="600" loading="lazy" decoding="async"></figure>
903        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-08.webp" alt="A teammate playing violin at an offsite" width="900" height="600" loading="lazy" decoding="async"></figure>
904        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-09.webp" alt="A teammate at their desk in the office" width="900" height="600" loading="lazy" decoding="async"></figure>
905        <figure class="gallery-item" role="button" tabindex="0"><img src="/public-page-assets/careers/gallery/team-10.webp" alt="Two teammates laughing while working from the office couches" width="900" height="600" loading="lazy" decoding="async"></figure>
906      </div>
907    </div>
908  </div>
909</section>
910
911<!-- Lightbox for the team gallery. Markup lives outside the section so its
912     fixed positioning isn't affected by the section's overflow:hidden. -->
913<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Team photos">
914  <button class="lightbox-btn lightbox-close" id="lightbox-close" aria-label="Close">
915    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>
916  </button>
917  <button class="lightbox-btn lightbox-prev" id="lightbox-prev" aria-label="Previous photo">
918    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
919  </button>
920  <figure class="lightbox-stage">
921    <img id="lightbox-img" alt="">
922    <figcaption class="lightbox-count"><span id="lightbox-i">1</span> / <span id="lightbox-n">10</span></figcaption>
923  </figure>
924  <button class="lightbox-btn lightbox-next" id="lightbox-next" aria-label="Next p
924hoto">
925    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
926  </button>
927</div>
928
929<!-- ─── SECTION 5: PERKS ────────────────────────────────────────────────────── -->
930<section class="section" style="padding-top:clamp(24px, 4vh, 48px);">
931  <div class="section-inner">
932    <div class="section-header">
933      <p class="section-eyebrow eyebrow-scramble">Perks &amp; benefits</p>
934      <div class="reveal-h"><h2 class="sc-section-h2">Taken care of, so you can do the work</h2></div>
935      <p class="section-sub">Health, retirement, time off, and the setup to do good work anywhere.</p>
936    </div>
937
938    <div class="perks-grid">
939      <div class="perk-card">
940        <div class="perk-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8.5" x2="12" y2="14.5"/><line x1="9" y1="11.5" x2="15" y2="11.5"/></svg></div>
941        <h3 class="perk-title">Health coverage</h3>
942        <p class="perk-desc">Medical, dental, and vision for you and your dependents, with the company covering the large majority of the premium.</p>
943      </div>
944      <div class="perk-card">
945        <div class="perk-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg></div>
946        <h3 class="perk-title">401(k)</h3>
947        <p class="perk-desc">A retirement plan you can contribute to from your first paycheck, with no waiting period to enroll.</p>
948      </div>
949      <div class="perk-card">
950        <div class="perk-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>
951        <h3 class="perk-title">Wellness</h3>
952        <p class="perk-desc">A monthly stipend for whatever keeps you functioning &mdash; the gym, therapy, the climbing membership, the good mattress.</p>
953      </div>
954      <div class="perk-card">
955        <div class="perk-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/>
955<path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg></div>
956        <h3 class="perk-title">Internet reimbursement</h3>
957        <p class="perk-desc">If you work from home, your connection is a work tool. We pay for it monthly &mdash; no expense-report theatre.</p>
958      </div>
959      <div class="perk-card">
960        <div class="perk-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
961        <h3 class="perk-title">Parental leave</h3>
962        <p class="perk-desc">Paid leave for every new parent &mdash; birth, adoption, or surrogacy &mdash; and a ramp back that assumes you have a newborn, because you do.</p>
963      </div>
964      <div class="perk-card">
965        <div class="perk-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.5"/><line x1="12" y1="1.5" x2="12" y2="3.5"/><line x1="12" y1="20.5" x2="12" y2="22.5"/><line x1="4.3" y1="4.3" x2="5.7" y2="5.7"/><line x1="18.3" y1="18.3" x2="19.7" y2="19.7"/><line x1="1.5" y1="12" x2="3.5" y2="12"/><line x1="20.5" y1="12" x2="22.5" y2="12"/><line x1="4.3" y1="19.7" x2="5.7" y2="18.3"/><line x1="18.3" y1="5.7" x2="19.7" y2="4.3"/></svg></div>
966        <h3 class="perk-title">Flexible PTO</h3>
967        <p class="perk-desc">No fixed allotment, and &mdash; more importantly &mdash; managers who take their own time off so you feel able to take yours.</p>
968      </div>
969    </div>
970
971  </div>
972</section>
973
974<div class="section-divider" aria-hidden="true">
975  <canvas id="section-divider-canvas-2"></canvas>
976</div>
977
978<!-- ─── SECTION 6: TESTIMONIALS ─────────────────────────────────────────────── -->
979<section class="section testi-section" id="testimonials">
980  <div class="section-inner">
981    <div class="section-header section-header--center">
982      <div class="reveal-h"><h2 class="sc-section-h2">Hear it from our team</h2></div>
983    </div>
984    <div class="testi-slider">
985    <div class="testi-stack">
986
987      <article class="testi-panel">
988        <canvas class="testi-pixels" id="testi-pixels-4"></canvas>
989        <div class="testi-content">
990          <p class="section-eyebrow testi-eyebrow">Engineering</p>
991          <p class="testi-quote">&ldquo;The culture at Bolt is genuinely exceptional. Engineers here aren&rsquo;t just handed specs to execute. You get the reasoning behind every goal, the space to disagree, and the trust to help shape how the team works. Things move fast, but no one uses that as an excuse to skip the details.&rdquo;</p>
992          <div class="testi-attr">
993            <span class="testi-name">Liya</span>
994            <span class="testi-role">Senior Frontend Engineer, Bolt.new</span>
995          </div>
996        </div>
997        <div class="testi-photo">
998          <img src="/public-page-assets/careers/team/liya.jpg" alt="Liya, Senior Frontend Engineer" loading="lazy" onerror="this.closest('.testi-photo').classList.add('is-empty'); this.remove();">
999        </div>
1000      </article>
1001
1002      <article class="testi-panel">
1003        <canvas class="testi-pixels" id="testi-pixels-3"></canvas>
1004        <div class="testi-content">
1005          <p class="section-eyebrow testi-eyebrow">Sales</p>
1006          <p class="testi-quote">&ldquo;What I love about Bolt is that everyone&rsquo;s genuinely in it together. From eng to product to support, we jump on problems as one team. Things move fast, feedback is honest and direct, and people actually listen. It&rsquo;s rare to feel this looped in and this cared for.&rdquo;</p>
1007          <div class="testi-attr">
1008            <span class="testi-name">Joe K.</span>
1009            <span class="testi-role">Sales Engineer, Bolt.new</span>
1010          </div>
1011        </div>
1012        <div class="testi-photo">
1013          <img src="/public-page-assets/careers/team/joe-k.jpg" alt="Joe K., Sales Engineer" loading="lazy" onerror="this.closest('.testi-photo').classList.add('is-empty'); this.remove();">
1014        </div>
1015      </article>
1016
1017      <article class="testi-panel">
1018        <canvas class="testi-pixels" id="testi-pixels-2"></canvas>
1019        <div class="testi-content">
1020          <p class="section-eyebrow testi-eyebrow">People Ops</p>
1021          <p class="testi-quote">&ldquo;What sold me on Bolt was the opportunity to help build something special, but what&rsquo;s kept me here is the people. I&rsquo;ve watched teammates jump in wherever they&rsquo;re needed and leadership move fast without losing sight of the people behind the work. It&rsquo;s rare to find a team this driven and this genuine.&rdquo;</p>
1022          <div class="testi-attr">
1023            <span class="testi-name">Brianna L.</span>
1024            <span class="testi-role">People Ops Generalist, Bolt.new</span>
1025          </div>
1026        </div>
1027        <div class="testi-photo">
1028          <img src="/public-page-assets/careers/team/brianna-l.jpg" alt="Brianna L., People Ops Generalist" loading="lazy" onerror="this.closest('.testi-photo').classList.add('is-empty'); this.remove();">
1029        </div>
1030      </article>
1031
1032      <article class="testi-panel">
1033        <canvas class="testi-pixels" id="testi-pixels-1"></canvas>
1034        <div class="testi-content">
1035          <p class="section-eyebrow testi-eyebrow">Customer Experience</p>
1036          <p class="testi-quote">&ldquo;I&rsquo;ve never worked for a company that is as collaborative as this one. At Bolt, regardless of your role in the company, you are empowered to provide feedback and suggest improvements to the product. Working at Bolt really makes you feel like you&rsquo;re contributing to something great.&rdquo;</p>
1037          <div class="testi-attr">
1038            <span class="testi-name">Charles W.</span>
1039            <span class="testi-role">Senior Customer Experience Engineer, Bolt.new</span>
1040          </div>
1041        </div>
1042        <div class="testi-photo">
1043          <img src="/public-page-assets/careers/team/charles-w.jpg" alt="Charles W., Senior Customer Experience Engineer" loading="lazy" onerror="this.closest('.testi-photo').classList.add('is-empty'); this.remove();">
1044        </div>
1045      </article>
1046
1047    </div>
1048    </div>
1049    <div class="testi-nav">
1050      <button class="testi-arrow" id="testi-prev" type="button" aria-label="Previous testimonials">
1051        <svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
1052      </button>
1053      <button class="testi-arrow" id="testi-next" type="button" aria-label="Next testimonials">
1054        <svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
1055      </button>
1056    </div>
1057  </div>
1058</section></section>
1059
1060<!-- ─── SECTION 7: OPEN ROLES ───────────────────────────────────────────────── -->
1061<section class="section" id="open-roles" style="padding-top:clamp(24px, 4vh, 48px);">
1062  <div class="section-inner">
1063    <div class="section-header">
1064      <p class="section-eyebrow eyebrow-scramble">Open roles</p>
1065      <div class="reveal-h"><h2 class="sc-section-h2">Find your seat</h2></div>
1066      <p class="section-sub">Live from our job board. If nothing fits today, the general application at the bottom goes to a human.</p>
1067    </div>
1068
1069    <!-- Team pills and the location select share one row; the pills scroll
1070         inside their wrap so a long team list never pushes the select off. -->
1071    <div class="roles-controls">
1072      <div class="roles-pills-wrap">
1073        <div class="roles-pills" id="roles-pills" role="tablist" aria-label="Filter by team"></div>
1074      </div>
1075      <label class="sr-only" for="roles-location">Filter by location</label>
1076      <select id="roles-location" class="roles-select">
1077        <option value="">All locations</option>
1078      </select>
1079    </div>
1080
1081    <div id="roles-output">
1082      <div class="roles-state" id="roles-state">Loading open positions&hellip;</div>
1083    </div>
1084
1085    <!-- Fallback: if the JSON board is unreachable, the official Greenhouse
1086         embed is injected here instead so the page never shows zero roles. -->
1087    <div id="grnhse_app"></div>
1088
1089    <p class="roles-note">Don&rsquo;t see the right role? New openings are posted here as they go live &mdash; it&rsquo;s worth checking back.</p>
1090  </div>
1091</section>
1092
1093<div class="section-divider" aria-hidden="true">
1094  <canvas id="section-divider-canvas"></canvas>
1095</div>
1096
1097<!-- ─── SECTION 8: FAQ ──────────────────────────────────────────────────────── -->
1098<section class="section ms-faq" style="padding-top:clamp(72px, 9vh, 112px);">
1099  <canvas id="faq-pixel-canvas"></canvas>
1100  <div class="ms-faq-inner">
1101    <div class="section-header section-header--center">
1102      <p class="ms-faq-eyebrow eyebrow-scramble">Hiring process</p>
1103      <div class="reveal-h"><h2 class="ms-faq-headline">What to expect</h2></div>
1104    </div>
1105    <div class="ms-faq-list">
1106      <div class="ms-faq-item">
1107        <button class="ms-faq-q">
1108          What does the interview process look like?
1109          <span class="ms-faq-icon"><svg class="ms-faq-icon-plus" width="10" height="10" viewBox="0 0 10 10" fill="none"><line x1="5" y1="1" x2="5" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg><svg class="ms-faq-icon-minus" width="10" height="10" viewBox="0 0 10 10" fill="none"><line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
1110        </button>
1111        <div class="ms-faq-a"><div class="ms-faq-a-inner"><p class="ms-faq-p">Our interviewing process is intended to be efficient and relevant. We want to understand how you think and solve problems.</p><p class="ms-faq-p">Interviews are usually 30&ndash;60 minutes long, depending on focus. Most loops finish within two to three weeks. The exact steps depend on the role.</p><ol class="faq-steps"><li><span class="faq-step-n">01</span><span class="faq-step-t">An initial recruiter conversation to better understand your experience and where that fits into what we are building</span></li><li><span class="faq-step-n">02</span><span class="faq-step-t">A call with the hiring manager to better understand your previous and anticipated impact</span></li><li><span class="faq-step-n">03</span><span class="faq-step-t">When applicable, you might do a practical exercise or portfolio walkthrough that reflects the actual job</span></li><li><span class="faq-step-n">04</span><span class="faq-step-t">A short set of team conversations to evaluate your skills (usually two to four chats)</span></li><li><span class="faq-step-n">05</span><span class="faq-step-t">A chance to speak with our CEO or CTO to address any residual questions you have about how you can help us build</span></li></ol></div></div>
1112      </div>
1113      <div class="ms-faq-item">
1114        <button class="ms-faq-q">
1115          Are these roles remote?
1116          <span class="ms-faq-icon"><svg class="ms-faq-icon-plus" width="10" height="10" viewBox="0 0 10 10" fill="none"><line x1="5" y1="1" x2="5" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg><svg class="ms-faq-icon-minus" width="10" height="10" viewBox="0 0 10 10" fill="none"><line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
1117        </button>
1118        <div class="ms-faq-a"><div class="ms-faq-a-inner">Most are. We&rsquo;re remote-first with a San Francisco hub, and each job post states the location requirement and any time-zone overlap we need. If a role is listed as remote, it genuinely is.</div></div>
1119      </div>
1120      <div class="ms-faq-item">
1121        <button class="ms-faq-q">
1122          Do I need to have used Bolt to apply?
1123          <span class="ms-faq-icon"><svg class="ms-faq-icon-plus" width="10" height="10" viewBox="0 0 10 10" fill="none"><line x1="5" y1="1" x2="5" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg><svg class="ms-faq-icon-minus" width="10" height="10" viewBox="0 0 10 10" fill="none"><line x1="1" y1="5" x2="9" y2="5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
1124        </button>
1125        <div class="ms-faq-a"><div class="ms-faq-a-inner">No, but it&rsquo;s the single best way to prepare. It&rsquo;s free to try, it takes about ten minutes to build something real, and candidates who&rsquo;ve done it always have sharper questions.</div></div>
1126      </div>
1127    </div>
1128  </div>
1129</section>
1130
1131<!-- ─── SECTION 9: SITE FOOTER ──────────────────────────────────────────────── -->
1132
1133<!-- ── shared component CSS (buttons, eyebrows, FAQ) — inlined; loads after
1134     the page CSS so it wins the cascade, exactly as on the sandbox page ── -->
1135<style>
1136/* ============================================================================
1137   SHARED COMPONENTS — single source of truth for standardized components.
1138   Linked at the END of <body> on each page so it wins the cascade over any
1139   leftover inline rules. Edit component styling HERE, not per-page.
1140
1141   Covers: Buttons (primary/ghost/cta) · Eyebrows · FAQ · Footer
1142           · Hero video component (narrow)
1143   Used by: microsoft.html, bolt-cli.html  (Phase 1)
1144   ============================================================================ */
1145
1146/* ─── DESIGN TOKENS ──────────────────────────────────────────────────────── */
1147/* Canonical subtitle → CTA vertical spacing. Change here to move every
1148   hero, section side-card, callout, and footer across every page in one edit.
1149   Mobile uses a shorter value — tight spacing reads better on small screens. */
1150:root { --sub-to-cta: 32px; }
1151@media (max-width: 768px) { :root { --sub-to-cta: 24px; } }
1152
1153/* Minimum width for standalone CTAs, so short labels ("Learn More",
1154   "Read More") don't render smaller than their neighbours. It's a floor, not
1155   a fixed width — longer labels still expand past it. */
1156:root { --cta-min-w: 180px; }
1157@media (max-width: 768px) { :root { --cta-min-w: 220px; } }
1158
1159/* Apply the token to every subtitle→CTA pattern. Each rule offsets the
1160   parent container's own flex `gap` so the total distance = --sub-to-cta.
1161   max(0px, …) prevents negative margins when the mobile token is smaller
1162   than the container's own gap (in which case the container gap alone is
1163   already close enough — the CTA row just doesn't add anything extra).
1164
1165   Container base `gap` values (per current page CSS):
1166     .hero-inner        → 24px  (20 at ≤768)
1167     .builtin-left      → 22px
1168     .run-callout       → 12px
1169     .footer-section    → 24px  (canvas hack: 24 - 152 + 192 = 64 baseline) */
1170/* NOTE: child combinator (>) is deliberate. .hero-inner's `gap` only creates
1171   space between its DIRECT children, so the offset is only valid there. With a
1172   descendant selector this also hit pages that nest the button group deeper
1173   (microsoft puts it inside .hero-headline-group), silently replacing a correct
1174   margin with an offset computed against a gap that never applied. */
1175.hero-inner > .hero-btn-group { margin-top: max(0px, calc(var(--sub-to-cta) - 24px)); }
1176/* At ≤768 .hero-inner's gap drops 24 → 20 on most pages, so the offset has to
1177   be recomputed against 20 or the hero lands 4px short. */
1178@media (max-width: 768px) {
1179  .hero-inner > .hero-btn-group { margin-top: max(0px, calc(var(--sub-to-cta) - 20px)); }
1180}
1181.builtin-cta                { margin-top: max(0px, calc(var(--sub-to-cta) - 22px)); }
1182.run-callout-btns           { margin-top: max(0px, calc(var(--sub-to-cta) - 12px)); }
1183/* Footer button sits inside a wrapper with margin-top:-152 + a 192px canvas.
1184   Desktop baseline = 64px, so button margin-top = --sub-to-cta - 64px. */
1185/* Baseline is the same at every breakpoint (the canvas keeps its inline 192px
1186   height on mobile), so this one rule covers desktop and mobile — the token
1187   itself is what changes. */
1188.footer-cta-btn { margin-top: calc(var(--sub-to-cta) - 64px); }
1189/* Footer email-form strips sit in the same wrapper (-152px pull + 192px canvas
1190   = 64px baseline), so the same token math gives them the same subtitle gap.
1191   Add this class to the strip and drop its inline margin-top. */
1192.footer-cta-strip { margin-top: calc(var(--sub-to-cta) - 64px); }
1193/* Footer CTAs are standalone buttons, so they take the documented
1194   standalone-CTA min-width (CLAUDE.md) instead of shrinking to short labels
1195   like "Read More". Matches the hero/callout CTA treatment. */
1196/* Standalone CTAs (hero, section side-card, callout, footer) get an
1197   equalizing min-width so short labels like "Learn More" / "Read More" don't
1198   render smaller than their neighbours. Scoped to the standalone CTA
1199   containers per CLAUDE.md, so form-attached submits (.hero-cta-button email
1200   strips, .re-prompt-submit prompt boxes) keep their natural width. */
1201.hero-btn-group .hero-btn-primary,   .hero-btn-group .hero-btn-ghost,
1202.hero-cta-row .hero-btn-primary,     .hero-cta-row .hero-btn-ghost,
1203.builtin-cta .hero-btn-primary,      .builtin-cta .hero-btn-ghost,
1204.run-callout-btns .hero-btn-primary, .run-callout-btns .hero-btn-ghost,
1205.footer-cta-btn { min-width: var(--cta-min-w); max-width: 100%; }
1206
1207/* Form/input-attached submits are sized by the field they're joined to, so
1208   they must never pick up the standalone min-width — not even if they end up
1209   inside one of the containers above. Specificity here is 0-2-0, matching the
1210   rule above, and it's declared after, so it wins. */
1211:is(form, .hero-form-row, .card-form-strip, .re-prompt-shell)
1212  :is(.hero-btn-primary, .hero-btn-ghost, .footer-cta-btn),
1213.hero-cta-button,
1214.re-prompt-submit { min-width: 0; max-width: none; }
1215
1216/* ─── PRIMARY / GHOST / CTA BUTTONS ──────────────────────────────────────── */
1217.hero-btn-primary {
1218  display: inline-flex; align-items: center; justify-content: center;
1219  height: 52px; padding: 0 28px; background: #1488FC; border: none; border-radius: 2px;
1220  color: #fff; font-size: 15px; font-weight: 600; font-family: 'Inter', sans-serif;
1221  cursor: pointer; position: relative; overflow: hidden; text-decoration: none;
1222}
1223.hero-btn-primary .btn-bg-hover { position: absolute; inset: 0; background: #0f6fd0; z-index: 1; transform: translateY(101%); transition: transform 0.45s cubic-bezier(0.165,0.84,0.44,1); }
1224.hero-btn-primary .btn-text-wrap { position: relative; z-index: 3; overflow: hidden; height: 1.2em; }
1225.hero-btn-primary .btn-text-inner { display: flex; flex-direction: column; transition: transform 0.45s cubic-bezier(0.165,0.84,0.44,1); }
1226.hero-btn-primary .btn-text-inner span { display: block; line-height: 1.2; }
1227.hero-btn-primary:hover .btn-bg-hover { transform: translateY(0); }
1228.hero-btn-primary:hover .btn-text-inner { transform: translateY(-50%); }
1229/* Ghost button uses the same two-span slide-up trick — share the rules */
1230.hero-btn-ghost .btn-bg-hover { display: none; }
1231.hero-btn-ghost .btn-text-wrap { position: relative; z-index: 3; overflow: hidden; height: 1.2em; }
1232.hero-btn-ghost .btn-text-inner { display: flex; flex-direction: column; transition: transform 0.45s cubic-bezier(0.165,0.84,0.44,1); }
1233.hero-btn-ghost .btn-text-inner span { display: block; line-height: 1.2; }
1234.hero-btn-ghost:hover .btn-text-inner { transform: translateY(-50%); }
1235
1236/* ─── PIXEL-FILL BUTTON HOVER (canonical, matches ai-for-real-estate/smb/microsoft) ───
1237   JS attaches a <canvas class="btn-pixel-canvas"> as the first child of each
1238   .hero-btn-primary / .hero-btn-ghost / .footer-cta-btn and toggles .btn-pixelized.
1239   When pixelized: slide-up bg layer + text duplicate hide; the canvas fills with
1240   brand-blue dots on mouseenter, fades on mouseleave, with a subtle flicker at peak. */
1241.btn-pixel-canvas { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
1242.hero-btn-ghost .btn-pixel-canvas { z-index: 1; border-radius: 2px; }
1243.hero-btn-primary.btn-pixelized .btn-bg-hover,
1244.hero-btn-ghost.btn-pixelized .btn-bg-hover,
1245.footer-cta-btn.btn-pixelized .btn-bg-hover { display: none !important; }
1246.hero-btn-primary.btn-pixelized .btn-text-inner,
1247.hero-btn-ghost.btn-pixelized .btn-text-inner,
1248.footer-cta-btn.btn-pixelized .btn-text-inner { transform: none !important; }
1249.hero-btn-primary.btn-pixelized .btn-text-inner span:nth-child(2),
1250.hero-btn-ghost.btn-pixelized .btn-text-inner span:nth-child(2),
1251.footer-cta-btn.btn-pixelized .btn-text-inner span:nth-child(2) { display: none; }
1252.hero-btn-primary.btn-pixelized .btn-text-wrap,
1253.hero-btn-ghost.btn-pixelized .btn-text-wrap,
1254.footer-cta-btn.btn-pixelized .btn-text-wrap { z-index: 3; }
1255
1256/* ─── SECTION HEADLINE (canonical H2) ─────────────────────────────────────
1257   ONE class for every section-level H2. Replaces the per-page aliases that
1258   had drifted apart (.builtin-h2 / .detail-h2 / .agent-h2 / .compliance-h2 /
1259   .bullets-h2 / .trust-h2 / .hiw-h2 / .run-callout-h2 / .section-headline) —
1260   twelve names for one component is how the ≤768px rule ended up applied to
1261   .builtin-h2 only, leaving .agent-h2 and .detail-h2 pinned at their 32px
1262   floor and visibly smaller.
1263
1264   .ms-faq-headline and .footer-headline share this type (grouped below);
1265   their component rules carry layout only (FAQ margin, footer centering).
1266   NOT covered: .feat-headline / .how-headline on non-migrated pages.
1267
1268   Adopted by: security.html, trust.html, trust-v2.html.
1269   Other pages still use their local aliases and are unaffected; migrate them
1270   by swapping the class in markup and deleting the local rule. */
1271.sc-section-h2,
1272.ms-faq-headline,
1273.footer-headline {
1274  /* 40px floor (not 32) so the size is continuous across the 768px breakpoint:
1275     the ≤768 rule caps at 40px, and a 32px floor made the headline SHRINK by
1276     8px when the viewport grew past 768 (4vw doesn't reach 40px until 1000px).
1277     The old 32px floor only ever bound in the 769–800px sliver. */
1278  font-size: clamp(40px, 4vw, 52px);
1279  font-weight: 500;
1280  color: #fff;
1281  line-height: 1.15;
1282  letter-spacing: -1px;
1283  margin: 0;
1284}
1285@media (max-width: 768px) {
1286  .sc-section-h2, .ms-faq-headline, .footer-headline { font-size: clamp(28px, 8vw, 40px); }
1287}
1288
1289/* ─── SCROLL-IN ANIMATIONS (canonical across all marketing pages) ──────────
1290   .dsa-reveal wraps an H1/H2 and triggers per-word blur-fade reveal on scroll.
1291   .eyebrow-scramble triggers a 600ms letter-scramble entrance and re-fires
1292   every 6s while visible. JS lives in shared-components.js. */
1293.dsa-reveal .word {
1294  display: inline-block;
1295  opacity: 0;
1296  transform: translate(-10px, -7px);
1297  filter: blur(5px);
1298  will-change: opacity, transform, filter;
1299}
1300.dsa-reveal.is-revealed .word {
1301  animation: dsa-word-reveal 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
1302  animation-delay: calc(var(--i, 0) * 28ms);
1303}
1304/* Fast-scroll catch-up (paired with the .is-instant classification in
1305   shared-components.js and the pages' local reveal modules): when a header is
1306   first noticed already deep in view, the cascade is skipped and the words are
1307   simply there. Selectors deliberately cover BOTH container types (.dsa-reveal
1308   shared / .reveal-h page-local) and carry the skip to the shared-owned
1309   subtitle words via the sibling combinator. */
1310.dsa-reveal.is-instant .word,
1311.reveal-h.is-instant .word,
1312.dsa-reveal.is-instant ~ .sc-sub-words .word,
1313.reveal-h.is-instant ~ .sc-sub-words .word { opacity: 1; transform: none; filter: none; animation: none; }
1314/* Subtitle word-reveal: the SAME per-word blur cascade as the H2 (microsoft
1315   behavior). shared-components.js wraps the subtitle's words with --i indexes
1316   continuing from the heading's word count, so heading → subtitle read as one
1317   continuous cascade. Classes are JS-set, so no-JS pages stay visible. */
1318.sc-sub-words .word {
1319  display: inline-block;
1320  opacity: 0;
1321  transform: translate(-10px, -7px);
1322  filter: blur(5px);
1323  will-change: opacity, transform, filter;
1324}
1325.sc-sub-words.sc-sub-go .word {
1326  animation: dsa-word-reveal 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards;
1327  animation-delay: calc(var(--i, 0) * 28ms);
1328}
1329/* Fast-scroll catch-up for subtitles, structure-independent: the class is set
1330   on the sub itself by the reveal JS (the sibling selectors above only reach
1331   subs that FOLLOW the container; subs inside it need this). Placed after the
1332   .sc-sub-go rule — equal specificity, later wins. */
1333.sc-sub-words.sc-sub-instant .word { opacity: 1; transform: none; filter: none; animation: none; }
1334@media (prefers-reduced-motion: reduce) {
1335  .sc-sub-words .word { opacity: 1; transform: none; filter: none; animation: none; }
1336}
1337
1338@keyframes dsa-word-reveal {
1339  from { opacity: 0; transform: translate(-10px, -7px); filter: blur(5px); }
1340  to   { opacity: 1; transform: translate(0, 0);        filter: blur(0);   }
1341}
1342
1343/* Email-capture submit button */
1344.hero-cta-button {
1345  width: auto; padding: 0 24px; height: 52px; flex-shrink: 0; background: #1488FC;
1346  border: none; border-radius: 0 2px 2px 0; color: #fff; font-size: 15px; font-weight: 600;
1347  font-family: 'Inter', sans-serif; cursor: pointer; position: relative; overflow: hidden;
1348  display: inline-flex; align-items: center; justify-content: center;
1349}
1350.hero-cta-button .btn-bg-hover { position: absolute; inset: 0; background: #0f6fd0; z-index: 1; transform: translateY(101%); transition: transform 0.45s cubic-bezier(0.165,0.84,0.44,1); }
1351.hero-cta-button .btn-text-wrap { position: relative; z-index: 3; overflow: hidden; height: 1.2em; }
1352.hero-cta-button .btn-text-inner { display: flex; flex-direction: column; transition: transform 0.45s cubic-bezier(0.165,0.84,0.44,1); }
1353.hero-cta-button .btn-text-inner span { display: block; line-height: 1.2; }
1354.hero-cta-button:hover .btn-bg-hover { transform: translateY(0); }
1355.hero-cta-button:hover .btn-text-inner { transform: translateY(-50%); }
1356
1357/* ─── EMAIL CAPTURE STRIP (bordered card: label + email input + submit) ───── */
1358.card-form-strip { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; padding: 12px 28px; cursor: default; }
1359/* Footer usage of the strip is a bordered rounded card */
1360.card-form-strip.is-carded { border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; }
1361.card-strip-text { display: flex; flex-direction: column; gap: 3px; flex-shrink: 0; }
1362.card-strip-sub { font-size: 16px; font-weight: 500; color: #fff; margin: 0; line-height: 1.2; display: inline-flex; align-items: baseline; gap: 0.25em; }
1363.hero-form-row { display: flex; gap: 0; flex: 1 1 320px; max-width: 420px; background: rgba(255,255,255,0.08); border-radius: 4px; overflow: hidden; }
1364.hero-email-input { flex: 1; height: 52px; background: transparent; border: none; padding: 0 18px; font-size: 15px; font-family: 'Inter', sans-serif; color: #fff; outline: none; }
1365.hero-email-input::placeholder { color: rgba(255,255,255,0.45); }
1366@media (max-width: 768px) {
1367  .card-form-strip { flex-direction: column; align-items: center; gap: 12px; padding: 16px 12px; }
1368  .card-form-strip .hero-form-row { width: 100%; max-width: none; flex-basis: auto; }
1369}
1370
1371.hero-btn-ghost {
1372  display: inline-flex; align-items: center; justify-content: center;
1373  height: 52px; padding: 0 28px; background: transparent;
1374  border: 1px solid rgba(255,255,255,0.22); border-radius: 2px;
1375  color: rgba(255,255,255,0.8); font-size: 15px; font-weight: 500;
1376  font-family: 'Inter', sans-serif; cursor: pointer; text-decoration: none;
1377  position: relative; overflow: hidden;     /* anchor + clip for the pixel-fill canvas hover */
1378}
1379.hero-btn-ghost:hover { border-color: rgba(255,255,255,0.4); color: #fff; }
1380
1381/* ─── EYEBROWS (Silkscreen blue labels) ──────────────────────────────────── */
1382.ms-faq-eyebrow, .footer-eyebrow, .how-eyebrow, .section-eyebrow,
1383.slider-section-eyebrow, .feat-eyebrow, .success-eyebrow, .practice-visual-label {
1384  font-family: 'Silkscreen', monospace; font-size: 16px; font-weight: 400;
1385  letter-spacing: 2px; text-transform: uppercase; color: #1488FC;
1386}
1387@media (max-width: 1024px) {
1388  .ms-faq-eyebrow, .footer-eyebrow, .how-eyebrow, .section-eyebrow,
1389  .slider-section-eyebrow, .feat-eyebrow, .success-eyebrow,
1390  .practice-section .practice-visual-label {
1391    font-size: 13px;
1392  }
1393}
1394
1395/* ─── FAQ — accordion: +/- icon, smooth grid-row expand, blue tint when open ── */
1396/* type comes from the canonical section-H2 group above; this is layout only */
1397.ms-faq-headline { margin: 0 0 48px; }
1398.ms-faq-list { display: flex; flex-direction: column; }
1399.ms-faq-item { border-top: 1px solid rgba(255,255,255,0.07); border-bottom: none; position: relative; transition: background 0.2s ease; overflow: hidden; }
1400.ms-faq-item:last-child { border-bottom: 1px solid rgba(255,255,255,0.07); }
1401.ms-faq-item.is-open { background: rgba(20,136,252,0.03); }
1402.ms-faq-q { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 28px 16px; background: none; border: none; cursor: pointer; font-family: 'Inter', sans-serif; font-size: 18px; font-weight: 500; color: rgba(255,255,255,0.7); text-align: left; transition: color 0.2s; }
1403.ms-faq-q:hover, .ms-faq-item.is-open .ms-faq-q { color: #fff; }
1404.ms-faq-icon { flex-shrink: 0; width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; color: #1488FC; position: relative; }
1405.ms-faq-icon-plus, .ms-faq-icon-minus { position: absolute; transition: opacity 0.2s, transform 0.2s; }
1406.ms-faq-icon-minus { opacity: 0; transform: rotate(-90deg); }
1407.ms-faq-item.is-open .ms-faq-icon-plus { opacity: 0; transform: rotate(90deg); }
1408.ms-faq-item.is-open .ms-faq-icon-minus { opacity: 1; transform: rotate(0deg); }
1409.ms-faq-chevron { display: none; }
1410.ms-faq-a { display: grid; grid-template-rows: 0fr; max-height: none; overflow: hidden; transition: grid-template-rows 0.38s cubic-bezier(0.4,0,0.2,1); }
1411.ms-faq-item.is-open .ms-faq-a { grid-template-rows: 1fr; }
1412.ms-faq-a-inner { overflow: hidden; padding: 0 16px; font-size: 15px; color: rgba(255,255,255,0.5); /* section-subtitle grey (standardized 2026-08-07, was 0.65) */ line-height: 1.7; max-width: 720px; transition: padding-bottom 0.38s cubic-bezier(0.4,0,0.2,1); }
1413.ms-faq-item.is-open .ms-faq-a-inner { padding-bottom: 24px; }
1414.ms-faq-link { color: #1488FC; text-decoration: none; }
1415.ms-faq-link:hover { text-decoration: underline; }
1416@media (max-width: 768px) {
1417  .ms-faq-inner { width: calc(100vw - 32px); }
1418  .ms-faq-q { font-size: 17px; padding: 22px 0; }
1419  .ms-faq-a-inner { padding: 0; font-size: 13px; }
1420  .ms-faq-item.is-open .ms-faq-a-inner { padding-bottom: 20px; }
1421}
1422@media (max-width: 540px) {
1423  .ms-faq-q { font-size: 16px; }
1424}
1425
1426/* FAQ open-state pixel animation (canvas injected per item by shared-components.js) */
1427.ms-faq-open-canvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: 0; transition: opacity 0.5s ease; z-index: 0; }
1428.ms-faq-item.is-open .ms-faq-open-canvas { opacity: 1; }
1429.ms-faq-q, .ms-faq-a { position: relative; z-index: 1; }
1430
1431/* ─── FOOTER (canonical — identical across all pages at every breakpoint) ─── */
1432.footer-section { padding: 90px 40px 0; gap: 24px; }
1433.footer-eyebrow { font-family: 'Silkscreen', monospace; }
1434/* type comes from the canonical section-H2 group above */
1435.footer-subtitle { font-size: 18px; color: #ABABAB; max-width: 640px; line-height: 1.6; margin: -16px auto 0; }
1436@media (max-width: 1024px) {
1437}
1438@media (max-width: 768px) {
1439  .footer-section { padding: 48px 0 0; }
1440  /* 380px (not the old per-page 310px) so typical footer copy wraps to 2 lines,
1441     not 3, on phones. Wins over each page's .footer-subtitle { max-width: 310px }
1442     by specificity + load order. */
1443  .footer-section .footer-subtitle { max-width: 380px; }
1444  /* .hero-pills exempted: the pills row is a shrink-wrapped component — this
1445     blanket width:100% stretched it edge-to-edge, and the 12px padding
1446     overrode the component's own 16px (0-2-0 beats .hero-pills' 0-1-0). */
1447  .footer-section > *:not(.bolt-image-wrap, .hero-pills) { padding-left: 12px; padding-right: 12px; box-sizing: border-box; width: 100%; }
1448  .footer-subtitle { font-size: 16px; max-width: 310px; }
1449  /* (The old `.footer-section .card-form-strip { margin-top: -28px !important }`
1450     mobile nudge is gone — strips now carry .footer-cta-strip, driven by the
1451     --sub-to-cta token, same as .footer-cta-btn.) */
1452}
1453
1454/* ─── FOOTER BOLT WORDMARK — bigger/full-bleed on MOBILE ONLY ─────────────── */
1455@media (max-width: 768px) {
1456  /* calc centering = full-bleed regardless of the footer's side padding */
1457  .bolt-image-wrap { width: 100vw !important; margin-left: calc(-50vw + 50%) !important; height: 42vw !important; max-height: 42vw !important; }
1458  .bolt-image-wrap img, #bolt-shimmer-canvas { transform: scale(1.35); transform-origin: center top; }
1459}
1460
1461/* ─── HERO VIDEO COMPONENT — narrow centered card ─────────────────────────────
1462   A narrow, height-aware video card that sits centered under a hero H1+subtitle:
1463   16:9 poster + play button → opens a fullscreen modal (tella embed); a strip is
1464   attached to the bottom edge (social-proof + CTA, a label, or an email form).
1465
1466   Sizing: .hero-video-col is capped to min(800px, 88vh). 800px matches the
1467   Enterprise hero card width; the 88vh term shrinks the card on shorter
1468   viewports so the whole H1 + subtitle + card stack stays centered inside a
1469   full-height hero (min-height: 100vh/100dvh) without clipping or crowding the top.
1470
1471   Required markup:
1472     <div class="hero-video-col">
1473       <div class="hero-video-card" id="hero-video-card">
1474         <div class="hero-video-area">
1475           <img src="…poster.webp" alt="" aria-hidden="true"
1476                style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;">
1477           <div class="hero-video-overlay"></div>
1478           <div class="hero-play-btn" id="hero-play-btn"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
1479         </div>
1480         <div class="hero-card-strip hero-card-strip--social"> … strip content … </div>
1481       </div>
1482     </div>
1483     <div class="video-modal" id="video-modal" style="--video-poster:url('…poster.webp')">
1484       <div class="video-modal-inner">
1485         <button class="video-modal-close" id="video-modal-close">×</button>
1486         <div id="modal-video" data-youtube="<youtube-id>"></div>
1487       </div>
1488     </div>
1489   Video source: set data-youtube="<id>" (preferred) OR data-tella="<slug>" on
1490   #modal-video. Behavior (open/close, embed injection, play ripple) lives in
1491   shared-components.js.
1492   Set the modal poster via the --video-poster custom property (shown above).
1493   ──────────────────────────────────────────────────────────────────────────── */
1494.hero-video-col { display: flex; flex-direction: column; gap: 0; width: 100%; max-width: min(800px, 88vh); margin: 0 auto; }
1495.hero-video-card { border-radius: 4px; overflow: hidden; border: none; cursor: pointer; display: flex; flex-direction: column; box-shadow: 0 24px 64px rgba(0,0,0,0.5); }
1496.hero-video-area { position: relative; aspect-ratio: 16/9; background: #000; border-radius: 4px 4px 0 0; overflow: hidden; }
1497/* Scoped to the video card's own markup. An unscoped .hero-video-overlay
1498   collides with pages that use the same class name for a full-page hero
1499   darkening layer (microsoft.html: rgba(10,10,14,0.72) over msft-bg.webp) —
1500   this vignette would silently replace it and the hero renders far too bright. */
1501.hero-video-area > .hero-video-overlay { position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.45) 100%); z-index: 2; }
1502
1503/* play button + click ripple/return animations */
1504.hero-play-btn {
1505  position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 4;
1506  width: 56px; height: 56px; border-radius: 50%;
1507  border: 1.5px solid rgba(255,255,255,0.55); background: rgba(255,255,255,0.08);
1508  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
1509  display: flex; align-items: center; justify-content: center;
1510  transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1), background 0.25s, box-shadow 0.25s;
1511  pointer-events: none;
1512}
1513.hero-video-card:hover .hero-play-btn { transform: translate(-50%,-50%) scale(1.12); background: rgba(255,255,255,0.15); box-shadow: 0 0 24px rgba(255,255,255,0.12); }
1514.hero-play-btn svg { width: 26px; height: 26px; fill: #fff; margin-left: 3px; }
1515@keyframes play-ripple { 0% { transform:translate(-50%,-50%) scale(1); opacity:1; } 100% { transform:translate(-50%,-50%) scale(1.65); opacity:0; } }
1516@keyframes play-fadein { from { opacity:0; transform:translate(-50%,-50%) scale(0.85); } to { opacity:1; transform:translate(-50%,-50%) scale(1); } }
1517.hero-play-btn.clicked  { animation: play-ripple 0.55s cubic-bezier(0.22,1,0.36,1) forwards; }
1518.hero-play-btn.returning { animation: play-fadein 0.35s cubic-bezier(0.34,1.56,0.64,1) forwards; }
1519
1520/* bottom strip — base + content variants (social-proof / label / email form) */
1521.hero-card-strip { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; padding: 14px 20px; background: #060606; }
1522.hero-card-strip--social { flex-wrap: nowrap; }
1523.hero-card-social-line { display: flex; align-items: flex-start; gap: 10px; flex: 1 1 auto; min-width: 0; }
1524.hero-card-social-text { font-size: 14px; color: rgba(255,255,255,0.55); line-height: 1.5; }
1525.hero-card-social-text strong { color: rgba(255,255,255,0.78); font-weight: 600; }
1526.hero-social-proof-dot { width: 7px; height: 7px; border-radius: 50%; background: #22c55e; flex-shrink: 0; margin-top: 4px; }
1527.hero-card-strip--social .hero-social-proof-dot { margin-top: 6px; }
1528.hero-strip-btn { flex-shrink: 0; }
1529.hero-card-strip-sub { font-size: 15px; font-weight: 500; color: #fff; margin: 0; line-height: 1.2; display: inline-flex; align-items: baseline; gap: 0.25em; flex-shrink: 0; }
1530.hero-card-strip-text { font-size: 14px; color: rgba(255,255,255,0.7); flex-shrink: 0; }
1531.hero-card-strip-text em { font-style: italic; color: #FFB093; font-family: 'Cormorant Garamond', serif; font-size: 16px; }
1532.hero-card-form { display: flex; gap: 0; flex: 1; max-width: 340px; background: rgba(255,255,255,0.08); border-radius: 4px; overflow: hidden; }
1533.hero-card-input { flex: 1; height: 44px; background: transparent; border: none; padding: 0 14px; font-size: 14px; font-family: 'Inter', sans-serif; color: #fff; outline: none; }
1534.hero-card-input::placeholder { color: rgba(255,255,255,0.4); }
1535.hero-card-submit { height: 44px; padding: 0 18px; background: #1488FC; border: none; border-radius: 0 2px 2px 0; color: #fff; font-size: 14px; font-weight: 600; font-family: 'Inter', sans-serif; cursor: pointer; white-space: nowrap; transition: background 0.2s; }
1536.hero-card-submit:hover { background: #0f6fd0; }
1537
1538/* fullscreen video modal (poster set per-page via --video-poster) */
1539.video-modal { display: none; position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,0.88); align-items: center; justify-content: center; }
1540.video-modal.open { display: flex; }
1541/* width grows to 1280px but the 149.33vh term (= 84vh × 16/9) caps height so it never overflows short screens */
1542.video-modal-inner { position: relative; width: min(92vw, 1280px, 149.33vh); aspect-ratio: 16/9; border-radius: 4px; overflow: hidden; background: #000 var(--video-poster, none) center/cover no-repeat; }
1543.video-modal-inner video, .video-modal-inner iframe, #modal-video, #modal-video iframe { width: 100%; height: 100%; object-fit: cover; border: 0; display: block; }
1544/* fade the iframe in on load so the poster bridges the gap (no black flash) */
1545#modal-video iframe { opacity: 0; transition: opacity 0.4s ease; }
1546#modal-video iframe.is-loaded { opacity: 1; }
1547.video-modal-close { position: absolute; top: -40px; right: 0; background: none; border: none; color: rgba(255,255,255,0.7); font-size: 28px; cursor: pointer; line-height: 1; }
1548.video-modal-close:hover { color: #fff; }
1549/* mobile: stack the social strip — full-width social line over a full-width CTA */
1550@media (max-width: 768px) {
1551  .hero-card-strip--social { flex-direction: column; align-items: stretch; gap: 14px; padding: 16px; }
1552  .hero-card-social-text { font-size: 13px; }
1553  .hero-card-strip--social .hero-strip-btn { width: 100%; }
1554}
1555
1556</style>
1557
1558<script>
1559/* ============================================================================
1560   PIXEL CANVAS HELPER (verbatim from solutions.html / security-agent.html)
1561   ========================================================================== */
1562function makePixelCanvas(id, spacing, dot, waveFn, opacity, color) {
1563  color = color || '20,136,252';
1564  const canvas = document.getElementById(id);
1565  if (!canvas) return;
1566  let phases = [];
1567  function buildPhases(cols, rows) {
1568    phases = Array.from({ length: rows }, () => Array.from({ length: cols }, () => Math.random() * Math.PI * 2));
1569  }
1570  function resize() {
1571    const parent = canvas.parentElement;
1572    const w = parent.offsetWidth || parent.getBoundingClientRect().width;
1573    const h = parent.offsetHeight || parent.getBoundingClientRect().height;
1574    if (!w || !h) return;
1575    canvas.width = Math.round(w); canvas.height = Math.round(h);
1576    buildPhases(Math.ceil(w / spacing), Math.ceil(h / spacing));
1577  }
1578  window.addEventListener('load', resize); window.addEventListener('resize', resize);
1579  new ResizeObserver(function() { resize(); }).observe(canvas.parentElement);
1580  requestAnimationFrame(function() { resize(); });
1581  function draw(t) {
1582    const w = canvas.width, h = canvas.height;
1583    if (!w || !h) { requestAnimationFrame(draw); return; }
1584    const ctx = canvas.getContext('2d');
1585    ctx.clearRect(0, 0, w, h);
1586    const cols = Math.ceil(w / spacing); const rows = Math.ceil(h / spacing);
1587    for (let r = 0; r < rows; r++) {
1588      for (let c = 0; c < cols; c++) {
1589        const phase = (phases[r] && phases[r][c]) || 0;
1590        const op = waveFn(c, r, t, phase) * opacity;
1591        if (op < 0.04) continue;
1592        ctx.fillStyle = `rgba(${color},${op.toFixed(3)})`;
1593        ctx.fillRect(c * spacing, r * spacing, dot, dot);
1594      }
1595    }
1596    requestAnimationFrame(draw);
1597  }
1598  requestAnimationFrame(draw);
1599}
1600
1601/* ── Hero pixel field — diagonal wave, brand blue, fades toward the centre ── */
1602makePixelCanvas('hero-pixel-canvas', 9, 3, function(c, r, t) {
1603  const el = document.getElementById('hero-pixel-canvas');
1604  const W = el ? el.width : 1200, H = el ? el.height : 800;
1605  const px = c * 9, py = r * 9;
1606  /* Keep the field to the outer edges so it never fights the headline */
1607  const cx = W / 2, cy = H / 2;
1608  const dx = (px - cx) / (W * 0.5), dy = (py - cy) / (H * 0.5);
1609  const edge = Math.min(1, Math.sqrt(dx * dx + dy * dy));
1610  const fade = Math.pow(edge, 1.6);
1611  if (fade < 0.05) return 0;
1612  const wave = Math.sin((px * 0.7 + py * 0.9) / 90 - t * 0.00055);
1613  return Math.max(0, wave) * 0.55 * fade;
1614}, 1.0, '20,136,252');
1615
1616/* ─── FAQ SECTION-LEVEL PIXEL CANVAS (breathing brand-blue wave) ───────────
1617   Canonical implementation, verbatim from platform/referral (and the other
1618   marketing FAQs). The cy = H * 1.22 anchor is the point of it: the field's
1619   centre sits BELOW the section, so only the top of the arc is visible and it
1620   reads as a low dome under the last question. An earlier pass here mistook
1621   that for a mispositioned field and re-centred it at 0.60H, which spread it
1622   across the questions — the size was the problem, not the anchor. ───────── */
1623makePixelCanvas('faq-pixel-canvas', 9, 2,
1624  function(c, r, t, p) {
1625    var el = document.getElementById('faq-pixel-canvas');
1626    if (!el) return 0;
1627    var H = el.height, W = el.width;
1628    var px = c * 9, py = r * 9;
1629    var cx = W / 2, cy = H * 1.22;
1630    var dx = px - cx, dy = py - cy;
1631    var dist = Math.sqrt(dx*dx + dy*dy) / (Math.max(W, H) * 0.9);
1632    var breath = Math.sin(t * 0.0006) * 0.5 + 0.5;
1633    var ripple = Math.sin(dist * 10 - t * 0.0012) * 0.5 + 0.5;
1634    var fade = Math.max(0, 1 - dist * 1.4);
1635    return fade * (0.08 + breath * 0.06 + ripple * 0.12);
1636  }, 1.0, '20,136,252');
1637
1638</script>
1638
1639
1640<script>
1641/* ── Team-gallery pixel border — dots occupy only the band around the grid,
1642      densest against its edge and fading outward. padCache is hoisted so the
1643      per-dot wave never calls getComputedStyle (it runs per dot per frame). ── */
1644(function () {
1645  if (!document.getElementById('teamshot-pixels')) return;
1646  var pad = 32;
1647  function refreshPad() {
1648    var f = document.querySelector('.teamshot-frame');
1649    if (f) pad = parseFloat(getComputedStyle(f).paddingLeft) || 32;
1650  }
1651  refreshPad();
1652  window.addEventListener('resize', refreshPad);
1653
1654  makePixelCanvas('teamshot-pixels', 7, 2, function (c, r, t, p) {
1655    var el = document.getElementById('teamshot-pixels');
1656    if (!el) return 0;
1657    var W = el.width, H = el.height, px = c * 7, py = r * 7;
1658    /* how far this dot sits OUTSIDE the grid rect (0 = over the photos) */
1659    var dx = Math.max(pad - px, px - (W - pad), 0);
1660    var dy = Math.max(pad - py, py - (H - pad), 0);
1661    var d = Math.max(dx, dy);
1662    if (d <= 0) return 0;
1663    var fade = Math.max(0, 1 - d / pad);
1664    var wave = Math.sin((px * 0.5 + py * 0.7) / 42 - t * 0.0016);
1665    var twinkle = (Math.sin(p * 53.7 + t * 0.004) * 0.5 + 0.5) * 0.22;
1666    return (0.18 + Math.max(0, wave) * 0.5 + twinkle) * Math.pow(fade, 1.25);
1667    /* deeper indigo than the brand #1488FC used elsewhere on the page */
1668  }, 1.0, '38,72,214');
1669})();
1670
1671/* ── Testimonial panel pixel fields (microsoft .success-pull-quote treatment) ── */
1672['testi-pixels-1', 'testi-pixels-2', 'testi-pixels-3', 'testi-pixels-4'].forEach(function (id, i) {
1673  makePixelCanvas(id, 9, 2, function (c, r, t, p) {
1674    var el = document.getElementById(id);
1675    var W = el ? el.width : 900, H = el ? el.height : 460;
1676    var px = c * 9, py = r * 9;
1677    /* fade toward the photo edge so the field never fights the portrait */
1678    var fade = Math.max(0, 1 - px / (W * 0.75)) * Math.max(0, 1 - py / (H * 1.15));
1679    if (fade <= 0) return 0;
1680    var wave = Math.sin((px * 0.6 + py * 0.9) / 80 - t * 0.0006 + i * 2.1);
1681    var twinkle = (Math.sin(p * 53.7 + t * 0.004) * 0.5 + 0.5) * 0.06;
1682    return (0.05 + Math.max(0, wave) * 0.30 + twinkle) * fade;
1683  }, 1.0, '20,136,252');
1684});
1685
1686/* ── Testimonial slider — scroll-snap does the motion; this wires the arrows
1687      and disables them at the ends. If everything fits (very wide screens),
1688      both arrows disable. ── */
1689(function () {
1690  var slider = document.querySelector('.testi-slider');
1691  var stack = slider ? slider.querySelector('.testi-stack') : null;
1692  if (!stack) return;
1693  var prev = document.getElementById('testi-prev');
1694  var next = document.getElementById('testi-next');
1695
1696  function step() {
1697    var card = stack.querySelector('.testi-panel');
1698    return card ? card.getBoundingClientRect().width + 20 : 340;
1699  }
1700  function update() {
1701    var max = stack.scrollWidth - stack.clientWidth - 1;
1702    if (prev) prev.disabled = stack.scrollLeft <= 0;
1703    if (next) next.disabled = stack.scrollLeft >= max;
1704    slider.classList.toggle('is-end', stack.scrollLeft >= max);
1705  }
1706  /* Hand-rolled glide: scrollBy/scrollTo with behavior:'smooth' silently
1707     no-op on this element in Chrome (instant scrolls work fine), so the
1708     easing is driven by rAF directly. Mobile's mandatory scroll-snap must be
1709     suspended for the duration — it re-snaps after every per-frame
1710     scrollLeft write, which collapses the animation into a jump-cut. The
1711     glide lands exactly on a snap point, so restoring snap doesn't shift
1712     anything. */
1713  var gliding = null;
1714  function endGlide() {
1715    if (gliding) { cancelAnimationFrame(gliding); gliding = null; }
1716    stack.style.scrollSnapType = '';
1717  }
1718  function glide(delta) {
1719    var from = stack.scrollLeft;
1720    var to = Math.max(0, Math.min(stack.scrollWidth - stack.clientWidth, from + delta));
1721    if (to === from) return;
1722    if (gliding) cancelAnimationFrame(gliding);
1723    stack.style.scrollSnapType = 'none';
1724    var t0 = null;
1725    var DUR = 380;
1726    function ease(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
1727    function frameStep(ts) {
1728      if (t0 === null) t0 = ts;
1729      var p = Math.min(1, (ts - t0) / DUR);
1730      stack.scrollLeft = from + (to - from) * ease(p);
1731      if (p < 1) {
1732        gliding = requestAnimationFrame(frameStep);
1733      } else {
1734        endGlide();
1735      }
1736    }
1737    gliding = requestAnimationFrame(frameStep);
1738  }
1739  /* a real touch takes over instantly — stop writing scrollLeft against it */
1740  stack.addEventListener('touchstart', endGlide, { passive: true });
1741  if (prev) prev.addEventListener('click', function () { glide(-step()); });
1742  if (next) next.addEventListener('click', function () { glide(step()); });
1743  stack.addEventListener('scroll', update, { passive: true });
1744  window.addEventListener('resize', update);
1745  update();
1746})();
1747
1748/* ── Section divider — verbatim solutions/security-agent traveling shimmer.
1749      (Restored: an earlier range-delete of the hero-video block took this
1750      with it, leaving the divider markup painting nothing.) ── */
1751if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
1752  /* Two instances. The +1.0 / +2.6 phase offsets are the same values
1753     solutions.html uses, so the two bands travel out of step rather than
1754     pulsing together. */
1755  [['section-divider-canvas', 1.0], ['section-divider-canvas-2', 2.6]].forEac
1755h(function (cfg) {
1756    makePixelCanvas(cfg[0], 11, 2,
1757      function (c, r, t, p) {
1758        var px = c * 11, py = r * 11;
1759        var phase = (px * 0.012 + py * 0.018) - t * 0.0018 + cfg[1];
1760        var band = Math.sin(phase) * 0.5 + 0.5;
1761        var highlight = Math.pow(band, 3) * 0.32;
1762        var twinkle = (Math.sin(p * 53.7 + t * 0.004) * 0.5 + 0.5) * 0.07;
1763        return 0.06 + highlight + twinkle;
1764      }, 1.0, '170,185,210');
1765  });
1766}
1767</script>
1767
1768
1769<script>
1770/* ============================================================================
1771   FAQ ACCORDION — toggles .is-open only. The expand itself is a CSS
1772   grid-template-rows 0fr->1fr transition (shared-components.css); the per-item
1773   pixel canvas is owned by shared-components.js.
1774   (Restored: lost to a range-delete when the hero-video block was removed.)
1775   ========================================================================== */
1776(function () {
1777  var items = [].slice.call(document.querySelectorAll('.ms-faq-item'));
1778
1779  /* The class alone is a purely visual state — screen readers get no
1780     announcement from it. Wire aria-expanded on the button and point it at the
1781     answer panel so the open/closed state is actually conveyed. */
1782  function setState(item, open) {
1783    item.classList.toggle('is-open', open);
1784    var btn = item.querySelector('.ms-faq-q');
1785    var panel = item.querySelector('.ms-faq-a');
1786    if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
1787    if (panel) panel.setAttribute('aria-hidden', open ? 'false' : 'true');
1788  }
1789
1790  items.forEach(function (item, i) {
1791    var btn = item.querySelector('.ms-faq-q');
1792    var panel = item.querySelector('.ms-faq-a');
1793    if (!btn) return;
1794    if (panel) {
1795      if (!panel.id) panel.id = 'faq-panel-' + (i + 1);
1796      btn.setAttribute('aria-controls', panel.id);
1797      panel.setAttribute('role', 'region');
1798      panel.setAttribute('aria-labelledby', btn.id || (btn.id = 'faq-q-' + (i + 1)));
1799    }
1800    setState(item, item.classList.contains('is-open'));
1801    btn.addEventListener('click', function () {
1802      var willOpen = !item.classList.contains('is-open');
1803      items.forEach(function (o) { setState(o, false); });
1804      setState(item, willOpen);
1805    });
1806  });
1807})();
1808
1809/* ============================================================================
1810   GALLERY LIGHTBOX — click/Enter a tile to open, arrows or buttons to move
1811   through the set, Escape or backdrop to close. Wraps at both ends. Focus is
1812   returned to the tile that opened it so keyboard users don't lose their place.
1813   ========================================================================== */
1814(function () {
1815  var tiles = [].slice.call(document.querySelectorAll('.gallery-item'));
1816  var box   = document.getElementById('lightbox');
1817  var img   = document.getElementById('lightbox-img');
1818  var idxEl = document.getElementById('lightbox-i');
1819  var nEl   = document.getElementById('lightbox-n');
1820  if (!tiles.length || !box || !img) return;
1821
1822  var shots = tiles.map(function (t) {
1823    var i = t.querySelector('img');
1824    return { src: i.getAttribute('src'), alt: i.getAttribute('alt') || '' };
1825  });
1826  var at = 0, opener = null;
1827  if (nEl) nEl.textContent = String(shots.length);
1828
1829  function show(i, dir) {
1830    at = (i + shots.length) % shots.length;   /* wrap both directions */
1831    img.src = shots[at].src;
1832    img.alt = shots[at].alt;
1833    if (idxEl) idxEl.textContent = String(at + 1);
1834    img.classList.remove('lb-anim-next', 'lb-anim-prev');
1835    if (dir) {
1836      void img.offsetWidth;   /* restart the animation when the direction repeats */
1837      img.classList.add(dir > 0 ? 'lb-anim-next' : 'lb-anim-prev');
1838    }
1839  }
1840  function open(i, from) {
1841    opener = from || null;
1842    show(i);
1843    box.classList.add('is-open');
1844    document.body.style.overflow = 'hidden';
1845    var close = document.getElementById('lightbox-close');
1846    if (close) close.focus();
1847  }
1848  function close() {
1849    box.classList.remove('is-open');
1850    document.body.style.overflow = '';
1851    if (opener) opener.focus();
1852  }
1853
1854  tiles.forEach(function (t, i) {
1855    t.setAttribute('aria-label', 'View photo ' + (i + 1) + ' of ' + shots.length);
1856    t.addEventListener('click', function () { open(i, t); });
1857    t.addEventListener('keydown', function (e) {
1858      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(i, t); }
1859    });
1860  });
1861
1862  var prev = document.getElementById('lightbox-prev');
1863  var next = document.getElementById('lightbox-next');
1864  if (prev) prev.addEventListener('click', function () { show(at - 1, -1); });
1865  if (next) next.addEventListener('click', function () { show(at + 1, 1); });
1866  var closeBtn = document.getElementById('lightbox-close');
1867  if (closeBtn) closeBtn.addEventListener('click', close);
1868
1869  /* backdrop only — clicks on the image or the controls must not close */
1870  box.addEventListener('click', function (e) { if (e.target === box) close(); });
1871
1872  document.addEventListener('keydown', function (e) {
1873    if (!box.classList.contains('is-open')) return;
1874    if (e.key === 'Escape') { close(); return; }
1875    if (e.key === 'ArrowLeft')  { show(at - 1, -1); return; }
1876    if (e.key === 'ArrowRight') { show(at + 1, 1); return; }
1877
1878    /* Focus trap. The overlay is aria-modal, so focus must not reach the page
1879       behind it — without this, Tab walks onto controls the user can't see. */
1880    if (e.key !== 'Tab') return;
1881    var focusable = [].slice.call(
1882      box.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
1883    ).filter(function (el) {
1884      return !el.disabled && el.getClientRects().length;
1885    });
1886    if (!focusable.length) { e.preventDefault(); return; }
1887    var first = focusable[0], last = focusable[focusable.length - 1];
1888    var active = document.activeElement;
1889    if (e.shiftKey && (active === first || !box.contains(active))) {
1890      e.preventDefault(); last.focus();
1891    } else if (!e.shiftKey && (active === last || !box.contains(active))) {
1892      e.preventDefault(); first.focus();
1893    }
1894  });
1895})();
1896
1897/* ── Film ambient preview — a muted YouTube loop built by JS behind the
1898      poster; the poster fades only once the player reports it is actually
1899      playing, so there's never a black frame. Same handoff as
1900      solutions/ai-for-real-estate, with two hardenings learned here:
1901
1902      1. The iframe is INSERTED BY JS when the card nears the viewport, never
1903         written in markup. A parse-time iframe intermittently bootstraps into
1904         YouTube's error-153 "configuration error" state — it answers the
1905         widget handshake but has no video and ignores playVideo — while a
1906         dynamically-inserted iframe bootstraps reliably. This also means the
1907         third-party player is only fetched by visitors who scroll here.
1908      2. If a full nudge burst passes with the card in view and the player
1909         never reports a single playerState, that embed is dead — tear it out
1910         and insert a fresh one (invisible behind the poster). Capped so a
1911         genuinely unavailable video doesn't loop forever. ── */
1912(function () {
1913  var area = document.querySelector('.film-card .hero-video-area');
1914  var cover = document.getElementById('film-preview-cover');
1915  if (!area || !cover) return;
1916
1917  var SRC = 'https://www.youtube-nocookie.com/embed/iIrbX4FB3-g?autoplay=1&mute=1&controls=0&loop=1&playlist=iIrbX4FB3-g&modestbranding=1&rel=0&iv_load_policy=3&disablekb=1&playsinline=1&showinfo=0&enablejsapi=1&start=4';
1918  var frame = null;
1919  var loaded = false;
1920  var burst = null;
1921  var boots = 0;
1922  var sawState = false;
1923  var inView = false;
1924
1925  function post(msg) {
1926    if (!frame) return;
1927    try { frame.contentWindow.postMessage(JSON.stringify(msg), '*'); } catch (e) {}
1928  }
1929  /* Commands carry id+channel like the official widget client's do. */
1930  function send(func, args) {
1931    post({ event: 'command', func: func, args: args || [], id: 'film-preview-iframe', channel: 'widget' });
1932  }
1933  /* The widget API only starts emitting events after a plain
1934     {event:'listening'} message — NOT a command (an earlier version sent
1935     {event:'command', func:'listening'}, which the player ignores, so
1936     onStateChange never arrived and the poster never faded even while the
1937     loop was playing behind it). */
1938  function handshake() {
1939    post({ event: 'listening', id: 'film-preview-iframe', channel: 'widget' });
1940    send('addEventListener', ['onStateChange']);
1941  }
1942  function nudge() { send('mute'); send('playVideo'); }
1943
1944  /* Reveal the loop ONLY once the player reports it is actually playing.
1945     A blind timer was worse than no fade: when autoplay doesn't start, the
1946     poster disappeared and left a black card. Now a stalled player just keeps
1947     its poster, which is a legitimate-looking state. */
1948  function reveal() {
1949    if (cover.dataset.done) return;
1950    cover.dataset.done = '1';
1951    cover.style.opacity = '0';
1952    setTimeout(function () { cover.style.display = 'none'; }, 700);
1953  }
1954  window.addEventListener('message', function (e) {
1955    if (!/(^|\.)youtube(-nocookie)?\.com$/.test((function () {
1956      try { return new URL(e.origin).hostname; } catch (_) { return ''; }
1957    })())) return;
1958    var d;
1959    try { d = JSON.parse(e.data); } catch (_) { return; }
1960    if (!d) return;
1961    /* Playing arrives as onStateChange (info 1) once subscribed, but also as
1962       infoDelivery {playerState:1} — accept either. Any playerState at all
1963       marks the embed healthy (sawState). */
1964    if (d.event === 'onStateChange' && typeof d.info === 'number') {
1965      sawState = true;
1966      if (d.info === 1) reveal();
1967    }
1968    if (d.event === 'infoDelivery' && d.info && d.info.playerState !== undefined) {
1969      sawState = true;
1970      if (d.info.playerState === 1) reveal();
1971    }
1972  });
1973
1974  function boot() {
1975    if (frame || boots >= 3 || cover.dataset.done) return;
1976    boots++;
1977    loaded = false;
1978    sawState = false;
1979    frame = document.createElement('iframe');
1980    frame.id = 'film-preview-iframe';
1981    frame.title = '';
1982    frame.setAttribute('aria-hidden', 'true');
1983    frame.setAttribute('frameborder', '0');
1984    frame.allow = 'autoplay; encrypted-media';
1985    frame.style.cssText = 'position:absolute;top:50%;left:50%;width:100%;height:100%;transform:translate(-50%,-50%) scale(1.08);transform-origin:center;pointer-events:none;border:0;display:block;z-index:1;';
1986    frame.addEventListener('load', function () {
1987      loaded = true;
1988      handshake();
1989      if (inView) kick();
1990    });
1991    frame.src = SRC;
1992    area.insertBefore(frame, area.firstChild);
1993  }
1994  function reboot() {
1995    if (frame) { frame.parentNode && frame.parentNode.removeChild(frame); frame = null; }
1996    boot();
1997  }
1998
1999  /* Nudge while the card is IN VIEW. The card sits below the 100vh hero and
2000     YouTube defers offscreen autoplay, so a burst tied only to iframe load
2001     would expire before the user scrolls down. The observer stays connected
2002     until the loop has actually revealed: a burst that expires early gets a
2003     fresh one on the next re-entry instead of latching off until reload. */
2004  function kick() {
2005    if (cover.dataset.done || !loaded) return;
2006    if (burst) return;                    /* a burst is already running */
2007    handshake();
2008    nudge();
2009    var tries = 0;
2010    burst = setInterval(function () {
2011      if (cover.dataset.done) { clearInterval(burst); burst = null; return; }
2012      handshake();
2013      nudge();
2014      if (++tries >= 8) {
2015        clearInterval(burst); burst = null;
2016        if (!sawState && inView) reboot();   /* dead embed — fresh bootstrap */
2017      }
2018    }, 1200);
2019  }
2020
2021  if ('IntersectionObserver' in window) {
2022    var io = new IntersectionObserver(function (entries) {
2023      for (var i = 0; i < entries.length; i++) inView = entries[i].isIntersecting;
2024      if (!inView) return;
2025      if (cover.dataset.done) { io.disconnect(); return; }
2026      boot();
2027      kick();
2028    }, { threshold: 0.35, rootMargin: '160px 0px' });
2029    io.observe(area);
2030  } else {
2031    inView = true;
2032    boot();
2033  }
2034})();
2035
2036/* ============================================================================
2037   CARD FLASHLIGHT — radial glow follows the cursor across the perk cards.
2038   Verbatim behaviour from solutions.html's .unlock-card.
2039   ========================================================================== */
2040(function () {
2041  document.querySelectorAll('.perk-card').forEach(function (el) {
2042    el.addEventListener('mousemove', function (e) {
2043      var r = el.getBoundingClientRect();
2044      el.style.setProperty('--mx', (e.clientX - r.left) + 'px');
2045      el.style.setProperty('--my', (e.clientY - r.top) + 'px');
2046    });
2047  });
2048})();
2049
2050/* ============================================================================
2051   CARD HOVER PIXEL FIELD — animated dot field that fades in on hover.
2052   Byte-for-byte the rotating-wave algo from solutions/ai-for-real-estate's
2053   .template-hover-canvas, pointed at this page's card classes. Only draws
2054   while a card is actually hovered (the `hover` flag), so an idle page does
2055   no per-frame work.
2056   ========================================================================== */
2057(function () {
2058  var cards = document.querySelectorAll('.value-card');
2059  if (!cards.length) return;
2060  var dpr = window.devicePixelRatio || 1;
2061  var objs = [];
2062  cards.forEach(function (card, idx) {
2063    var canvas = document.createElement('canvas');
2064    canvas.className = 'card-hover-canvas';
2065    canvas.setAttribute('aria-hidden', 'true');
2066    card.insertBefore(canvas, card.firstChild);
2067    var obj = { canvas: canvas, card: card, phase: idx * 1.7, hover: false };
2068    card.addEventListener('mouseenter', function () { obj.hover = true; });
2069    card.addEventListener('mouseleave', function () { obj.hover = false; });
2070    objs.push(obj);
2071  });
2072  function resizeAll() {
2073    objs.forEach(function (o) {
2074      o.canvas.width  = o.card.offsetWidth  * dpr;
2075      o.canvas.height = o.card.offsetHeight * dpr;
2076    });
2077  }
2078  resizeAll();
2079  window.addEventListener('resize', resizeAll);
2080  function draw(t) {
2081    objs.forEach(function (o) {
2082      if (!o.hover) return;
2083      var canvas = o.canvas, h = o.card.offsetHeight;
2084      if (canvas.height !== h * dpr) { canvas.width = o.card.offsetWidth * dpr; canvas.height = h * dpr; }
2085      var ctx = canvas.getContext('2d');
2086      var W = canvas.width, H = canvas.height;
2087      ctx.clearRect(0, 0, W, H);
2088      var sp = 7 * dpr, maxR = 2.2 * dpr;
2089      var cols = Math.ceil(W / sp) + 1, rows = Math.ceil(H / sp) + 1;
2090      for (var r = 0; r < rows; r++) {
2091        for (var c = 0; c < cols; c++) {
2092          var px = c * sp, py = r * sp;
2093          var angle1 = t * 0.00012 + o.phase;
2094          var u1 = (px * Math.cos(angle1) + py * Math.sin(angle1)) / (350 * dpr);
2095          var v1 = (-px * Math.sin(angle1) + py * Math.cos(angle1)) / (220 * dpr);
2096          var angle2 = t * 0.00022 + o.phase + 1.5;
2097          var u2 = (px * Math.cos(angle2) + py * Math.sin(angle2)) / (180 * dpr);
2098          var v2 = (-px * Math.sin(angle2) + py * Math.cos(angle2)) / (120 * dpr);
2099          var wave = (Math.sin(u1 * Math.PI * 2 + t * 0.00030) * Math.cos(v1 * Math.PI * 1.5))
2100                   * 0.55 + (Math.sin(u2 * Math.PI * 2 + t * 0.00060) * 0.45);
2101          var density = Math.pow(Math.max(0, wave * 0.5 + 0.5), 0.8);
2102          var rand = (Math.sin(c * 127.1 + r * 311.7 + o.phase * 50) * 0.5 + 0.5) * 0.9;
2103          if (density > rand) {
2104            var alpha = density * 0.22;
2105            ctx.beginPath();
2106            ctx.arc(px, py, maxR * density, 0, Math.PI * 2);
2107            ctx.fillStyle = 'rgba(180,185,195,' + alpha.toFixed(2) + ')';
2108            ctx.fill();
2109          }
2110        }
2111      }
2112    });
2113    requestAnimationFrame(draw);
2114  }
2115  requestAnimationFrame(draw);
2116})();
2117
2118/* ============================================================================
2119   OPEN ROLES — Greenhouse job board, rendered in our own design.
2120
2121   Source: the public Greenhouse JSON board for `stackblitz`. The /departments
2122   endpoint returns departments WITH their jobs nested, so one request gives us
2123   both the grouping and the listings.
2124
2125   If the request fails for any reason (network, CORS, Greenhouse outage), we
2126   fall back to the official Greenhouse embed script so the page never renders
2127   an empty roles section.
2128   ========================================================================== */
2129(function () {
2130  var BOARD = 'stackblitz';
2131  var API   = 'https://boards-api.greenhouse.io/v1/boards/' + BOARD + '/departments';
2132
2133  var out       = document.getElementById('roles-output');
2134  var stateEl   = document.getElementById('roles-state');
2135  var pillsEl   = document.getElementById('roles-pills');
2136  var locEl     = document.getElementById('roles-location');
2137  var heroMeta  = document.getElementById('hero-role-meta');
2138  if (!out) return;
2139
2140  var departments = [];      // [{ name, jobs: [...] }]
2141  var activeDept  = '';      // '' = all
2142  var pinIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>';
2143  var arrowIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>';
2144
2145  /* The Apply CTAs are built from the API response, so the shared pixel-fill
2146     hover (which runs once, at load) never sees them. Re-run it after every
2147     render — but this inline block sits ABOVE the shared-components.js tag,
2148     and a warm-cached API response can finish before that script executes,
2149     so the hook may not exist yet. Fall back to window 'load', by which point
2150     it always does. Idempotent per button, so double-calling is harmless. */
2151  function pixelizeCTAs() {
2152    if (window.__scPixelizeButtons) { window.__scPixelizeButtons(); return; }
2153    window.addEventListener('load', function () {
2154      if (window.__scPixelizeButtons) window.__scPixelizeButtons();
2155    }, { once: true });
2156  }
2157
2158  function esc(s) {
2159    return String(s == null ? '' : s).replace(/[&<>"']/g, function (ch) {
2160      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch];
2161    });
2162  }
2163
2164  /* Greenhouse titles frequently carry trailing spaces from the ATS */
2165  function clean(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
2166
2167  function loadEmbedFallback() {
2168    if (stateEl) stateEl.remove();
2169    var s = document.createElement('script');
2170    s.src = 'https://boards.greenhouse.io/embed/job_board/js?for=' + BOARD;
2171    document.body.appendChild(s);
2172  }
2173
2174  /* Greenhouse returns a job under EVERY department it belongs to, so the
2175     flattened arrays double-count cross-listed openings. Dedupe by job id for
2176     any count, and render each opening only under the first department it
2177     appears in so it never shows twice in the list. */
2178  function allJobs() {
2179    var seen = Object.create(null);
2180    return departments.reduce(function (acc, d) {
2181      d.jobs.forEach(function (j) {
2182        if (seen[j.id]) return;
2183        seen[j.id] = true;
2184        acc.push(j);
2185      });
2186      return acc;
2187    }, []);
2188  }
2189  /* jobs for a department, minus any already rendered under an earlier one */
2190  function ownJobs(dept, claimed) {
2191    return dept.jobs.filter(function (j) {
2192      if (claimed[j.id]) return false;
2193      claimed[j.id] = true;
2194      return true;
2195    });
2196  }
2197
2198  /* Location filter only — department is excluded here on purpose, so each
2199     pill's count reflects the OTHER filter rather than itself. */
2200  function passesTextAndLoc(job) {
2201    var loc = (locEl && locEl.value) || '';
2202    if (loc && clean(job.location && job.location.name) !== loc) return false;
2203    return true;
2204  }
2205
2206  function buildLocations() {
2207    if (!locEl) return;
2208    var seen = {};
2209    allJobs().forEach(function (j) {
2210      var l = clean(j.location && j.location.name);
2211      if (l) seen[l] = true;
2212    });
2213    Object.keys(seen).sort().forEach(function (l) {
2214      var o = document.createElement('option');
2215      o.value = l; o.textContent = l;
2216      locEl.appendChild(o);
2217    });
2218  }
2219
2220  function buildPills() {
2221    if (!pillsEl) return;
2222    var html = '';
2223    var total = allJobs().filter(passesTextAndLoc).length;
2224    html += '<button class="role-pill' + (activeDept === '' ? ' is-active' : '') + '" data-dept="">All Teams<span class="role-pill-count">' + total + '</span></button>';
2225    var pillClaimed = Object.create(null);
2226    departments.forEach(function (d) {
2227      var n = ownJobs(d, pillClaimed).filter(passesTextAndLoc).length;
2228      if (!n) return;
2229      html += '<button class="role-pill' + (activeDept === d.name ? ' is-active' : '') + '" data-dept="' + esc(d.name) + '">' + esc(d.name) + '<span class="role-pill-count">' + n + '</span></button>';
2230    });
2231    pillsEl.innerHTML = html;
2232    pillsEl.querySelectorAll('.role-pill').forEach(function (b) {
2233      b.addEventListener('click', function () {
2234        activeDept = b.getAttribute('data-dept') || '';
2235        render();
2236      });
2237    });
2238    updateFade();
2239  }
2240
2241  /* Edge fade only when the pill row actually overflows — a row that fits must
2242     never be clipped by a gradient that implies hidden content. */
2243  var wrap = pillsEl ? pillsEl.parentElement : null;
2244  function updateFade() {
2245    if (!wrap || !pillsEl) return;
2246    var scrollable = pillsEl.scrollWidth > pillsEl.clientWidth + 1;
2247    wrap.classList.toggle('is-scrollable', scrollable);
2248    var atEnd = pillsEl.scrollLeft + pillsEl.clientWidth >= pillsEl.scrollWidth - 2;
2249    wrap.classList.toggle('at-end', atEnd);
2250  }
2251  if (pillsEl) pillsEl.addEventListener('scroll', updateFade);
2252  window.addEventListener('resize', updateFade);
2253
2254  function render() {
2255    var html = '';
2256    var shown = 0;
2257
2258    var claimed = Object.create(null);
2259    departments.forEach(function (d) {
2260      var own = ownJobs(d, claimed);
2261      if (activeDept && d.name !== activeDept) return;
2262      var jobs = own.filter(passesTextAndLoc);
2263      if (!jobs.length) return;
2264      shown += jobs.length;
2265
2266      html += '<div class="roles-group">';
2267      html += '<p class="roles-group-label">' + esc(d.name) + '</p>';
2268      html += '<div class="roles-list">';
2269      jobs.forEach(function (j) {
2270        var loc = clean(j.location && j.location.name);
2271        html += '<a class="role-row" href="' + esc(j.absolute_url) + '" target="_blank" rel="noopener">';
2272        html += '<span class="role-title">' + esc(clean(j.title)) + '</span>';
2273        html += '<span class="role-loc">' + (loc ? pinIcon + esc(loc) : '') + '</span>';
2274        html += '<span class="hero-btn-ghost role-apply">' +
2275                  '<span class="btn-bg-hover"></span>' +
2276                  '<span class="btn-text-wrap"><span class="btn-text-inner"><span>Apply</span><span>Apply</span></span></span>' +
2277                '</span>';
2278        html += '</a>';
2279      });
2280      html += '</div></div>';
2281    });
2282
2283    if (!shown) {
2284      html = '<div class="roles-state">No roles match that filter right now &mdash; try another team or location.</div>';
2285    }
2286
2287    out.innerHTML = html;
2288    buildPills();
2289    pixelizeCTAs();
2290    wireRowHover();
2291  }
2292
2293  /* The whole row is the link, but the pixel-fill hover only listens on the
2294     button itself — so hovering the title/location left the Apply button
2295     inert (the .btn-pixelized class also disables the CSS-only hover, so
2296     border-color was the only cue). Forward the row's hover to the button
2297     with synthetic events; the extra button mouseleave guard re-asserts the
2298     fill when the pointer slides off the button but is still inside the row
2299     (the button's own real mouseleave would otherwise retract it). */
2300  function wireRowHover() {
2301    document.querySelectorAll('.role-row').forEach(function (row) {
2302      if (row.__hoverWired) return;
2303      row.__hoverWired = true;
2304      var btn = row.querySelector('.role-apply');
2305      if (!btn) return;
2306      var enter = function () { btn.dispatchEvent(new Event('mouseenter')); };
2307      row.addEventListener('mouseenter', enter);
2308      row.addEventListener('mouseleave', function () { btn.dispatchEvent(new Event('mouseleave')); });
2309      btn.addEventListener('mouseleave', function () { if (row.matches(':hover')) enter(); });
2310    });
2311  }
2312
2313  fetch(API, { headers: { 'Accept': 'application/json' } })
2314    .then(function (r) {
2315      if (!r.ok) throw new Error('HTTP ' + r.status);
2316      return r.json();
2317    })
2318    .then(function (data) {
2319      departments = (data.departments || [])
2320        .filter(function (d) { return d.jobs && d.jobs.length; })
2321        .map(function (d) { return { name: clean(d.name), jobs: d.jobs }; })
2322        .sort(function (a, b) { return a.name.localeCompare(b.name); });
2323
2324      if (!departments.length) { loadEmbedFallback(); return; }
2325
2326      var total = allJobs().length;
2327      var teams = departments.length;
2328
2329      if (heroMeta) {
2330        heroMeta.innerHTML = '<span class="hero-meta-dot"></span>' +
2331          total + ' open role' + (total === 1 ? '' : 's') + ' across ' + teams + ' team' + (teams === 1 ? '' : 's');
2332      }
2333
2334      buildLocations();
2335      render();
2336
2337      if (locEl) locEl.addEventListener('change', render);
2338    })
2339    .catch(function () {
2340      loadEmbedFallback();
2341    });
2342})();
2343
2344/* ============================================================================
2345   LOCAL SCROLL ANIMATIONS — .reveal-h word blur-reveal.
2346   The eyebrow scramble and .dsa-reveal are owned by shared-components.js; this
2347   only adds the .reveal-h variant (fires at ~50% viewport). We do NOT set
2348   __pageOwnsScrollAnims — the two never touch the same elements.
2349   ========================================================================== */
2350(function () {
2351  if (typeof IntersectionObserver === 'undefined') return;
2352
2353  function wrapWords(container) {
2354    var el = container.querySelector('h1, h2, h3');
2355    if (!el || el.dataset.wrapped) return;
2356    var words = el.textContent.split(/\s+/).filter(Boolean);
2357    el.innerHTML = words.map(function (w, i) {
2358      return '<span class="word" style="--i:' + i + '">' + w + '</span>';
2359    }).join(' ');
2360    el.dataset.wrapped = '1';
2361  }
2362
2363  var obs = new IntersectionObserver(function (entries) {
2364    entries.forEach(function (e) {
2365      if (!e.isIntersecting) return;
2366      var el = e.target;
2367      wrapWords(el);
2368      /* Fast-scroll catch-up: if it's already deep in view, skip the cascade */
2369      var r = el.getBoundingClientRect();
2370      if (r.top < window.innerHeight * 0.25) el.classList.add('is-instant');
2371      el.classList.add('is-revealed');
2372      obs.unobserve(el);
2373    });
2374  }, { threshold: 0.5 });
2375
2376  document.querySelectorAll('.reveal-h').forEach(function (el) { obs.observe(el); });
2377})();
2378</script>
2378
2379
2380<!-- Canonical component CSS (buttons, eyebrows, FAQ). Linked at end of body so
2381     it wins the cascade — standard order, same as solutions/pricing/press. -->
2382
2383<!-- ── shared component JS — inlined. Every module self-guards, so the
2384     video-modal and wordmark-shimmer blocks no-op on this page ── -->
2385<script>
2386/* ============================================================================
2387   SHARED COMPONENTS JS — behaviors for standardized components.
2388   Loaded at the end of <body> on each page.
2389   Currently: FAQ open-state pixel animation (the dotted field that fades in
2390   on the right of an open FAQ item) · Hero video card → fullscreen modal.
2391   ============================================================================ */
2392(function () {
2393  var items = document.querySelectorAll('.ms-faq-item');
2394  if (!items.length) return;
2395  var dpr = window.devicePixelRatio || 1;
2396  var canvasObjs = [];
2397
2398  items.forEach(function (item, idx) {
2399    var canvas = document.createElement('canvas');
2400    canvas.className = 'ms-faq-open-canvas';
2401    item.insertBefore(canvas, item.firstChild);
2402    canvasObjs.push({ canvas: canvas, item: item, phase: idx * 1.7 });
2403  });
2404
2405  function resizeAll() {
2406    canvasObjs.forEach(function (obj) {
2407      obj.canvas.width = obj.item.offsetWidth * dpr;
2408      obj.canvas.height = obj.item.offsetHeight * dpr;
2409    });
2410  }
2411  resizeAll();
2412  window.addEventListener('resize', resizeAll);
2413
2414  function draw(t) {
2415    canvasObjs.forEach(function (obj) {
2416      if (!obj.item.classList.contains('is-open')) return;
2417      var canvas = obj.canvas;
2418      var h = obj.item.offsetHeight;
2419      if (canvas.height !== h * dpr) {
2420        canvas.width = obj.item.offsetWidth * dpr;
2421        canvas.height = h * dpr;
2422      }
2423      var ctx = canvas.getContext('2d');
2424      var W = canvas.width, H = canvas.height;
2425      ctx.clearRect(0, 0, W, H);
2426
2427      var sp = 7 * dpr, maxR = 2.2 * dpr;
2428      var cols = Math.ceil(W / sp) + 1, rows = Math.ceil(H / sp) + 1;
2429
2430      for (var r = 0; r < rows; r++) {
2431        for (var c = 0; c < cols; c++) {
2432          var px = c * sp, py = r * sp;
2433          var hFade = Math.max(0, Math.min(1, (px / W - 0.2) / 0.5));
2434          var fade = hFade;
2435          if (fade < 0.02) continue;
2436
2437          var angle1 = t * 0.00012 + obj.phase;
2438          var u1 = (px * Math.cos(angle1) + py * Math.sin(angle1)) / (350 * dpr);
2439          var v1 = (-px * Math.sin(angle1) + py * Math.cos(angle1)) / (220 * dpr);
2440          var angle2 = t * 0.00022 + obj.phase + 1.5;
2441          var u2 = (px * Math.cos(angle2) + py * Math.sin(angle2)) / (180 * dpr);
2442          var v2 = (-px * Math.sin(angle2) + py * Math.cos(angle2)) / (120 * dpr);
2443
2444          var wave = (Math.sin(u1 * Math.PI * 2 + t * 0.00030) * Math.cos(v1 * Math.PI * 1.5))
2445                   * 0.55 + (Math.sin(u2 * Math.PI * 2 + t * 0.00060) * 0.45);
2446          var density = Math.pow(Math.max(0, wave * 0.5 + 0.5), 0.8) * fade;
2447          var rand = (Math.sin(c * 127.1 + r * 311.7 + obj.phase * 50) * 0.5 + 0.5) * 0.9;
2448          if (density > rand) {
2449            var alpha = density * 0.25;
2450            ctx.beginPath();
2451            ctx.arc(px, py, maxR * density, 0, Math.PI * 2);
2452            ctx.fillStyle = 'rgba(180,185,195,' + alpha.toFixed(2) + ')';
2453            ctx.fill();
2454          }
2455        }
2456      }
2457    });
2458    requestAnimationFrame(draw);
2459  }
2460  requestAnimationFrame(draw);
2461})();
2462
2463/* ============================================================================
2464   HERO VIDEO COMPONENT — card click → fullscreen modal (tella embed)
2465   Markup hooks: #hero-video-card, #hero-play-btn, #video-modal,
2466   #video-modal-close, #modal-video[data-tella]. No-ops if absent.
2467   The tella slug comes from #modal-video's data-tella attribute. Clicks on the
2468   bottom strip (.hero-card-strip) are ignored so the CTA/form still works.
2469   ============================================================================ */
2470(function () {
2471  var card = document.getElementById('hero-video-card');
2472  var playBtn = document.getElementById('hero-play-btn');
2473  var modal = document.getElementById('video-modal');
2474  var closeBtn = document.getElementById('video-modal-close');
2475  var modalVideo = document.getElementById('modal-video');
2476  if (!card || !modal) return;
2477  var ytId = modalVideo ? modalVideo.getAttribute('data-youtube') : null;
2478  var tellaId = modalVideo ? modalVideo.getAttribute('data-tella') : null;
2479
2480  card.addEventListener('click', function (e) {
2481    if (e.target.closest('.hero-card-strip')) return; /* let strip CTA/form work */
2482    modal.classList.add('open');
2483    if (modalVideo && ytId) {
2484      modalVideo.innerHTML = '<iframe src="https://www.youtube-nocookie.com/embed/' + ytId +
2485        '?autoplay=1&rel=0&modestbranding=1" allow="autoplay; encrypted-media; fullscreen" allowfullscreen></iframe>';
2486    } else if (modalVideo && tellaId) {
2487      modalVideo.innerHTML = '<iframe src="https://www.tella.tv/video/' + tellaId +
2488        '/embed?b=0&title=0&a=1&loop=0&t=0&muted=0&wt=0&o=0" allow="autoplay; fullscreen" allowtransparency></iframe>';
2489    }
2490    var ifr = modalVideo ? modalVideo.querySelector('iframe') : null;
2491    if (ifr) ifr.addEventListener('load', function () { ifr.classList.add('is-loaded'); });
2492    if (playBtn) {
2493      playBtn.classList.add('clicked');
2494      setTimeout(function () {
2495        playBtn.classList.remove('clicked');
2496        playBtn.classList.add('returning');
2497        setTimeout(function () { playBtn.classList.remove('returning'); }, 400);
2498      }, 550);
2499    }
2500  });
2501
2502  /* Cards marked role="button" tabindex="0" must activate like a native
2503     button: Enter and Space open the modal (Space would otherwise scroll the
2504     page). Guard on e.target so keys typed into strip form fields pass
2505     through untouched. */
2506  card.addEventListener('keydown', function (e) {
2507    if (e.key !== 'Enter' && e.key !== ' ' && e.key !== 'Spacebar') return;
2508    if (e.target !== card) return;
2509    e.preventDefault();
2510    card.click();
2511  });
2512
2513  function closeModal() {
2514    modal.classList.remove('open');
2515    if (modalVideo) modalVideo.innerHTML = '';
2516  }
2517  if (closeBtn) closeBtn.addEventListener('click', closeModal);
2518  modal.addEventListener('click', function (e) { if (e.target === modal) closeModal(); });
2519  document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeModal(); });
2520})();
2521
2522/* ============================================================================
2523   PIXEL-FILL BUTTON HOVER — canonical, byte-for-byte port of the page-local
2524   implementation on solutions/ai-for-real-estate.    Auto-runs on every
2525   .hero-btn-primary / .hero-btn-ghost / .footer-cta-btn on the page, except
2526   buttons inside astro-island / .mkt-nav-shell (Header hydrates those). The
2527   matching .btn-pixel-canvas + .btn-pixelized CSS lives in shared-components.css.
2528   ============================================================================ */
2529(function () {
2530  function attach(btn, color) {
2531    // Header/Footer islands SSR a .hero-btn-primary and hydrate later. Mutating
2532    // that button here races React and throws #418/#423 (BOLT-825).
2533    if (btn.closest && btn.closest('astro-island, .mkt-nav-shell')) return;
2534    if (btn.__pixelized) return;
2535    btn.__pixelized = true;
2536    btn.classList.add('btn-pixelized');
2537    var canvas = document.createElement('canvas');
2538    canvas.className = 'btn-pixel-canvas';
2539    btn.insertBefore(canvas, btn.firstChild);
2540    var ctx = canvas.getContext('2d');
2541    var spacing = 4, dot = 2, cols = 0, rows = 0, noise = [];
2542    function resize() {
2543      var r = btn.getBoundingClientRect();
2544      if (!r.width || !r.height) return;
2545      canvas.width = Math.round(r.width);
2546      canvas.height = Math.round(r.height);
2547      cols = Math.ceil(canvas.width / spacing);
2548      rows = Math.ceil(canvas.height / spacing);
2549      noise = [];
2550      for (var y = 0; y < rows; y++) {
2551        noise[y] = [];
2552        for (var x = 0; x < cols; x++) {
2553          var bias = 1 - (y / Math.max(1, rows - 1));
2554          noise[y][x] = bias * 0.55 + Math.random() * 0.55;
2555        }
2556      }
2557      draw();
2558    }
2559    try { new ResizeObserver(resize).observe(btn); } catch (e) {}
2560    window.addEventListener('resize', resize);
2561    requestAnimationFrame(resize);
2562    var progress = 0, target = 0, raf = null, last = 0;
2563    function tick(t) {
2564      if (!last) last = t;
2565      var dt = Math.min(0.05, (t - last) / 1000); last = t;
2566      progress += (target - progress) * Math.min(1, dt * 8);
2567      if (Math.abs(target - progress) < 0.003) progress = target;
2568      draw();
2569      if (progress !== target) { raf = requestAnimationFrame(tick); }
2570      else { raf = null; last = 0; if (target === 1) flickerLoop(); }
2571    }
2572    function flickerLoop() {
2573      requestAnimationFrame(function step() {
2574        if (target !== 1) return;
2575        draw(true);
2576        setTimeout(function () { requestAnimationFrame(step); }, 120);
2577      });
2578    }
2579    function draw(flicker) {
2580      if (!canvas.width) return;
2581      ctx.clearRect(0, 0, canvas.width, canvas.height);
2582      ctx.fillStyle = color;
2583      var threshold = progress * 1.3;
2584      for (var y = 0; y < rows; y++) {
2585        for (var x = 0; x < cols; x++) {
2586          var n = noise[y][x];
2587          if (n < threshold) {
2588            if (flicker && target === 1 && Math.random() < 0.015) continue;
2589            var edge = threshold - n;
2590            if (edge < 0.08 && Math.random() < 0.45) continue;
2591            ctx.fillRect(x * spacing, y * spacing, dot, dot);
2592          }
2593        }
2594      }
2595    }
2596    btn.addEventListener('mouseenter', function () { target = 1; if (!raf) { last = 0; raf = requestAnimationFrame(tick); } });
2597    btn.addEventListener('mouseleave', function () { target = 0; if (!raf) { last = 0; raf = requestAnimationFrame(tick); } });
2598  }
2599  function init() {
2600    document.querySelectorAll('.hero-btn-primary').forEach(function (b) { attach(b, '#0a5aa8'); });
2601    document.querySelectorAll('.hero-btn-ghost').forEach(function (b) { attach(b, 'rgba(255,255,255,0.18)'); });
2602    document.querySelectorAll('.footer-cta-btn').forEach(function (b) { attach(b, '#0a5aa8'); });
2603  }
2604  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
2605  else init();
2606
2607  /* Public re-init for buttons rendered AFTER load (e.g. careers.html builds
2608     its role list from the Greenhouse API, so those CTAs don't exist at
2609     DOMContentLoaded and would otherwise never get the hover effect).
2610     Idempotent — attach() early-returns on any button it already owns, so
2611     calling this repeatedly is safe. */
2612  window.__scPixelizeButtons = init;
2613})();
2614
2615/* ============================================================================
2616   BOLT WORDMARK SHIMMER — animates light strips across the BOLT wordmark
2617   image at the bottom of marketing/solutions pages. Auto-initializes on any
2618   page that includes a #bolt-shimmer-canvas inside a parent <img>. The image
2619   must be loaded same-origin (or with crossorigin="anonymous") so getImageData
2620   can sample bright vertical strips from the wordmark.
2621   ============================================================================ */
2622(function () {
2623  var canvas = document.getElementById('bolt-shimmer-canvas');
2624  if (!canvas || !canvas.getContext) return;
2625  var wrap = canvas.parentElement;
2626  var img  = wrap && wrap.querySelector('img');
2627  if (!img) return;
2628  var ctx = canvas.getContext('2d');
2629  var W, H, strips = [], t = 0;
2630  var hoverT = 0, isHovered = false, splashT = 0, splashStart = null, splashDuration = 300;
2631
2632  var observer = new IntersectionObserver(function (entries) {
2633    if (entries[0].isIntersecting && !splashStart) { splashStart = Date.now(); observer.disconnect(); }
2634  }, { threshold: 0.1 });
2635  observer.observe(wrap);
2636
2637  function sampleStrips() {
2638    var SW = 400, SH = 200;
2639    var s = document.createElement('canvas');
2640    s.width = SW; s.height = SH;
2641    var sc = s.getContext('2d');
2642    try { sc.drawImage(img, 0, 0, SW, SH); } catch (e) { return; }
2643    var data;
2644    try { data = sc.getImageData(0, 0, SW, SH).data; } catch (e) { return; }
2645    strips = [];
2646    var inStrip = false, stripStart = 0;
2647    for (var x = 0; x < SW; x++) {
2648      var b = 0;
2649      for (var y = 20; y < SH - 20; y++) {
2650        var idx = (y * SW + x) * 4;
2651        b += (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
2652      }
2653      b /= (SH - 40);
2654      if (b > 28 && !inStrip) { inStrip = true; stripStart = x; }
2655      else if (b <= 28 && inStrip) {
2656        inStrip = false;
2657        var cx = (stripStart + x) / 2 / SW;
2658        var w = (x - stripStart) / SW;
2659        strips.push({
2660          cx: cx, w: w,
2661          phase: Math.random() * Math.PI * 2,
2662          freq: 0.5 + Math.random() * 1.2,
2663          yPhase: Math.random() * Math.PI * 2,
2664          yFreq: 0.35 + Math.random() * 0.7,
2665          speedMult: 0.6 + Math.random() * 0.8,
2666        });
2667      }
2668    }
2669  }
2670
2671  function init() {
2672    var r = wrap.getBoundingClientRect();
2673    W = canvas.width = r.width; H = canvas.height = r.height;
2674    t = Math.random() * 80;
2675    sampleStrips();
2676    var isTouch = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
2677    if (isTouch) splashDuration = 1500;
2678    else {
2679      wrap.addEventListener('mouseenter', function () { isHovered = true; });
2680      wrap.addEventListener('mouseleave', function () { isHovered = false; });
2681    }
2682    requestAnimationFrame(tick);
2683  }
2684
2685  function tick() {
2686    var splashRaw = splashStart ? Math.max(0, 1 - (Date.now() - splashStart) / splashDuration) : 0;
2687    splashT = splashRaw * splashRaw * splashRaw;
2688    t += 0.012 + hoverT * 0.008;  /* canonical: matches ai-for-real-estate / bolt-cli.html — enterprise-v2's 0.028 was an outlier */
2689    hoverT += ((isHovered ? 1 : 0) - hoverT) * (isHovered ? 0.07 : 0.025);
2690
2691    ctx.clearRect(0, 0, W, H);
2692    for (var i = 0; i < strips.length; i++) {
2693      var st = strips[i];
2694      var brightness = (Math.sin(t * st.freq * st.speedMult + st.phase) * 0.5 + 0.5);
2695      if (brightness < 0.01) continue;
2696      var x = st.cx * W;
2697      var w = Math.max(st.w * W, 3);
2698      var centerY = (Math.sin(t * st.yFreq * st.speedMult + st.yPhase) * 0.5 + 0.5) * H;
2699      var spread = H * (0.55 + hoverT * 0.25);
2700
2701      var baseA  = brightness * 0.11;
2702      var peakA  = brightness * 0.28;
2703      var boost  = Math.max(hoverT, splashT);
2704      var a  = (baseA + (peakA - baseA) * boost).toFixed(3);
2705      var a2 = (parseFloat(a) * 1.4).toFixed(3);
2706
2707      var grad = ctx.createLinearGradient(0, centerY - spread, 0, centerY + spread);
2708      grad.addColorStop(0,   'rgba(220,225,240,0)');
2709      grad.addColorStop(0.3, 'rgba(220,225,240,' + a + ')');
2710      grad.addColorStop(0.5, 'rgba(255,255,255,' + a2 + ')');
2711      grad.addColorStop(0.7, 'rgba(220,225,240,' + a + ')');
2712      grad.addColorStop(1,   'rgba(220,225,240,0)');
2713      ctx.fillStyle = grad;
2714      ctx.fillRect(x - w / 2, 0, w, H);
2715    }
2716    requestAnimationFrame(tick);
2717  }
2718
2719  if (img.complete && img.naturalWidth) init();
2720  else img.addEventListener('load', init, { once: true });
2721  window.addEventListener('resize', function () {
2722    var r = wrap.getBoundingClientRect();
2723    W = canvas.width = r.width; H = canvas.height = r.height;
2724  });
2725})();
2726
2727/* ============================================================================
2728   SCROLL ANIMATIONS — eyebrow scramble + word reveal
2729   Canonical across every marketing page. Triggers when an element scrolls
2730   into view (fires on entry):
2731   - .eyebrow-scramble → letter-by-letter scramble entrance, re-fires every 6s
2732   - .dsa-reveal       → per-word blur+slide reveal on the h1/h2 inside
2733   ============================================================================ */
2734(function () {
2735  /* Pages that ship their OWN word-reveal + scramble (microsoft, bolt-cli,
2736     platform, pricing — all complete before shared-components.js existed; they
2737     link this file for the FAQ/button components only) set
2738     `window.__pageOwnsScrollAnims = true` before this script loads. Running
2739     both implementations on the same elements is a race that corrupts text —
2740     each captures "final" text at run time, so whichever starts second locks
2741     in the other's mid-animation garbage. One owner per page, never two. */
2742  if (window.__pageOwnsScrollAnims) return;
2743  if (typeof IntersectionObserver === 'undefined') return;
2744  var scrambleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&';
2745
2746  function scrambleEl(el) {
2747    /* Guard 1: never scramble an element that contains child elements. This
2748       animation assigns to textContent, which would delete them — e.g. the
2749       Microsoft logo <svg> inside the "Now available in..." badge. */
2750    if (el.children.length) return;
2751    /* Guard 2: prefer the snapshot taken at bind() time. Reading textContent
2752       here is unsafe on pages that ALSO run their own scramble: if theirs is
2753       mid-animation we'd capture garbled text and lock it in as "final". */
2754    var final = el.dataset.sf || el.textContent.trim();
2755    el.dataset.sf = final;
2756    var chars = final.split('');
2757    var locked = chars.map(function () { return false; });
2758    var cur = chars.map(function (c) { return c === ' ' ? ' ' : scrambleChars[Math.floor(Math.random() * scrambleChars.length)]; });
2759    var start = null, dur = 600;
2760    (function tick(ts) {
2761      if (!start) start = ts;
2762      var p = Math.min((ts - start) / dur, 1);
2763      chars.forEach(function (ch, i) {
2764        if (ch === ' ') { locked[i] = true; cur[i] = ' '; return; }
2765        if (!locked[i] && p > (i / chars.length) * 0.75 + Math.random() * 0.1) { locked[i] = true; cur[i] = ch; }
2766        else if (!locked[i]) cur[i] = scrambleChars[Math.floor(Math.random() * scrambleChars.length)];
2767      });
2768      el.textContent = cur.join('');
2769      if (p < 1 || locked.some(function (l) { return !l; })) requestAnimationFrame(tick);
2770      else el.textContent = final;
2771    })(performance.now());
2772  }
2773
2774  function wrapWords(el) {
2775    /* Guard 3: bail if the words are already wrapped. Several pages
2776       (microsoft, bolt-cli, platform, pricing) ship their own word-reveal that
2777       wraps via DOM nodes without setting data-wrapped. Running this after
2778       theirs would regex over the markup THEY produced — `<span`, `class="word"`
2779       and `style="--i:0;">What` each get wrapped again — and the attribute text
2780       renders as visible copy in the heading. */
2781    if (el.querySelector('.word')) return;
2782    var i = 0;
2783    el.innerHTML = el.innerHTML.replace(/\S+/g, function (w) {
2784      return '<span class="word" style="--i:' + (i++) + '">' + w + '</span>';
2785    });
2786  }
2787
2788  var io = new IntersectionObserver(function (entries) {
2789    entries.forEach(function (entry) {
2790      if (!entry.isIntersecting) return;
2791      var el = entry.target;
2792      if (el.classList.contains('dsa-reveal')) {
2793        el.querySelectorAll('h1, h2').forEach(function (h) {
2794          if (!h.dataset.wrapped) { wrapWords(h); h.dataset.wrapped = '1'; }
2795        });
2796        /* Fast-scroll catch-up: first noticed already DEEP in view (top above
2797           45% of the viewport — momentum scroll blew past the trigger, or the
2798           page loaded mid-scroll). Playing the 0.7s cascade now reads as lag
2799           on text the reader is already reading — .is-instant (see the CSS)
2800           shows the words immediately instead. Subtitles inherit via the
2801           sibling selector, so heading and subtitle can't disagree. */
2802        var vh = window.innerHeight || document.documentElement.clientHeight;
2803        if (entry.boundingClientRect.top < vh * 0.45) el.classList.add('is-instant');
2804        el.classList.add('is-revealed');
2805      } else if (el.classList.contains('eyebrow-scramble')) {
2806        scrambleEl(el);
2807        if (!el.dataset.scrambleLoop) {
2808          el.dataset.scrambleLoop = '1';
2809          setInterval(function () { scrambleEl(el); }, 6000);
2810        }
2811      }
2812      io.unobserve(el);
2813    });
2814  /* Fires as the heading ENTERS view (was threshold 0.5 = half-visible, which
2815     on tall sections meant scrolling well past the heading before it appeared).
2816     A small bottom rootMargin keeps it from firing on the very first pixel. */
2817  }, { threshold: 0 });
2818
2819  /* ── Subtitle block-reveal ───────────────────────────────────────────────
2820     Section subtitles get a whole-element fade/blur-up when their heading's
2821     reveal container gains .is-revealed. Crucially this does NOT observe
2822     scroll itself — it reacts (via MutationObserver) to the class added by
2823     whichever implementation owns the container (shared's .dsa-reveal IO, or
2824     a page-local .reveal-h implementation). Single-owner rule holds: only
2825     this code writes .sc-sub-* classes, and nothing else animates these
2826     elements. Subtitles containing child elements are still fine — this is a
2827     class toggle, not an innerHTML rewrite. */
2828  var SUB_SEL = '.section-sub, .builtin-sub, .detail-sub, .controls-sub, .compliance-sub, .hiw-sub, .agent-desc, .run-callout-sub, .success-sub, .feat-subheadline, .footer-subtitle';   /* only classes that actually sit in/after a reveal container */
2829  /* DOM-based word wrapper (microsoft.html's proven approach): splits text
2830     nodes into .word spans and wraps element children whole. Never touches
2831     innerHTML with a regex, so nested markup cannot be corrupted. The --i
2832     index continues from startIdx so the subtitle's cascade picks up where
2833     the heading's words left off — the same rhythm as microsoft. */
2834  function wrapSubWords(el, startIdx) {
2835    if (el.querySelector('.word')) return startIdx;
2836    var idx = startIdx;
2837    var children = [].slice.call(el.childNodes);
2838    el.textContent = '';
2839    children.forEach(function (node) {
2840      if (node.nodeType === 3) {
2841        node.textContent.split(/(\s+)/).forEach(function (p) {
2842          if (!p.trim().length) { if (p.length) el.appendChild(document.createTextNode(p)); return; }
2843          var s = document.createElement('span'); s.className = 'word'; s.style.setProperty('--i', idx++); s.textContent = p; el.appendChild(s);
2844        });
2845      } else if (node.nodeName === 'BR') { el.appendChild(node); }
2846      else { var w = document.createElement('span'); w.className = 'word'; w.style.setProperty('--i', idx++); w.appendChild(node); el.appendChild(w); }
2847    });
2848    return idx;
2849  }
2850  function bindSubtitleReveals() {
2851    document.querySelectorAll('.dsa-reveal, .reveal-h').forEach(function (c) {
2852      if (c.dataset.scSubBound) return;
2853      c.dataset.scSubBound = '1';
2854      var subs = [].slice.call(c.querySelectorAll(SUB_SEL));
2855      var n = c.nextElementSibling;
2856      while (n && n.matches && n.matches(SUB_SEL)) { subs.push(n); n = n.nextElementSibling; }
2857      if (!subs.length) return;
2858      if (c.classList.contains('is-revealed')) return;   // already fired — leave visible
2859      /* Start the subtitle's --i after the HEADING's word count so the
2860         cascade flows heading → subtitle in one continuous sequence. Count
2861         only the heading — the whole container's text would include the
2862         eyebrow and the subtitle itself, delaying the subtitle's first word
2863         by the full total and leaving a dead pause (caught by review on
2864         bolt-public-pages#250; same code, same bug). */
2865      var heading = c.querySelector('h1, h2');
2866      var idx = heading && heading.textContent.trim() ? heading.textContent.trim().split(/\s+/).length : 0;
2867      subs.forEach(function (s) { s.classList.add('sc-sub-words'); idx = wrapSubWords(s, idx); });
2868      var mo = new MutationObserver(function () {
2869        if (!c.classList.contains('is-revealed')) return;
2870        /* Propagate the fast-scroll instant state onto the subs THEMSELVES:
2871           the CSS sibling-combinator fallback only reaches subs that are
2872           siblings of the container, but subs collected from INSIDE it need
2873           the class directly (structure-independent). */
2874        var inst = c.classList.contains('is-instant');
2875        subs.forEach(function (s) { if (inst) s.classList.add('sc-sub-instant'); s.classList.add('sc-sub-go'); });
2876        mo.disconnect();
2877      });
2878      mo.observe(c, { attributes: true, attributeFilter: ['class'] });
2879    });
2880  }
2881
2882  function bind() {
2883    /* Snapshot each eyebrow's true text BEFORE any animation can run, so a
2884       page-local scramble can never cause us to capture garbled text as final. */
2885    document.querySelectorAll('.eyebrow-scramble').forEach(function (el) {
2886      if (!el.dataset.sf && !el.children.length) el.dataset.sf = el.textContent.trim();
2887    });
2888    document.querySelectorAll('.dsa-reveal, .eyebrow-scramble').forEach(function (el) { io.observe(el); });
2889    bindSubtitleReveals();
2890  }
2891  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bind);
2892  else bind();
2893})();
2894
2895</script>
2895
2896</main> <astro-island uid="Z8xqqE" prefix="r4" component-url="/public-page-assets/astro/Footer.DdfHZrRR.js" component-export="default" renderer-url="/public-page-assets/astro/client.BvbZLJKQ.js" props="{}" ssr client="load" opts="{&quot;name&quot;:&quot;Footer&quot;,&quot;value&quot;:true}" await-children><footer class="site-footer"><div class="site-footer-cols"><div class="site-footer-col"><p class="site-footer-label">Solutions</p><ul class="site-footer-links"><li><a href="https://bolt.new/use-cases/ai-app-builder">AI App Builder</a></li><li><a href="https://bolt.new/use-cases/ai-website-builder">AI Website Builder</a></li><li><a href="https://bolt.new/use-cases/custom-crm-software">SaaS Dev</a></li><li><a href="https://bolt.new/use-cases/ai-prototyping-tools">Prototyping</a></li><li><a href="https://bolt.new/solutions/ai-for-small-business">Small Business</a></li><li><a href="https://bolt.new/enterprise">Enterprise</a></li></ul></div><div class="site-footer-col"><p class="site-footer-label">Resources</p><ul class="site-footer-links"><li><a href="https://bolt.new/blog">Blog</a></li><li><a href="https://discord.com/invite/stackblitz" target="_blank" rel="noopener">Community</a></li><li><a href="https://support.bolt.new" target="_blank" rel="noopener">Help Center</a></li><li><a href="https://bolt.new/platform/security">Security &amp; Trust</a></li><li><a href="https://bolt.new/resources/templates">Templates</a></li></ul></div><div class="site-footer-col"><p class="site-footer-label">More</p><ul class="site-footer-links"><li><a href="https://bolt.new/careers">Careers</a></li><li><a href="https://bolt.new/pricing">Pricing</a></li><li><a href="https://stackblitz.com/terms-of-service" target="_blank" rel="noopener">Terms of Use</a></li><li><a href="https://stackblitz.com/privacy-policy" target="_blank" rel="noopener">Privacy Policy</a></li></ul></div><div class="site-footer-col site-footer-col--connect"><p class="site-footer-label">Connect</p><div class="site-footer-social site-footer-social--col"><a href="https://x.com/boltdotnew" target="_blank" rel="noopener" aria-label="X (Twitter)"><svg viewBox="0 0 14 15" fill="none" class="site-footer-social-icon" aria-hidden="true"><path fill="currentColor" d="M10.62 1.813h1.93L8.332 6.63l4.96 6.556H9.409L6.368 9.212l-3.48 3.976H.956l4.509-5.153L.709 1.812H4.69l2.75 3.635 3.18-3.635Zm-.678 10.22h1.07L4.11 2.907H2.962l6.98 9.126Z"></path></svg></a><a href="https://www.linkedin.com/company/boltdotnew/" target="_blank" rel="noopener" aria-label="LinkedIn"><svg viewBox="0 0 529.6 529.6" class="site-footer-social-icon" aria-hidden="true"><path fill="currentColor" d="M451.2 451.2h-78.5V328.3c0-29.3-.5-67-40.8-67s-47.1 31.9-47.1 64.9v125h-78.5V198.5h75.3V233h1.1c15.4-26.3 43.9-41.9 74.3-40.8 79.5 0 94.2 52.3 94.2 120.4v138.6ZM117.8 164c-25.1 0-45.5-20.4-45.5-45.5S92.7 73 117.8 73s45.5 20.4 45.5 45.5-20.4 45.5-45.5 45.5m39.3 287.2H78.5V198.5h78.6v252.7ZM490.4 0H39.1C17.8-.2.3 16.9 0 38.2v453.2c.3 21.3 17.7 38.4 39.1 38.2h451.3c21.4.3 38.9-16.8 39.2-38.2V38.2C529.3 16.8 511.7-.3 490.4 0"></path></svg></a><a href="https://discord.com/invite/stackblitz" target="_blank" rel="noopener" aria-label="Discord"><svg viewBox="0 0 21 20" fill="none" class="site-footer-social-icon" aria-hidden="true"><path fill="currentColor" d="M16.246 4.442a14.154 14.154 0 0 0-3.559-1.108.083.083 0 0 0-.058.025c-.15.275-.325.633-.442.908-1.325-.2-2.674-.2-4 0a8.355 8.355 0 0 0-.45-.908c-.008-.017-.033-.025-.058-.025a14.26 14.26 0 0 0-3.558 1.108c-.009 0-.017.009-.025.017-2.267 3.392-2.892 6.692-2.584 9.958 0 .017.009.034.025.042 1.5 1.1 2.942 1.767 4.367 2.208.025.009.05 0 .058-.016.334-.459.634-.942.892-1.45.017-.034 0-.067-.033-.075-.475-.184-.925-.4-1.367-.65-.033-.017-.033-.067-.008-.092.091-.067.183-.142.275-.208a.051.051 0 0 1 .058-.009c2.867 1.309 5.958 1.309 8.792 0a.051.051 0 0 1 .058.009c.092.075.183.141.275.216.033.025.033.075-.008.092a8.935 8.935 0 0 1-1.367.65c-.033.008-.042.05-.033.075.266.508.566.992.891 1.45.025.008.05.017.075.008a14.416 14.416 0 0 0 4.375-2.208.046.046 0 0 0 .025-.042c.367-3.775-.608-7.05-2.583-9.958-.008-.008-.017-.017-.033-.017Zm-8.959 7.984c-.858 0-1.575-.792-1.5
289675-1.767 0-.975.7-1.767 1.575-1.767.884 0 1.584.8 1.575 1.767 0 .975-.7 1.767-1.575 1.767Zm5.809 0c-.859 0-1.575-.792-1.575-1.767 0-.975.7-1.767 1.575-1.767.883 0 1.583.8 1.575 1.767 0 .975-.692 1.767-1.575 1.767Z"></path></svg></a></div></div></div></footer><section class="footer-image-section"><div class="bolt-image-wrap"><img src="/public-page-assets/marketing/footer/bolt-bottom.webp" alt="bolt" loading="lazy" style="width:100%;height:100%;object-fit:cover;object-position:top;display:block;opacity:0.2"/><canvas style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:2;mix-blend-mode:screen"></canvas><div style="position:absolute;inset:0;background:linear-gradient(to bottom, #000 0%, transparent 22%, transparent 88%, #000 100%);z-index:3"></div></div></section><!--astro:end--></astro-island> </div>  <astro-island uid="Z1jRwoc" prefix="r5" component-url="/public-page-assets/astro/AuthFrame.Bt7MpRBC.js" component-export="AuthFrame" renderer-url="/public-page-assets/astro/client.BvbZLJKQ.js" props="{&quot;data-astro-cid-sckkx6r4&quot;:[0,true]}" ssr client="load" opts="{&quot;name&quot;:&quot;AuthFrame&quot;,&quot;value&quot;:true}" await-children><dialog class="bolt-auth-frame" aria-label="Sign in to Bolt" inert="" aria-hidden="true"></dialog><!--astro:end--></astro-island> </body></html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.