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 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 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 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&A and board-level reporting, quote-to-cash, deal desk, procurement, and sales compensation, and you'll partner with Product and Growth on pricing & 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. You will be a key partner in building the operating backbone for Bolt's next stage of growth. ð ï¸ How You'll Contribute Close & Reporting: Run monthly/quarterly close, consolidations, and reconciliations; deliver a fast, accurate investor and board reporting pack (ARR/NRR, retention, cash). FP&A Budgeting & Forecasting: Establish quarterly budgets by team; drive rolling forecast and unit economic analysis; monitor plan vs. actual for the full P&L. Revenue Recognition & 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 & 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 & 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 & 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 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 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 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 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 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 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 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 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 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 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 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 & 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 & 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 & 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 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 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 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="{"name":"Header","value":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 & 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 & 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">×</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’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’t a title, it’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’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 & 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 — 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 — 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 — birth, adoption, or surrogacy — 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 — more importantly — 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">“The culture at Bolt is genuinely exceptional. Engineers here aren’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.”</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">“What I love about Bolt is that everyone’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’s rare to feel this looped in and this cared for.”</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">“What sold me on Bolt was the opportunity to help build something special, but what’s kept me here is the people. I’ve watched teammates jump in wherever they’re needed and leadership move fast without losing sight of the people behind the work. It’s rare to find a team this driven and this genuine.”</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">“I’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’re contributing to something great.”</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…</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’t see the right role? New openings are posted here as they go live — it’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–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’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’s the single best way to prepare. It’s free to try, it takes about ten minutes to build something real, and candidates who’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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 — 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="{"name":"Footer","value":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 & 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="{"data-astro-cid-sckkx6r4":[0,true]}" ssr client="load" opts="{"name":"AuthFrame","value":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.