/*
 * LeadX Security — new home page
 * ─────────────────────────────────────────────────────────────────────────────
 *
 * HOW THIS FILE IS MEANT TO BE EDITED
 *
 * Everything a design decides — colour, type size, spacing, radius — is a token
 * in :root below, and nothing further down this file hardcodes one. So matching
 * Figma means editing the :root block and nothing else; the layout underneath
 * already knows what to do with whatever the values turn out to be.
 *
 * Tokens tagged FIGMA carry a value read off the comp. Tokens tagged GUESS were
 * measured from a screenshot and still need confirming.
 *
 * The rules below :root are structure — flex, stacking, responsive behaviour.
 * Those are engineering decisions rather than design ones, and should not need
 * touching to match a comp.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   FONTS
   Product Sans, self-hosted to match LeadXForms. Three weights only — the full
   family is twelve files and 1.1 MB, and nothing on this page sets the other
   nine. font-display:swap so text paints in the fallback immediately rather
   than holding the hero blank while a 90 KB file arrives.
   ═══════════════════════════════════════════════════════════════════════════ */

@font-face {
  font-family: 'Product Sans';
  src: url('../fonts/product-sans/ProductSans-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Product Sans';
  src: url('../fonts/product-sans/ProductSans-Medium.ttf') format('truetype');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Product Sans';
  src: url('../fonts/product-sans/ProductSans-Bold.ttf') format('truetype');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ═══════════════════════════════════════════════════════════════════════════
   TOKENS — this is the block to edit
   ═══════════════════════════════════════════════════════════════════════════ */

:root {

  /* ── Type families ──────────────────────────────────────────────────────
     The comp uses three, the same three LeadXForms does. Which one applies is
     decided per element rather than per level, so they are named for the job
     they do here rather than a hierarchy they do not follow:

       --font         Product Sans — headline and both lede paragraphs, and the
                      page default, which the carried-over nav inherits
       --font-ui      Poppins      — eyebrow and the checked claims
       --font-accent  Albert Sans  — the text CTA and the micro line
       --font-dash    Inter        — the dashboard mock only, which imitates a
                      real product UI rather than this page's own voice

     The fallback stacks matter more than they look: font-display is swap, so
     these are what a visitor actually reads for the first few hundred
     milliseconds, and they are chosen to be geometric rather than whatever the
     platform default happens to be. */
  --font: 'Product Sans', 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  --font-ui: 'Poppins', 'Product Sans', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  --font-accent: 'Albert Sans', 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  --font-dash: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;

  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  /* ── Type scale ─────────────────────────────────────────────────────────
     Every size is the comp's own number, written plainly. Nothing is computed,
     so what is in Figma is what is here and a value can be checked by eye.

     Narrow screens are handled by redeclaring these same tokens inside the two
     media queries at the end of the file — one place per breakpoint, and the
     rules below never have to know a breakpoint exists. */
  /* 70, not Figma's 66. Ahmed's call: the hero is the one heading allowed to be
     bigger than the rest, and it is now one round number against the 50 every
     other section carries. */
  --fs-h1: 70px;
  --lh-h1: 1.1;
  /* FIGMA — 110%, i.e. 72.6px */
  --fw-h1: 400;
  /* FIGMA — Regular, not Bold. Product Sans carries
                                 display sizes on its own weight. */

  /* Every section heading below the hero. One size, one weight, one measure —
     set here so a new section inherits the page's voice instead of choosing
     its own. */
  /*
   * 50, and this token is now the only desktop heading size below the hero.
   *
   * The comment above already said "one size ... so a new section inherits the
   * page's voice instead of choosing its own", but three sections had opted out
   * with tokens of their own and desktop was carrying five heading sizes: 66
   * hero, 56 here, 48 on AI Code Guardian, 48 on Geo Blocking, 42 on the closing
   * CTA. --fs-guardian-title, --fs-geo-title and --fs-cta-title are all 50 now
   * too, so the page reads as one voice at desktop.
   *
   * They are kept as separate tokens rather than deleted because each still
   * steps to its own value on tablet and phone, which the narrow layouts are
   * measured against.
   */
  --fs-section-title: 50px;
  --lh-section-title: 1.1;
  /* FIGMA — 110% */
  --text-heading: #000;
  /* FIGMA — headings are pure black, a shade darker than body ink */

  --fs-eyebrow: 16px;
  /* FIGMA */
  --lh-eyebrow: 1.5;
  /* FIGMA — 150%, i.e. 24px */

  --fs-lede-strong: 20px;
  /* FIGMA — the first, bold paragraph */
  --fs-lede: 18px;
  /* FIGMA — the second */
  --lh-lede: 1.5;
  /* FIGMA — 150% */

  --fs-btn: 16px;
  /* FIGMA */
  --fs-micro: 16px;
  /* FIGMA */
  --fs-trust: 16px;
  /* FIGMA */
  --lh-trust: 1.5;
  /* FIGMA — 150% */

  /* ── Colour ───────────────────────────────────────────────────────────── */
  --text: #151515;
  /* FIGMA — "Black" */
  --text-muted: #646464;
  /* FIGMA — "Grey", both lede paragraphs */
  --text-micro: #808080;
  /* FIGMA — the micro line */

  /* The headline is painted with this and clipped to its glyphs. Solid to the
     halfway point, then falling away to grey — which is why the last words of
     each line read lighter without any of them being marked up that way. */
  --title-gradient: linear-gradient(90deg, #151515 50%, #808080 100%);
  /* FIGMA */

  --green: #008744;
  /* brand green, shared with LeadXForms */
  --green-hover: #00743A;
  --gold: #F5B301;
  /* GUESS — the sparkle */
  --white: #fff;

  /* ── Hero background wash ───────────────────────────────────────────────
     The wash itself is assets/images/hero-gradient.webp, applied in .hero-bg. Only the
     base colour is a token now — the three stop colours that used to build it
     by hand are gone along with the gradients they fed. */
  --wash-base: #FFFFFF;

  /* ── Layout ───────────────────────────────────────────────────────────── */
  /* The comp's own content width. Nothing reads it any more — .container is
     Bootstrap's, to match the brand — but it is the number every Figma frame
     is drawn against, so it is worth being able to look up. */

  /* Height of the fixed header. site.js overwrites this on load and on resize
     with the real measured height, so the hero always clears it — the value
     here is only what applies for the moment before that script runs. */
  --header-h: 88px;

  --hero-pad-top: 96px;
  /* GUESS — nav bottom to eyebrow */
  --hero-pad-bottom: 0px;
  /* the shot carries its own bottom space */

  /* ── Vertical rhythm inside the hero ────────────────────────────────────
     Top to bottom. The first four come off the comp; the last three still need
     checking against it. */
  /* The HERO's eyebrow-to-title gap, and only the hero's — .hero-content is the
     one rule that reads it. The sections use --gap-section-head below. */
  --gap-eyebrow-title: 24px;

  /* One number for every SECTION's heading stack. It was four — 32, 24, 20 and
     16, one per section — and none of them stepped. 20 is what geo, diff,
     action, stat and the plans intro were already drawing, so it is the value
     the page already reads as its own.

     The hero is deliberately not on it: 32 there, at every width, because that
     is the gap that opens the page rather than paces it. */
  --gap-section-head: 20px;
  /* FIGMA */
  --gap-micro-trust: 64px;
  /* GUESS — micro line down to the checked row */
  --gap-trust-shot: 60px;
  /* GUESS */

  /* ── Buttons ──────────────────────────────────────────────────────────── */
  --btn-h: 40px;
  /* FIGMA */
  --btn-pad-x: 16px;
  /* FIGMA */
  --btn-radius: 8px;

  /* Phone only — see the hero structure block. 50px is LeadXForms' own
     .button radius, already on this page in the header. */

  --gap-trust-row: 14px;
  --gap-actions-trust: 28px;
  /* GUESS — the comp gave no radius */

  /* ── Product shot ─────────────────────────────────────────────────────── */
  /* 1128, not the 1110 Figma reports. Figma measures the card and leaves the
     9px frame outside it; this export draws the frame into the artwork, so the
     file is 1110 + 9 + 9 wide. Capping at 1110 scales the whole thing down and
     leaves the card itself 18px short of the comp. */
  --shot-w: 1128px;
  /* FIGMA */
  --shot-radius: 25.085px;
  /* FIGMA — top corners only; the frame runs off the bottom of the section */
  --shot-border: 9px;
  /* FIGMA */

  /* ── Measure ────────────────────────────────────────────────────────────
     Both `none` on purpose: the grid column is the measure. An earlier pass
     capped the headline at 17ch and the lede at 62ch, which sounds reasonable
     and is badly wrong here — 17ch at 64px is roughly 595px against a line the
     comp runs to about 950px, so the headline broke into four lines instead of
     the drawn two, and the first lede wrapped where the design keeps it on one.
     To shorten a line, narrow the column (col-xl-*), not these. */
  --title-measure: none;
  --lede-measure: none;

  /* ── How protection works ─────────────────────────────────────────────── */
  /* The value that was drawing. The token said #180D28 and .engine-card said
     #19022A, and the card won because nothing read the token. */
  --engine-card-bg: #19022A;
  /* GUESS — the dark plate */
  --engine-card-radius: 28px;
  /* GUESS */
  --engine-card-pad: 56px;
  /* GUESS */
  --engine-ink: #FFF;
  /* GUESS — copy on the dark card */
  --engine-ink-faint: #8A809B;
  /* GUESS — the step label */
  --fs-engine-step: 14px;
  /* GUESS */
  --fs-engine-title: 44px;
  /* GUESS — "The Gate" */
  --fs-engine-desc: 22px;
  /* GUESS */
  /*
   * 18, not the 13 that used to sit here. 13 was a GUESS that never drew —
   * .engine-tag hardcoded 18px and never read this token, the same dead-token
   * bug as --fs-stat-lede and --fs-faq-q. The token records what desktop
   * actually renders, so wiring the rule to it moves nothing above 480.
   */
  --fs-engine-tag: 18px;
  /* Padding and gap are tokens for the same reason: the phone step needs them. */
  --engine-tag-pad-y: 8px;
  --engine-tag-pad-x: 14px;
  --engine-tags-gap: 10px;
  --fs-engine-list: 22px;
  /* GUESS */
  --fs-engine-tab: 15px;
  /* 35, not the 32 this said, because 35 is what .engine-panels was drawing
     from a literal. Same reason for --gap-engine-panel below. */
  --gap-engine-tabs: 35px;
  --gap-engine-tab: 8px;
  --engine-panel-fade: .3s;
  --gap-engine-panel: 35px;

  /* The note and the two insets had no tokens at all, so they never stepped:
     22px of note heading and 30px of list indent stayed put on a 280px column. */
  --fs-engine-note-title: 22px;
  --fs-engine-note-body: 18px;
  --engine-list-indent: 32px;
  --engine-note-indent: 18px;
  --engine-tab-pad: 10px 18px;
  --engine-strip-pad: 4px;
  --engine-strip-radius: 12px;
  --engine-strip-bg: rgba(255, 255, 255, .05);
  --engine-seg-radius: 8px;
  --engine-seg-hover: rgba(255, 255, 255, .07);
  --engine-tab-h: 44px;
  /* GUESS */
  --engine-tag-block: #FFE4E4;
  /* FIGMA */
  --engine-tag-filter: #D8F3E4;
  /* GUESS */
  --engine-tag-harden: #D9E8FF;
  /* GUESS */
  /* How far the page scrolls per layer. Five layers, so the section is five
     times this — the single number that decides whether the sequence feels
     unhurried or like the page has stopped responding. 80vh is a little less
     than a screen per step, which reads as brisk. */
  --engine-step-h: 80vh;

  --engine-accent: #17A05A;

  /* ── Feature deep-dives ───────────────────────────────────────────────── */

  /* The three background washes. Width is the comp's frame; the offsets place
     each one down the section and are the numbers to change when Figma says a
     wash sits somewhere else. Everything else about them — size, colour, the
     200px blur — is inside the SVG files. See .deep-blur. */
  --deep-blur-w: 1440px;
  /* FIGMA — the design frame */
  --deep-blur-1-top: 0;
  /* FIGMA */
  --deep-blur-2-top: 50%;
  /* FIGMA */
  --deep-blur-3-bottom: 0;
  /* FIGMA */
  --deep-line: #E0E0E0;
  /* FIGMA — the rules and the diamond outlines */

  /* The rules. One flat colour for their whole length — they hold the same
     weight from edge to edge in the comp and do not fade out toward it. */
  --deep-rule: #E5E5E5;

  /* The rule as the comp draws it: over the COPY's half only, solid against the
     middle where the marker sits and dissolving as it reaches the section's
     outer edge. The picture's half of the line is open — it is why the artwork
     never has a rule above it or beside it.

     Two of them, because which half that is depends on which side the row keeps
     its copy: -l for rows 01, 03 and 05, -r for the mirrored 02, 04 and 06. The
     two stops that share a position are a hard cut, not a ramp — the line stops
     dead at the middle rather than bleeding past the marker.

     #E5E5E500 rather than transparent, and the difference is not cosmetic —
     transparent is rgba(0,0,0,0), so an engine interpolating without
     premultiplied alpha greys the fade out. Same RGB at zero alpha cannot. It is
     --deep-rule's own value; a hex token cannot carry an alpha suffix, which is
     the only reason it is written out rather than derived. */
  --deep-rule-fade-l:
    linear-gradient(to right,
      #E5E5E500 0%,
      var(--deep-rule) 25%,
      var(--deep-rule) 50%,
      #E5E5E500 50%);

  --deep-rule-fade-r:
    linear-gradient(to right,
      #E5E5E500 50%,
      var(--deep-rule) 50%,
      var(--deep-rule) 75%,
      #E5E5E500 100%);

  /* Rows 04 and 06: the line runs the whole width, but only ONE of its two ends
     has something to land on.

     Those two are closed on the right, where the copy is — so that end runs
     square into the edge and stops. The left end is the picture's, and the
     picture has no edge on any row: the line runs out into the section's
     measure there, so it dissolves, exactly as every other rule does at an open
     end. Square at both ends was the mistake — it made the left end look cut
     off rather than finished. */
  --deep-rule-closed-r:
    linear-gradient(to right,
      #E5E5E500 0%,
      var(--deep-rule) 25%,
      var(--deep-rule) 100%);
  /* FIGMA */

  /* Figma draws the copy cell at a flat 500. Here it is a floor, not a fixed
     height — see .deep-copy for why the difference matters. */
  --deep-cell-min-h: 500px;
  /* FIGMA */

  /* A pale wash across the copy cell, fading out toward the rule in the
     middle of the row. Turned around on mirrored rows. */

  /* site.js overwrites this the first time the bar sticks; the fallback only
     covers the frames before any scroll has happened. */
  --header-h-sticky: 74px;

  --gap-deep-copy: 32px;
  /* FIGMA — title to the What/How/Why list */
  /* The mark where the rules cross. It is used as a mask rather than an image,
     so the shape lives here once and takes its colour from --deep-rule with the
     lines it sits on — change that one value and all of them follow. */
  --deep-star: 20px;
  /* FIGMA */
  /* The tick on the AI Code Guardian claims, as the comp draws it — a brush
     stroke, not a geometric check. Inlined rather than fetched: 3KB is less than
     the request would cost, and it is used once.

     A mask, not a background image, for the same reason --deep-star-shape is
     one: a mask takes only the shape and lets background-color supply the
     colour, so the tick reads from --green like everything else green on the
     page. As an image its #008744 would be baked into the URI and would quietly
     stop matching the moment that token changed. */
  --guardian-tick: url("data:image/svg+xml,%3Csvg width=%2211%22 height=%2212%22 viewBox=%220 0 11 12%22 fill=%22none%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cpath d=%22M2.37011 11.5501C2.37011 11.4201 2.34511 11.3551 2.29511 11.3551L1.95011 11.5201C1.95011 11.4501 1.91011 11.4001 1.83011 11.3701L1.71011 11.3551C1.63011 11.3551 1.53011 11.3901 1.41011 11.4601C1.39011 11.4101 1.36511 11.3601 1.33511 11.3101C1.30511 11.2601 1.28011 11.2151 1.26011 11.1751C1.13011 10.9251 1.00011 10.6501 0.870107 10.3501C0.750107 10.0401 0.635107 9.74514 0.525107 9.46514C0.425107 9.18514 0.345107 8.96514 0.285107 8.80514C0.245107 8.67514 0.200107 8.48014 0.150107 8.22014C0.100107 7.96014 0.0501075 7.63014 0.000107463 7.23014C0.110107 7.30014 0.195107 7.33514 0.255107 7.33514C0.325107 7.33514 0.390107 7.23014 0.450107 7.02014C0.480107 7.06014 0.535108 7.08014 0.615108 7.08014C0.675108 7.08014 0.720107 7.06014 0.750107 7.02014L0.990108 6.66014L1.26011 6.75014H1.27511C1.29511 6.75014 1.31511 6.74014 1.33511 6.72014C1.35511 6.70014 1.38511 6.68014 1.42511 6.66014C1.50511 6.61014 1.56511 6.58514 1.60511 6.58514L1.65011 6.60014C1.90011 6.72014 2.06011 6.94014 2.13011 7.26014C2.31011 8.02014 2.49011 8.40014 2.67011 8.40014C2.85011 8.40014 3.06011 8.21014 3.30011 7.83014C3.42011 7.64014 3.54011 7.42014 3.66011 7.17014C3.79011 6.92014 3.92011 6.64014 4.05011 6.33014C4.07011 6.45014 4.09011 6.51014 4.11011 6.51014C4.16011 6.51014 4.24511 6.38514 4.36511 6.13514C4.49511 5.88514 4.70011 5.54014 4.98011 5.10014C5.14011 4.83014 5.34011 4.52514 5.58011 4.18514C5.83011 3.84514 6.09511 3.49514 6.37511 3.13514C6.65511 2.77514 6.92511 2.43514 7.18511 2.11514C7.45511 1.79514 7.69511 1.52014 7.90511 1.29014C8.11511 1.06014 8.27011 0.910136 8.37011 0.840136C8.75011 0.580136 9.05011 0.330136 9.27011 0.0901363C9.26011 0.160136 9.24511 0.225136 9.22511 0.285136C9.21511 0.335136 9.21011 0.370136 9.21011 0.390136C9.21011 0.430136 9.23011 0.450136 9.27011 0.450136L9.69011 0.240136V0.300136C9.69011 0.380136 9.71011 0.420137 9.75011 0.420137C9.78011 0.420137 9.84011 0.375136 9.93011 0.285136C10.0201 0.195136 10.0701 0.130136 10.0801 0.0901363L10.0501 0.300136L10.5601 0.000136554L10.4401 0.270137C10.6001 0.160136 10.7151 0.105136 10.7851 0.105136C10.8251 0.105136 10.8551 0.130136 10.8751 0.180136C10.8951 0.220136 10.9051 0.260136 10.9051 0.300136C10.9051 0.360136 10.8801 0.430136 10.8301 0.510136C10.7801 0.590136 10.7151 0.685136 10.6351 0.795136C10.5751 0.875136 10.4751 0.995136 10.3351 1.15514C10.2051 1.30514 10.0051 1.53014 9.73511 1.83014C9.46511 2.12014 9.10511 2.52514 8.65511 3.04514C8.53511 3.17514 8.35011 3.40514 8.10011 3.73514C7.85011 4.05514 7.56511 4.43014 7.24511 4.86014C6.93511 5.28014 6.62511 5.70514 6.31511 6.13514C6.00511 6.56514 5.73011 6.95514 5.49011 7.30514C5.25011 7.64514 5.08011 7.90014 4.98011 8.07014L4.05011 9.64514C3.85011 9.98514 3.68511 10.2651 3.55511 10.4851C3.42511 10.6951 3.32511 10.8401 3.25511 10.9201C3.10511 11.1001 2.94011 11.2601 2.76011 11.4001L2.62511 11.3251L2.50511 11.4001L2.37011 11.5501Z%22 fill=%22%23000%22/%3E%3C/svg%3E");

  --deep-star-shape: url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22 viewBox=%220 0 20 20%22 fill=%22none%22%3E%3Cpath d=%22M9.47917 20C9.28819 18.8715 8.78472 17.6562 7.96875 16.3542C7.15278 15.0347 5.98958 13.8108 4.47917 12.6823C2.98611 11.5538 1.49306 10.8333 0 10.5208V9.42708C1.47569 9.07986 2.89062 8.4375 4.24479 7.5C5.61632 6.54514 6.76215 5.39931 7.68229 4.0625C8.61979 2.69097 9.21875 1.3368 9.47917 0H10.5729C10.7292 0.868055 11.0417 1.76215 11.5104 2.68229C11.9792 3.58507 12.5781 4.45312 13.3073 5.28646C14.0538 6.10243 14.8872 6.84028 15.8073 7.5C17.1788 8.47222 18.5764 9.11458 20 9.42708V10.5208C19.0451 10.7118 18.0556 11.1024 17.0312 11.6927C16.0243 12.283 15.0868 12.9861 14.2187 13.8021C13.3507 14.6007 12.6389 15.4427 12.0833 16.3281C11.2674 17.6302 10.7639 18.8542 10.5729 20H9.47917Z%22 fill=%22%23000%22/%3E%3C/svg%3E");
  --deep-cell-pad: 40px;
  /* GUESS — inside each half of a row */
  /* 18 to match every other section lede at desktop; was a guessed 16. It has
     no tablet step, so the phone step added in the 575 block keeps narrow
     screens reading exactly as they did. */
  /* 22, not the 18 that used to sit here. 18 was never drawn: .deep-lede
     hardcoded 22px and never read this token — the same dead-token bug as
     --fs-faq-q and --fs-faq-a. The token now records what desktop actually
     renders, so wiring the rule to it changes nothing above 991. */
  --fs-deep-lede: 22px;
  /* FIGMA. Was 26 here while the rule hardcoded 38, so 26 never drew. */
  --fs-deep-title: 38px;
  /* GUESS */

  /* ── Stats ────────────────────────────────────────────────────────────── */
  /* The figure in the headline takes its own gradient — strong blue into
     near-white, left to right, so "43" carries the weight and the percent sign
     trails off. Match this to the blue in stat-card.svg (its #0066DB) if the artwork is ever
     re-exported. */
  --stat-figure-gradient: linear-gradient(90deg, #0066DB 0%, #D6E9FF 100%);
  /* FIGMA */
  --fs-stat-figure: 100px;
  /* FIGMA */
  /*
   * 30 and 22, which is what the rules were hardcoding while these tokens sat
   * dead — see the note on .stat-lede. The old 20/18 were written to "match
   * --fs-lede-strong/--fs-lede at desktop", but since nothing read them the
   * section never actually matched anything; it drew 30/22 at every width.
   * These record the truth, so desktop is unchanged and the mobile step below
   * 991 is the only thing that moves.
   */
  --fs-stat-lede-strong: 30px;
  --fs-stat-lede: 22px;
  /* normal is what both rules had. One token so the phone step can set both. */
  --lh-stat-lede: normal;
  /* 150% is what .deep-lede and .action-lede both hardcoded. */
  --lh-lede: 150%;
  --stat-card-w: 295px;
  /* GUESS — the card is narrow; the column would otherwise stretch it */
  --stat-card-radius: 12px;
  /* GUESS */
  --gap-stat-copy: var(--gap-section-head);
  /* GUESS — between the title and the two paragraphs */

  /* Top and bottom of every section below the hero. */
  --section-pad-y: 50px;

  /* ── PLANS ──────────────────────────────────────────────────────────────
     Read off the comp. Anything marked GUESS was measured from the image
     rather than given as a number, so one value corrects it. */
  /* The wash behind the head of the section. Width is the comp's frame, not
     the size of the colour inside it. The offset is what lines the blob up with
     the heading - see .plans-wash. */
  /* Superseded - the wash spans the viewport now, so there is no frame width
     left to hold it at. See .plans-wash for why this one cannot be centred at
     1440 the way the other two washes are.

  /* The file's own height. Held fixed at every width so --plans-wash-top below
     means the same thing on a laptop and on a desktop. */
  --plans-wash-h: 1376px;
  /* FIGMA - the design frame */
  /*
   * Negative on purpose. The file is 1376 tall and its colour sits in the lower
   * middle of that canvas, around y 500-980 once the blur filters are counted -
   * so at top: 0 the blob landed behind the CARDS and the heading had nothing
   * behind it but the fade. Pulling it up 450 puts the dense part across the
   * eyebrow and the heading and lets it fall away through the cards, which is
   * the order the comp reads in.
   *
   * The part that travels above the section is cropped by overflow: clip, and
   * that crop is wanted - the comp shows the wash cut flat at its top edge too.
   */
  --plans-wash-top: -450px;
  /* GUESS - tuned against the comp at 1440 */

  --gap-plans-head: var(--gap-section-head);
  --gap-plans-grid: 64px;
  --plans-title-measure: 950px;
  --fs-plans-title: 50px;
  --lh-plans-title: 120%;

  --gap-plans-card: 24px;

  /* The drawn card width. .plan caps itself here and the column that holds it
     is capped to match, so the 24px gap above is the only space between two
     cards - nothing is left over inside a column to widen it. */
  --plan-w: 360px;
  --plan-pad: 32px;
  --plan-pad-featured: 44px;
  /* the featured card's extra height */
  --plan-line: #E8E8EE;
  /* GUESS */
  --plan-line-featured: #D9D9E3;
  /* GUESS */
  --plan-shadow: 0 18px 40px rgba(20, 20, 40, .08);
  /* GUESS */

  --plan-badge-pad: 6px 14px;
  --fs-plan-badge: 11px;
  --fs-plan-name: 26px;

  /* ── The card's price ───────────────────────────────────────────────────
     40 to match --fs-plans-price on the annual band below the grid, so the two
     figures in this section are one size rather than two. The symbol and the
     period are deliberately NOT the same size as the figure: leadxforms.com
     renders A$ and 20 as separate elements for that reason, and this follows
     it so the two sites quote a price the same way. */
  --fs-plan-price: 40px;
  --fs-plan-price-symbol: 22px;
  --fs-plan-price-period: 14px;
  /* Tight, because the symbol belongs to the figure - the period is pushed off
     by its own margin instead. */
  --gap-plan-price: 2px;
  --fs-plan-desc: 15px;
  --fs-plan-count: 16px;
  --fs-plan-feature: 15px;

  --gap-plan-count: 20px;
  --gap-plan-feature: 12px;

  --gap-plans-yearly-top: 48px;
  --gap-plans-yearly: 32px;
  --plans-yearly-pad: 48px;
  --plans-yearly-radius: 24px;
  --plans-yearly-bg: #17211C;
  /* GUESS */
  --plans-yearly-ink: #B9C2BC;
  /* GUESS */
  --plans-yearly-copy-min: 420px;
  --plans-yearly-measure: 520px;
  --gap-plans-yearly-badge: 20px;
  --gap-plans-yearly-title: 16px;
  --fs-plans-yearly-title: 28px;
  --fs-plans-yearly-body: 14px;

  --gap-plans-price: 10px;
  --fs-plans-price: 40px;
  --fs-plans-price-unit: 14px;
  --fs-plans-price-note: 13px;

  --gap-plans-foot: 40px;
  --fs-plans-foot: 15px;

  /* GUESS */

  /* ── Why LeadX Security ───────────────────────────────────────────────── */

  /* The band's tint, and how far the section stands off it. There is no panel
     width or radius any more — the colour is on the section and runs to the
     window's edges, so there is no box to size or corner to round. */
  /* FIGMA. Was 72 here while the rule hardcoded 80, so 72 never drew. */
  --diff-panel-pad-y: 80px;
  /* GUESS */

  /* 50 to match --fs-section-title at desktop; was 56. */
  --fs-diff-title: 50px;
  /* FIGMA */
  --lh-diff-title: 110%;
  /* FIGMA — 62px line spacing at 56px */

  /* Holds the break after "Dashboards." — the comp puts the second sentence on
     its own line, and this is the width that does it. */
  --diff-title-measure: 880px;
  /* GUESS — measured off the comp */

  --gap-diff-head: var(--gap-section-head);
  /* GUESS — eyebrow to headline */
  --gap-diff-cards: 68px;
  /* GUESS — headline to the row */

  --gap-diff-card: 26px;
  /* FIGMA — between one card and the next */
  --diff-card-pad: 32px;
  /* FIGMA — all four sides */

  --diff-icon-size: 70px;
  /* FIGMA — the export's own width and height */
  /* The card's two internal gaps are the same measurement, which is what makes
     the tile, the heading and the passage read as one stack rather than as a
     picture with a caption under it. */

  /* ── FAQ and closing CTA ──────────────────────────────────────────────── */

  /* 50 to match --fs-section-title at desktop; was 56. */
  --fs-faq-title: 50px;
  /* FIGMA — 62px line spacing */
  --lh-faq-title: 110%;
  /* FIGMA */
  /* FIGMA. Steps to 64 below lg so this section matches every other one —
     --section-pad-y drops to 64 there and this had no step at all, leaving the
     FAQ 16px deeper than its neighbours at every phone width. */
  --faq-pad-y: 50px;
  /* FIGMA — the section's own top and bottom */
  /* FIGMA — between line 2's 510 and the 560 that would pull "Owners" up */
  /*
     Figma's 75 is measured from the headline's ink, because a Figma text frame
     hugs its glyphs. A CSS margin starts at the LINE BOX, which at 56px/110%
     sits 2.8px below that ink — so a flat 75 here renders as 78 on screen.
     Taking the half-leading off makes the white band the 75 the comp shows.
  */
  --gap-faq-list: 72px;
  /* FIGMA — 75 measured, less the headline's 2.8px of half-leading */

  --faq-list-w: 900px;
  /* FIGMA — measured off the dividers */
  /*
   * FIGMA gives 16px of row padding and 18px between items, and those two
   * numbers together put every question OFF CENTRE between its two rules.
   *
   * The rule is drawn on the item's BOTTOM, so the 18px that separates one item
   * from the next falls entirely ABOVE the following question and nothing
   * balances it below. Measured from the pixels on the closed row "What happens
   * if the firewall blocks a legitimate visitor?": 38.5px of space above the
   * text, 15.5px below it, inside a 73px band. The chevron, a flex item that
   * align-items:center actually centres, sat correctly — so the row read as
   * visibly lopsided, question against chevron.
   *
   * The 18 moves into the padding instead of sitting between items, split
   * evenly: 16 + 18/2 = 25. The band is the same height it always was —
   * 18+16+22+16+1 and 0+25+22+25+1 are both 73 — so nothing about the page's
   * rhythm moves; the space is just on both sides of the question now instead
   * of stacked above it.
   *
   * The padding stays on the <summary> rather than moving back onto the item,
   * for the tap-target reason recorded on .faq-q.
   */
  --faq-row-pad: 25px;
  /* FIGMA — question to answer, inside an open item */
  --gap-faq-qa: 8px;
  /* Was 18px: folded into --faq-row-pad above so it lands on both sides of the
     question rather than only above it. */
  --gap-faq-item: 0px;

  /* Derived, not read off a panel: 1.6 is what makes Figma's two numbers add up
     to the 82px row pitch the comp actually measures — 18 between items, 16 of
     padding, a 32px line box, 16 again. At 1.4 the row lands 4px short. */
  --lh-faq-q: 160%;
  /* FIGMA — by arithmetic */
  --fs-faq-q: 20px;
  /* FIGMA — 20px ink band, and 646px across for the longest question */
  /* FIGMA. Was 16 here while the rule hardcoded 18, so 16 never drew. */
  --fs-faq-a: 18px;
  /* FIGMA */
  /* FIGMA. Same story as --fs-faq-a above. */
  --lh-faq-a: 150%;
  /* FIGMA — 27px line spacing */
  --faq-answer-measure: 890px;
  /* GUESS */

  /*
   * One constant page gutter below 992 — see the fluid-container block.
   *
   * 10, not 24. The Engine section was carrying its own 10px while every other
   * band sat at 24, and a page whose margin changes from section to section
   * reads as unfinished however carefully each section is built. This is the
   * number the whole page shares now, and the Engine's special case is gone
   * (see THE ENGINE, UNPINNED).
   *
   * One value to change if 10 turns out to be too tight for body copy.
   */
  --page-gutter: 15px;

  --gap-cta-card: 110px;
  /* FIGMA — last divider to the green card */
  /* The section, not the card: the comp gives the card 80 of its own. Steps to
     64 below lg so this band matches every other section — it had no step at
     all and stayed 16px deeper than its neighbours on every phone. */
  --cta-pad-y: 50px;
  /* GUESS */

  /*
   * The card is narrower than the container: it is inset while the wordmark
   * under it runs the full width, and that contrast is what makes it read as a
   * card. 1160 was my estimate off the comp; 1122 is the number from Figma.
   */
  --cta-card-w: 1200px;
  /* FIGMA */

  --gap-cta-wordmark: 60px;
  /* GUESS */

  /*
   * assets/wordmark.svg is 161.36 x 28.95. The box is deliberately SHORTER than
   * that — 22 of those 28.95 units, about three quarters — because the comp cuts
   * the glyphs off at the bottom of the section. Change the 22 to change how
   * much of the name shows; 28.95 would show all of it.
   */
  --cta-wordmark-ratio: 161.36 / 22;
  --cta-wordmark-ink: #EDEFF3;
  /* GUESS */

  --gap-cta-card-rows: 50px;
  /* FIGMA */
  --cta-radius: 32px;
  /* FIGMA — measured off the corner */
  --cta-card-pad-y: 80px;
  /* FIGMA — 82 above the headline, 80 below the button */
  --cta-card-pad-x: 0px;
  /* FIGMA — see .cta-card */
  /* GUESS */
  /* FIGMA. Was 40 here while the rule hardcoded 42, so 40 never drew. */
  /* 50 to match --fs-section-title at desktop; was 42. The tablet and phone
     steps below are untouched. */
  --fs-cta-title: 50px;
  /* FIGMA. Same story as --fs-cta-title above. */
  --lh-cta-title: 110%;
  /* FIGMA */
  /* GUESS — holds the break after "before" */
  /* FIGMA. Was 16 here while the rule hardcoded 18. */
  --fs-cta-lede: 18px;
  /* FIGMA */
  --cta-btn-bg: #151515;
  /* FIGMA */
  --cta-btn-h: 40px;
  /* FIGMA */
  --cta-btn-pad-x: 20px;
  /* GUESS — 158px wide at this label */
  --cta-btn-radius: 8px;
  /* GUESS */
  --fs-cta-btn: 14px;
  /* GUESS */

  /* ── Header and footer ────────────────────────────────────────────────── */

  /* Every value here is LeadXForms', copied so the two products wear the same
     chrome. They are tokens so this file can read them, not so they can be
     tuned — changing one breaks the match, which is the point of them. */
  --hf-ink: #151823;
  /* LEADXFORMS — --lxf-dark */
  --hf-ink-hover: #44464F;
  /* LEADXFORMS */
  --hf-green-hover: #339F69;
  /* LEADXFORMS */
  --hf-body: #404040;
  /* LEADXFORMS — footer copy and links */
  --hf-line: #E5E5E5;
  /* LEADXFORMS — the footer's top rule */

  --hf-bar-pad: 20px;
  /* LEADXFORMS */
  --hf-bar-pad-sticky: 12px;
  /* LEADXFORMS */
  --hf-logo-w: 200px;
  /* LEADXFORMS */
  --hf-logo-w-sticky: 180px;
  /* LEADXFORMS */
  --hf-footer-logo-w: 224px;
  /* LEADXFORMS */
  /*
   * 38, not LeadXForms' 25. That 25 was set for a bar of six items whose labels
   * were single words — Home, Features, Templates, Integrations, Resources,
   * Pricing. This bar carries "How It Works" and "AI Guardian", two-and
   * three-word labels, and at 25px the gaps between items read as narrower than
   * the spaces inside them, so the bar looked like one long string rather than
   * six things. 38 puts the gap comfortably wider than a word space at
   * --fs-hf-menu and the items separate again.
   */
  --hf-menu-gap: 38px;
  --hf-btn-h: 50px;
  /* The button component's own cost, tokenised so the phone block can cut it.
     --hf-btn-icon-pad-r reserves the arrow disc: 6px inset + the disc + clearance. */
  --hf-btn-pad-y: 6px;
  --hf-btn-pad-x: 24px;
  --hf-btn-disc-w: 50px;
  --hf-btn-icon-pad-r: 65px;
  /* LEADXFORMS */

  --fs-hf-menu: 17px;
  /* LEADXFORMS */
  /* LeadXForms' .button sets no font-size at all — it inherits body's 18px.
     Stated here instead of relying on a body size this page does not set: at
     the browser default of 16px the pills came out about 30px narrower each
     and the labels visibly smaller than the brand's. */
  --fs-hf-btn: 18px;
  /* LEADXFORMS — inherited from body there */
  --fs-hf-heading: 25px;
  /* LEADXFORMS — footer column headings */
  --fs-hf-body: 16px;
  /* LEADXFORMS */
  --fs-hf-small: 14px;
  /* LEADXFORMS — the copyright line */

  /* ── Product in action ────────────────────────────────────────────────── */

  /* The wash behind this section. Width is the comp's frame, not the size of
     the colour inside it — see .action-wash for why those are different
     numbers. The offset places it down the section. */
  --action-wash-w: 1440px;
  /* FIGMA — the design frame */
  --action-wash-top: 0;
  /* GUESS */

  /* 22 for the same reason as --fs-deep-lede above: .action-lede hardcoded it
     and never read this token, so 18 never drew. Desktop is unchanged by
     wiring it. */
  --fs-action-lede: 22px;
  /* GUESS */
  --gap-action-head: var(--gap-section-head);
  /* GUESS */

  /* The dashboard. 1200 in the comp, which is neither the page container nor
     the hero's shot width — it has its own. */
  --dash-w: 1200px;
  /* GUESS — measured off the comp */
  --dash-radius: 14px;
  /* GUESS */
  --gap-dash-tabs: 32px;
  /* FIGMA */
  --dash-pad: 24px;
  /* GUESS — card edge to its content */
  --dash-border: #EAEBEF;
  /* FIGMA — sampled from the design export */
  --dash-bg: #FFFFFF;
  /* GUESS */

  /* The tab strip sits on its own tint so the strip reads as chrome and the
     panel below it as content. */
  --fs-dash-tab: 15px;
  /* GUESS */
  --dash-tab-ink-active: #111827;
  /* FIGMA */

  /* The open tab's underline is the brand green, not the colour of its own
     text — established by hue: the design export reads #72B797 there, which is
     --green at about 55% over the strip, and both sit on hue 150. */
  --dash-tab-underline: var(--green);
  /* FIGMA */

  --fs-dash-title: 20px;
  /* GUESS — "Site Overview" */
  --fs-dash-sub: 14px;
  /* GUESS */
  --fs-dash-row: 15px;
  /* GUESS */
  --dash-time-ink: #9CA3AF;
  /* GUESS — "live", "2 min ago" */
  --dash-row-h: 53px;
  /* GUESS */

  /* The status pill, and the one red the rows ever use. */
  --dash-bad-ink: #D03A3A;
  /* GUESS — Blocked, Rate-limited, IP banned */
  --dash-neutral-ink: #151515;
  /* GUESS — the outcomes that are neither good news nor bad: Reviewed, and
     Sent to client */

  /* ── Live threat intelligence ─────────────────────────────────────────── */

  /* Full-bleed dark, so like the Guardian card this block declares its own
     colours rather than borrowing from the light palette above. */
  --geo-bg: #050A14;
  /* FIGMA — sampled from the design export */
  --geo-ink: #FFFFFF;
  /* GUESS — headline, eyebrow, labels */
  --geo-rule: #2C3B59;
  /* GUESS — the two rules between those columns */

  --fs-geo-eyebrow: 14px;
  /* GUESS */
  /* 50 to match --fs-section-title at desktop; was a guessed 48. The tablet
     and phone steps below are untouched. */
  --fs-geo-title: 50px;
  --lh-geo-title: 124%;
  /* GUESS */
  --fs-geo-label: 16px;
  /* GUESS — "GEO BLOCKING" */
  /* The trio under the map. Heading and paragraph are the same size in the
     comp and differ only in weight, which is why one token serves both. */
  --fs-geo-def: 18px;
  /* FIGMA */
  --lh-geo-heading: 150%;
  /* FIGMA */
  --lh-geo-paragraph: 26px;
  /* FIGMA */
  --geo-def-ink: #FFF;
  /* FIGMA — both lines, not the softer grey used elsewhere */

  /* The headline breaks after the full stop in the comp; this holds it. */
  --geo-title-measure: 950px;
  /* GUESS — measured off the comp */

  /* Reserves the map's space before the file arrives, so the three columns
     below it do not jump up the page while it loads. */
  /*
   * Must match the SVG viewBox exactly, or the browser letterboxes the map
   * inside its own box and the two disagree about where the middle is.
   *
   * 1160 x 520 was the frame the paths were exported in, not the paths'
   * own extent. Measured with getBBox(): the land stops at x=1089.4, so 70
   * units — 79px at a 1440 viewport — were empty on the RIGHT and nothing was
   * empty on the left. The map was pinned to the left edge of a correctly
   * centred box. Height 520 also cut 36px off the tip of Chile (land runs to
   * y=552.1) and 2px off Greenland (y=-1.6).
   *
   * Cropping the box down to that extent fixed the centring but made the map
   * noticeably taller and squarer (1.96) than the comp, which draws a wide, flat
   * map. So the 1160 x 520 frame stays — it is the comp's proportion — and the
   * viewBox is SHIFTED instead: x=-35.3 puts the 70 spare units half on each
   * side, so the land sits centred with equal breathing room rather than jammed
   * against the left edge.
   *
   * The tip of Chile is still cropped by that frame, as it is in the comp. Change
   * this and the viewBox together, and re-run the pin conversion: .geo-pin
   * --x/--y are percentages of this same box.
   */
  --geo-map-ratio: 1160 / 520;

  /* The heading-to-legend and map-to-passages step. */
  --gap-geo-block: 64px;

  /* The map's rail, as eleven of twelve columns.

     Measured off the comp: the map is 1183 of a 1296 content box at the 1440
     frame, or 91.3% — and 11/12 is 91.67%, five pixels apart at that width.
     Bootstrap's fraction is the one used, so the map lands on the same grid
     the heading (ten) and the What/How/Why trio (ten) are already on rather
     than on a measurement of its own that agrees with them by luck. */
  --geo-map-rail: 91.666667%;
  /* the stand-in crop's own ratio — correct this to the vector's when it lands */

  /* The map's own palette. Every country reads one of these three, chosen by a
     class on its path — so the blocked list lives in the markup, not in a file
     somebody has to re-export. */
  --geo-country: #111B2E;
  /* FIGMA — the countries that are neither */
  --geo-customer: #204B36;
  /* FIGMA */
  --geo-blocked: #572F2F;
  /* FIGMA */

  /* Hover. Each state lightens into its own colour rather than every country
     turning the same highlight — a blocked country under the cursor is still
     a blocked country, and the map would otherwise forget that for as long as
     you are pointing at it. */
  --geo-country-hover: #1B2A45;
  /* GUESS — a step up from --geo-country */
  --geo-customer-hover: #2D6A4C;
  /* GUESS — a step up from --geo-customer */
  --geo-blocked-hover: #7A4242;
  /* GUESS — a step up from --geo-blocked */

  --geo-pulse-dur: 2.4s;
  /* GUESS — one full ring, from the dot to gone */

  --geo-pin-size: 10px;
  /* GUESS */
  --geo-pin-ok: #00DAA5;
  /* FIGMA */
  --geo-pin-bad: #FF645A;
  /* FIGMA */
  --geo-pin-count: #A23DE8;
  /* FIGMA */
  --fs-geo-pin: 13px;
  /* GUESS */

  /* ── AI Code Guardian ─────────────────────────────────────────────────── */

  /* The card. The only dark surface on the page, so its colours are declared
     here rather than borrowed from the light palette above. */
  --guardian-card-bg: #19022A;
  /* FIGMA */
  --guardian-card-radius: 44px;
  /* FIGMA */
  --guardian-card-pad: 80px;
  /* FIGMA — the inset from the card edge to the copy */
  --guardian-card-w: 1240px;
  /* FIGMA */

  /* The headline's measure. Set in pixels because it is doing a specific job:
     holding the break after 'code' so the two lines fall as the comp draws
     them. Wider and 'before' joins line one; much narrower and it becomes
     three lines. */
  --guardian-title-measure: 730px;
  /* GUESS — measured off the comp */

  /* The code panel is narrower than the column it sits in and hangs off the
     right edge of the card's content, not the middle of the row. */
  --guardian-panel-w: 450px;
  /* GUESS — measured off the comp */
  --gap-guardian-card: 50px;
  /* FIGMA — head to the row below it */

  --guardian-ink: #FFFFFF;
  /* GUESS — the headline */
  --guardian-ink-soft: #798BA8;
  /* GUESS — body copy on the dark card */
  --guardian-ink-faint: #9B8FAE;
  /* GUESS — the eyebrow and the file name */

  /* 50 to match --fs-section-title at desktop; was a guessed 48. The tablet
     and phone steps below are untouched. */
  --fs-guardian-title: 50px;
  --lh-guardian-title: 118%;
  /* GUESS */
  --fs-guardian-lede: 18px;
  /* GUESS */
  --fs-guardian-claim: 17px;
  /* GUESS */

  --gap-guardian-head: var(--gap-section-head);
  /* GUESS — eyebrow to headline */
  --gap-guardian-copy: 32px;
  /* GUESS — lede to claims to button */

  /* The code panel. Darker than the card it sits on rather than lighter — the
     comp sinks it in, which is what makes it read as a window rather than a
     tile lying on top. */
  --guardian-panel-bg: #120722;
  /* FIGMA — the title bar is a shade lighter than the body it caps */
  --guardian-bar-bg: #182234;
  /* GUESS */
  --guardian-panel-line: rgba(255, 255, 255, .08);
  /* GUESS — the panel's edge and the rule under its title bar */
  --guardian-panel-radius: 12px;
  /* GUESS */
  --fs-guardian-code: 13.5px;
  /* GUESS */

  /* Syntax colours. Deliberately few: the panel is making one point — that eval
     line is dangerous — and every extra hue spent elsewhere weakens it. */
  --guardian-tok-fn: #798BA8;
  /* GUESS — add_action */
  --guardian-tok-kw: #83B5FF;
  /* GUESS — function */
  --guardian-tok-str: #5ED9A4;
  /* GUESS — quoted strings */
  --guardian-tok-bad: #FDA49E;
  /* GUESS — eval */

  --guardian-flagged-bg: rgba(255, 92, 92, .12);
  /* GUESS — the tinted row behind the eval line */
  --guardian-flag-bg: rgba(248, 196, 109, .12);
  /* GUESS — the warning row */
  --guardian-flag-ink: #F8C46D;
  /* GUESS */
}

/* ═══════════════════════════════════════════════════════════════════════════
   BASE
   ═══════════════════════════════════════════════════════════════════════════ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font);
  font-weight: var(--fw-regular);
  color: var(--text);
  background: var(--wash-base);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  color: inherit;
}

/* Never remove a focus ring without replacing it — keyboard users navigate by
   it. :focus-visible shows it for keyboard traversal and hides it for a mouse
   click, which is the behaviour that made people remove the outline in the
   first place. */
:where(a, button):focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
  border-radius: 4px;
}

/*
 * clip, not hidden.
 *
 * They look identical — both cut whatever hangs past the side — but hidden also
 * makes this element a SCROLL CONTAINER, and a scroll container is what every
 * position:sticky inside it sticks to. This one never scrolls (the page does),
 * so sticky had nothing to stick to and simply stopped working.
 *
 * That silently broke the Engine section: its card is meant to pin while the
 * 3200px track scrolls past underneath, and instead the card scrolled away and
 * left the rest of the track empty below it. Measured at a 1440x800 viewport —
 * the card's bottom reached y=164 with 636px of empty section under it, and it
 * read as "free" at every scroll position through the track.
 *
 * It only surfaced today: a stray </div> in the Stats section had been closing
 * this wrapper early, so the Engine section was escaping it and sticking by
 * accident. Repairing the markup put it back inside, and the conflict appeared.
 *
 * overflow: clip cuts the same way and creates no scroll container, so sticky
 * keeps working. Chrome 90+, Firefox 81+, Safari 16+.
 */
.page-wrap {
  overflow-x: clip;
}

/* ═══════════════════════════════════════════════════════════════════════════
   GRID
   Layout is Bootstrap's container/row/col, matching how LeadXForms builds its
   pages — but only bootstrap-grid.min.css is loaded, not the whole framework.
   The grid file is 51 KB against 227 KB for full Bootstrap, and the difference
   is components this page defines for itself: buttons, cards, accordions.

   What that leaves out is the typography and colour utilities. `text-center` is
   the only one the markup wants, so it is declared below rather than pulling in
   another 83 KB for one rule.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * .container is left exactly as Bootstrap ships it, which is what LeadXForms
 * does — its stylesheet carries no .container rule at all. The two sites are
 * one brand and the page's spine is the most visible thing they can share, so
 * this page does not carry one of its own either.
 *
 *   ≥1400  1320    ≥1200  1140    ≥992  960    ≥768  720    ≥576  540
 *
 * It was 1280 + a gutter, because the comp draws 1280 of content in a 1440
 * frame. That is 16px narrower than the brand's, and matching the brand is
 * worth more than matching the comp to the pixel on a measurement the comp
 * inherited from Bootstrap in the first place.
 */

/*
 * Sections that line up with the hero's dashboard rather than with the page
 * container.
 *
 * The shot is the widest, heaviest thing on the page, and the eye reads its edge
 * as where the page begins — so a section starting 76px outside it looks
 * misaligned even though both are perfectly centred. Opt in with
 * `class="container container-aligned"`.
 *
 * Only needed at xxl. Below 1400 Bootstrap's container is already narrower than
 * the shot, so the shot is being constrained by the column too and the two agree
 * on their own.
 */
/* @media (min-width: 1400px) {
  .container-aligned {
    max-width: calc(var(--shot-w) + var(--bs-gutter-x, 1.5rem));
  }
} */

/* ═══════════════════════════════════════════════════════════════════════════
   SECTION HEADING
   The h2 every section below the hero uses. Centred, black, 56 — that is the
   page's default voice, and a section that wants something else says so by
   adding a class rather than by restating the whole thing.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * EVERY EYEBROW IS OFF, at every width.
 *
 * Two of these are on the page — the hero's trust line and geo's "Live Threat
 * Intelligence". The other six are styled here and used by no element, and they
 * are listed anyway so that adding one back to the markup does not quietly put
 * it on screen.
 *
 * display, not visibility or an offscreen clip, because it is meant to be gone
 * rather than merely unseen: display:none takes it out of the accessibility
 * tree too, so a screen reader is not read a line the page no longer shows.
 */
.hero-eyebrow,
.stat-eyebrow,
.engine-eyebrow,
.deep-eyebrow,
.guardian-eyebrow,
.geo-eyebrow,
.action-eyebrow,
.diff-eyebrow,
.plans-eyebrow {
  display: none;
}

.section-title {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-section-title);
  font-weight: var(--fw-regular);
  line-height: var(--lh-section-title);
  text-align: center;
  color: var(--text-heading);
}

/* The only utility borrowed from outside the grid build. */

/* ═══════════════════════════════════════════════════════════════════════════
   HERO
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The header is fixed, so it is out of flow and reserves no space. The hero has
 * to clear it itself — hence --header-h in the top padding. site.js keeps that
 * variable equal to the header's measured height, so it stays correct when the
 * nav wraps or the mobile menu opens rather than being a number that drifts.
 */
.hero {
  position: relative;
  isolation: isolate;
  padding-top: calc(var(--header-h) + var(--hero-pad-top));
  padding-bottom: var(--hero-pad-bottom);
  overflow: hidden;
}

/*
 * The wash is the designer's own artwork rather than a CSS approximation of it.
 * That is affordable here because the file is 2.8 KB — three blurred organic
 * shapes over a white base, all vector — so it costs about what the hand-made
 * gradients cost and is exactly right instead of merely close.
 *
 * Stretched to the box, not covered. The file is 1440×1307 and its blue sits low
 * and to the left; `cover` on anything wider than 4:3 scales it up until the
 * width fits and then shows only the top of it, which is the pale end. On a
 * 1920×1000 hero that is the top 57% of the artwork — the composition's whole
 * weight cropped off the bottom, which reads as the wash being too faint.
 *
 * 100% 100% scales the two axes independently, so the whole composition is
 * always on screen at the density the comp draws it. Non-uniform scaling would
 * matter on artwork with edges; this is three Gaussian blurs, and there is
 * nothing in it sharp enough to look stretched.
 *
 * --wash-base sits underneath as the last layer, so the section is white rather
 * than transparent for the moment before the file arrives — and if it never does.
 */
.hero-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    url('../images/hero-gradient.webp') center top / 100% 100% no-repeat,
    var(--wash-base);
}

/* Above the wash, which sits at z-index -1 inside .hero's stacking context. */
.hero-container {
  position: relative;
}

/*
 * The copy column. Everything inside is left-aligned and stacked; the gap is the
 * rhythm, so it lives on this one rule rather than on each child.
 */
.hero-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
}

/*
 * The eyebrow and the headline are set closer to each other than the copy
 * column's own rhythm, so they are wrapped and given their own gap. Without the
 * wrapper the flex gap above applies between every pair, and the eyebrow drifts
 * away from the heading it introduces.
 */
.hero-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-eyebrow-title);
}

/* ── Eyebrow ─────────────────────────────────────────────────────────────── */

.hero-eyebrow {
  margin: 0px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  font-family: var(--font-ui);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-medium);
  line-height: var(--lh-eyebrow);
  text-transform: capitalize;
  color: var(--text);
}

/* The separators are drawn, not typed — a literal bullet character in the
   markup is read aloud as "bullet" between every phrase. Drawn ones are
   invisible to assistive tech, and the first item correctly has none. */
.hero-eyebrow span+span::before {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-right: 14px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: middle;
}

/* ── Headline ────────────────────────────────────────────────────────────── */

/*
 * The fade is a gradient clipped to the glyphs, not two colours of text. That is
 * what the comp specifies, and it is also why the effect reads the way it does:
 * the gradient is laid across the whole heading box, so each line picks up
 * whatever part of it sits at that line's horizontal position. Both lines end
 * lighter than they start without a single word being marked up.
 *
 * -webkit-text-fill-color is what actually makes the glyphs transparent so the
 * background shows through; `color` alone would paint over it. The colour
 * declaration below it is not redundant — it is what any engine without
 * background-clip support falls back to, and what a copied selection inherits.
 */
.hero-title {
  margin: 0;
  max-width: var(--title-measure);
  font-size: var(--fs-h1);
  line-height: var(--lh-h1);
  font-weight: var(--fw-h1);
  color: var(--text);
  background: var(--title-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

/* ── Lede ────────────────────────────────────────────────────────────────── */

.hero-lede {
  margin: 8px 0px;
  max-width: var(--lede-measure);
  font-size: var(--fs-lede);
  line-height: var(--lh-lede);
  font-weight: var(--fw-regular);
  color: var(--text-muted);
}

/*
 * Larger and bolder than the paragraph under it, but the same grey — the comp
 * does not darken it. The weight and the two extra points are what carry the
 * emphasis, so this is not the place to "fix" the colour to black.
 */
.hero-lede-strong {
  font-size: var(--fs-lede-strong);
  font-weight: var(--fw-bold);
  color: var(--text-muted);
}

/* ── Actions ─────────────────────────────────────────────────────────────── */

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 22px;
}

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-primary,
/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-size: var(--fs-btn);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color .18s ease, color .18s ease;
}
*/
*/
/*
 * Figma reports height 40 and padding 16 together, which cannot both hold: 16
 * top and bottom inside a 40px box leaves 8px for a 16px line. The 40 is the
 * pill's real height and the 16 is its horizontal inset, so that is how it is
 * written here. min-height rather than height so a wrapped label grows the
 * button instead of spilling out of it.
 */
/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-primary {
  min-height: var(--btn-h);
  padding: 0 var(--btn-pad-x);
  border-radius: var(--btn-radius);
  background: var(--green);
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: var(--fw-medium);
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-primary:hover {
  background: var(--green-hover);
}
*/

/* The secondary action is a text link on purpose — two filled buttons compete
   for the same attention, and the design gives this one no weight of its own. */
/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-ghost {
  min-height: var(--btn-h);
  padding: 0;
  color: var(--text);
  font-family: var(--font-accent);
  font-weight: var(--fw-semibold);
  line-height: normal;
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-ghost:hover {
  color: var(--green);
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-icon {
  display: inline-flex;
  transition: transform .18s ease;
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.btn-hero-ghost:hover .btn-icon {
  transform: translateX(3px);
}
*/

/* ── Micro line ──────────────────────────────────────────────────────────── */

.hero-micro {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-family: var(--font-accent);
  font-size: var(--fs-micro);
  font-weight: var(--fw-regular);
  line-height: 1.5;
  text-transform: capitalize;
  color: var(--text-micro);
}

.hero-micro-star {
  display: inline-flex;
  color: var(--gold);
}

/* ── Trust row ───────────────────────────────────────────────────────────── */

/*
 * Centred while everything above it is left-aligned — that is what the design
 * does, and it reads as a band rather than a continuation of the copy. It sits
 * in its own full-width row for exactly that reason.
 */
.hero-trust {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 18px 56px;
  margin: 0 0 var(--gap-trust-shot);
  padding: 0;
  list-style: none;
}

.hero-trust-item {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-ui);
  font-size: var(--fs-trust);
  font-weight: var(--fw-medium);
  line-height: var(--lh-trust);
  text-transform: capitalize;
  color: var(--text);
}

.hero-trust-icon {
  display: inline-flex;
  flex-shrink: 0;
  color: var(--green);
}

/* ── Product shot ────────────────────────────────────────────────────────── */

/*
 * No radius, no clipping and no shadow here: the export carries its own rounded
 * frame, and CSS decoration would draw a second, rectangular edge around artwork
 * whose real edge sits inside it.
 *
 * max-width is 1128 rather than the 1110 Figma reports — see --shot-w.
 */
.hero-shot {
  width: 100%;
  margin: 0 auto;
  /* The shot used to be the narrowest thing on the page at 1128, and
     .container-aligned existed to pull the other sections in to meet it. That
     override is commented out — the LeadXForms container is the rail now — so
     the shot meets the sections instead. It is an SVG, so it loses nothing
     being drawn wider. */
  /* max-width: var(--shot-w); */
}

.hero-shot img {
  width: 100%;
  height: auto;
}

/* ═══════════════════════════════════════════════════════════════════════════
   STATS
   ═══════════════════════════════════════════════════════════════════════════ */

.stats {
  padding-block: var(--section-pad-y);
}

/* The chart and the copy are set against each other vertically; on a narrow
   screen the row stacks and this gap becomes the space between them. */
.stats-row {
  row-gap: 40px;
}

/* ── The card ────────────────────────────────────────────────────────────── */

/*
 * The chart is an image, so the frame it used to need — border, radius, padding,
 * the grid, the legend rules — went with it. What is left is the box the picture
 * sits in.
 *
 * max-width rather than a fixed width: the column is wider than the artwork and
 * would otherwise stretch a 294px file across it.
 */
.stat-card {
  max-width: var(--stat-card-w);
  margin: 0;
}

.stat-card img {
  width: 100%;
  height: auto;
  border-radius: var(--stat-card-radius);
}

/* ── The copy ────────────────────────────────────────────────────────────── */

.stat-copy {
  display: flex;
  flex-direction: column;
  gap: var(--gap-stat-copy);
}

/*
 * Stats is the one section whose heading is not centred: it sits in a column
 * beside the chart, so it reads left like the paragraph under it.
 *
 * The heading itself carries no colour any more. It is two pieces of text with
 * two different treatments — the figure and the sentence — and each says so on
 * its own element rather than one inheriting from the block.
 */
.stat-title {
  text-align: left;
}

/*
 * The figure. Its own gradient, clipped to its own glyphs: a single inline box
 * on one line, so the blue runs from the "4" to the percent sign and finishes
 * there.
 */
.stat-figure {
  font-size: var(--fs-stat-figure);
  background: var(--stat-figure-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

/*
 * The sentence, in the page's ink gradient — the same token the hero headline
 * uses, because it is the same treatment.
 *
 * box-decoration-break is the load-bearing line. This span breaks across two
 * lines, and by default a broken inline box has its background painted as one
 * long unbroken strip and then sliced between the lines — which would hand the
 * first line the dark half and the second the faded half, leaving "runs on
 * WordPress" grey when the comp draws it dark. `clone` gives each line its own
 * full copy of the gradient.
 */
.stat-title-text {
  font-size: var(--fs-section-title);
  background: var(--title-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/*
 * Both of these read their token now. They did not, and that is why this
 * section never responded: the rules hardcoded 22 and 30, so --fs-stat-lede and
 * --fs-stat-lede-strong were dead and their phone steps — 16 and 18 — were
 * never drawn. Measured before the fix, both ledes were still 22 and 30 on a
 * 390px screen while .guardian-lede and .cta-lede, which DO read their tokens,
 * had stepped to 16. That gap is the inconsistency this section was reported
 * for, and it was four dead tokens, not a missing breakpoint.
 *
 * The base values moved to 22 and 30 to match — those are what desktop has been
 * drawing all along, so reading the token changes nothing above 991.
 */
.stat-lede {
  margin: 0;
  color: #000;
  font-family: "Product Sans";
  font-size: var(--fs-stat-lede);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh-stat-lede);
}

.stat-lede-strong {
  color: #000;
  font-family: "Product Sans";
  font-size: var(--fs-stat-lede-strong);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh-stat-lede);
}

/*
 * Forced colours means someone has asked the OS to override the page's palette,
 * usually for contrast. A transparent fill over a gradient leaves them with an
 * invisible headline, so the effect is dropped and the text painted normally.
 */
@media (forced-colors: active) {

  .hero-title,
  .stat-figure,
  .stat-title-text {
    background: none;
    -webkit-text-fill-color: currentColor;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   HOW PROTECTION WORKS
   ═══════════════════════════════════════════════════════════════════════════ */

.engine {
  padding-block: var(--section-pad-y);
  background:
    url('../images/engine-bg.webp') top center / 100% auto no-repeat,
    var(--wash-base);
}

/* ── Scroll track ────────────────────────────────────────────────────────── */

/*
 * The section is scrolled through rather than past: the track is tall, the card
 * inside it sticks, and how far the track has travelled decides which layer is
 * shown. site.js reads the track's position and does the rest.
 *
 * Nothing here depends on that script. With JavaScript off the track is simply a
 * tall box with the card pinned inside it and layer one showing — the copy for
 * all five layers is still in the HTML.
 */
.engine-track {
  position: relative;
  height: calc(5 * var(--engine-step-h));
}

/* No will-change here on purpose. Promoting the sticky element puts the whole
   card — five blurred SVGs included — into one layer, and then any change inside
   it repaints all of that. The things that actually animate are promoted
   individually instead. */
.engine-sticky {
  position: sticky;
  top: var(--header-h);
  display: flex;
  align-items: center;
  min-height: calc(100vh - var(--header-h));
}

.engine-card {
  width: 100%;
}

/* ── Section header ──────────────────────────────────────────────────────── */

.engine-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-section-head);
  margin-bottom: 56px;
  text-align: center;
}

/* ── The dark card ───────────────────────────────────────────────────────── */

.engine-card {
  padding: var(--engine-card-pad);
  background: var(--engine-card-bg);
  border-radius: var(--engine-card-radius);
}

.engine-row {
  row-gap: 48px;
}

/* The tabs are the last child and belong at the foot of the column, however
   short the panel above them happens to be. */
.engine-copy {
  display: flex;
  flex-direction: column;
}

/* ── Panels ──────────────────────────────────────────────────────────────── */

/*
 * All five panels share one grid cell, so the container is always as tall as the
 * longest of them and a switch changes nothing about the layout. An earlier
 * version toggled [hidden] — a display change, which is a layout, happening
 * mid-scroll inside a card that a sticky element takes its height from.
 *
 * visibility rather than display does the hiding: it keeps the cell sized and
 * still removes the panel from the accessibility tree, so a screen reader is
 * read the open layer and not all five.
 */
/*
 * The tabs are inside this block now, and its gap is what sets them off.
 *
 * The five panels all claim grid-area 1/1, so the tabs are the only child left
 * to auto-place and they land in row 2, under whichever panel is showing. One
 * row-gap then governs the step to them.
 *
 * They are here and not inside .engine-panel because there are five of those and
 * one tablist: putting it in the panel would mean five copies of it, five sets
 * of the same ids, and a screen reader offered the same five controls over and
 * over. This is the nearest box that holds all five panels at once.
 *
 * 35 is the gap .engine-panel sets between its own children, so the step from
 * the note down to the tabs is the same step as every one above it.
 */
.engine-panels {
  display: grid;

  /*
   * minmax(0, 1fr), and it is not decoration.
   *
   * With no columns declared, the single implicit track is sized auto — and an
   * auto track's MINIMUM is min-content, which a grid refuses to go below even
   * when its own parent is narrower. So any child with a wide min-content pushes
   * this box out past .engine-copy and the copy runs outside the column.
   *
   * The 0 floor lets the track take the width it is given and hands the wrapping
   * back to the content, which is what every child in here does anyway: the
   * paragraph wraps, both lists wrap, the tags wrap. Nothing is made narrower
   * than it should be; it is only allowed to be as narrow as it has to.
   *
   * The same trap the note on .engine-tags describes — a nowrap row's
   * min-content is all of its children side by side — one level up.
   */
  grid-template-columns: minmax(0, 1fr);
  /* The step from the panel down to the tabs, which is what this gap is. */
  gap: var(--gap-engine-tabs);
}

.engine-panel {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-engine-panel);
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s ease, visibility 0s linear .3s;
}

.engine-panel.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity .3s ease, visibility 0s;
}

.engine-step {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-engine-step);
  font-weight: var(--fw-medium);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--engine-ink-faint);
}

.engine-title {
  margin: 0;
  font-size: var(--fs-engine-title);
  font-weight: var(--fw-regular);
  line-height: 1.1;
  color: var(--engine-ink);
}

.engine-desc {
  margin: 0;
  color: #FFF;
  font-family: "Product Sans";

  /*
   * max-width, not width. 600 was a flat width, and a width is a width at every
   * screen size — on a 390px phone the paragraph stayed 600 and ran out of the
   * card, which is the copy that was cut off down the right-hand edge. Same
   * fault .cta-card's note records, and the same fix: 600 is still the number,
   * it is a ceiling now instead of a floor.
   */
  max-width: 600px;

  /* Through the token, so the size can step with the screen. Hardcoded at 22 it
     stayed 22 on a phone while the heading beside it came down to 30, and the
     list under it did the same — which is why the bullets read LARGER than this
     paragraph at that width. */
  font-size: var(--fs-engine-desc);
  font-style: normal;
  font-weight: 400;
  line-height: normal;
}

/* ── Tags ────────────────────────────────────────────────────────────────── */

.engine-tags {
  list-style: none;
  margin: 0;
  text-transform: uppercase;
  display: flex;
  color: var(--Black, #151515);
  font-family: "Product Sans";
  font-size: 18px;
  font-style: normal;
  font-weight: 400;
  line-height: 150%;
  flex-wrap: wrap;
  min-height: 40px;
  padding: 6px 0;
  align-items: center;
  gap: var(--engine-tags-gap);
  border-radius: 10px;
}

/* Pale fills with dark text: on this card they are the only light shapes, so
   they read as labels rather than as buttons someone can press. */
.engine-tag {
  padding: var(--engine-tag-pad-y) var(--engine-tag-pad-x);
  border-radius: 8px;
  color: var(--Black, #151515);
  font-family: "Product Sans";
  font-size: var(--fs-engine-tag);
  font-style: normal;
  font-weight: 400;
  line-height: 150%;
}

.engine-tag--block {
  background: var(--engine-tag-block);
}

.engine-tag--filter {
  background: var(--engine-tag-filter);
}

.engine-tag--harden {
  background: var(--engine-tag-harden);
}

/* ── What it catches ─────────────────────────────────────────────────────── */

.engine-list {
  margin: 0;
  padding-left: var(--engine-list-indent);
  /* Through the token — see the note on .engine-desc. */
  font-size: var(--fs-engine-list);
  line-height: 1.7;
  color: #FFF;
}

.engine-list li::marker {
  color: #FFF;
}

/* ── The base-plate note ─────────────────────────────────────────────────── */

/* A rule down the left rather than a box: it marks the passage as an aside
   without building a second card inside the card. */
.engine-note {
  padding-left: var(--engine-note-indent);
  border-left: 2px solid rgba(255, 255, 255, .18);
}

.engine-note-title {
  margin: 0 0 6px;
  font-size: var(--fs-engine-note-title);
  color: #FFF;
}

.engine-note-body {
  margin: 0;
  font-size: var(--fs-engine-note-body);
  line-height: 1.5;
  color: #FFF;
}

/* ── Tabs ────────────────────────────────────────────────────────────────── */

.engine-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.engine-tab {
  padding: var(--engine-tab-pad);
  border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 8px;
  background: transparent;
  font-family: var(--font-ui);
  font-size: var(--fs-engine-tab);
  font-weight: var(--fw-medium);
  color: var(--engine-ink);
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease;
}

/* Same reasoning as the segmented hover below: this and .is-active have equal
   specificity and this comes first, so the border is safe — but on touch a stuck
   :hover would still light up the tab beside the one you picked. Pointer only. */
@media (hover: hover) {
  .engine-tab:hover:not(.is-active) {
    border-color: rgba(255, 255, 255, .45);
  }
}

.engine-tab.is-active {
  background: var(--engine-accent);
  border-color: var(--engine-accent);
}

/* ── Diagram ─────────────────────────────────────────────────────────────── */

.engine-visual {
  display: flex;
  align-items: center;
  justify-content: center;
}

/*
 * The five diagrams are stacked in one box. aspect-ratio gives that box its
 * height without any of the images being in flow — they are all absolute, so
 * nothing would size it otherwise, and a box that resized per layer would make
 * the card jump on every change.
 */
.engine-diagrams {
  position: relative;
  width: 100%;
  max-width: 525px;
  aspect-ratio: 525 / 652;
}

/*
 * will-change promotes each diagram to its own compositor layer. Without it they
 * all live in the sticky element's single layer, so fading one in forces that
 * whole layer to repaint — and repainting it means re-running the Gaussian blur
 * inside every one of the five SVGs, on a scroll frame.
 *
 * Five permanent layers is a real memory cost and normally worth avoiding. Here
 * it is the right trade: these five elements are the only thing on the page that
 * animates during a scroll, and they are the expensive thing.
 */
.engine-diagram {
  will-change: opacity;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  visibility: hidden;
  transition: opacity .35s ease, visibility 0s linear .35s;
}

.engine-diagram.is-active {
  opacity: 1;
  visibility: visible;
  transition: opacity .35s ease, visibility 0s;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FEATURE DEEP-DIVES
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Three washes down the section, the designer's own files — a blue, a lavender
 * and a warm pink, each one large soft circle. About 0.5 KB apiece over the wire.
 *
 * Positioned by keyword rather than by pixel offset. Their combined natural
 * height is over 4,300px while the section's real height depends on the copy and
 * the viewport, so stacking them at fixed offsets would be right at exactly one
 * window size. top / center / bottom spreads them down whatever the section
 * turns out to be.
 */
.deep {
  position: relative;
  isolation: isolate;
  padding-block: var(--section-pad-y);
  background: var(--wash-base);

  /*
   * The washes are wider than a narrow viewport and sit half outside the section
   * at top and bottom. Without this they would push the page sideways and
   * lengthen it. Cropping them is what Figma does at a narrower frame too.
   *
   * clip, not hidden — the same distinction that broke position:sticky on
   * .page-wrap. hidden makes this element a SCROLL CONTAINER, and a scroll
   * container is what animation-timeline: view() measures an element's progress
   * against. This one never scrolls, so the rows' reveal sat frozen at its end
   * state: measured at six scroll positions through the section, every half read
   * opacity 1 and 0px of travel, at every one of them.
   *
   * clip crops identically and creates no scroll container, so view() resolves
   * against the page as it should.
   */
  overflow: clip;
}

/* ── The washes ────────────────────────────────────────── */

/*
 * THE BLUR IS ALREADY IN THE FILE. Do not add filter: blur() to these.
 *
 * Each one is the Figma layer exported whole: a 507-radius circle (1014 across,
 * which is the width Figma reports), filled with a white-to-#DDE9FF gradient at
 * 50% opacity, through a Gaussian blur of stdDeviation 200. That is the same
 * thing the CSS panel describes — so the CSS panel's border-radius, background,
 * filter, width and height all have a counterpart inside the file and none of
 * them belongs out here. Adding filter: blur(200px) on top would blur an
 * already-blurred picture.
 *
 * THESE ARE .webp, AND THAT IS THE POINT. They were .svg, and the SVG carried
 * the blur as a live <feGaussianBlur> rather than as pixels. "Exported from
 * Figma" is not the same as "rasterised": the filter is re-executed every time
 * the browser rasters a tile of the image, and these are 1440px wide and up to
 * 1814 tall. Scrolling brings new tiles in constantly, so the page spent 22.5
 * SECONDS in RasterTask over one pass down it — against 37ms of JavaScript —
 * and dropped 39% of its frames. Baking them to bitmaps took that to zero.
 *
 * So: regenerate from the .svg sources next to them (kept for exactly that),
 * at 360px wide — the art is a 200-radius blur, it has no detail to lose, and
 * a bitmap upscales for free where a filter re-runs. Measured against the SVGs
 * the swap moves no pixel by more than 8/255. What you must not do is point
 * these back at the .svg files.
 *
 * What stays a CSS decision is only where each one sits, which is why they are
 * three elements rather than three background layers on one rule: a layer list
 * puts each wash's position and size in a different part of the same shorthand,
 * and nudging one means counting commas.
 */
.deep-blurs {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

/*
 * Centred on the section and held at the width of the comp rather than
 * stretched to the viewport: these are soft-edged and already transparent well
 * before their bounding box ends, so a wider screen shows no seam at the sides
 * — while stretching would make the glow grow with the window and stop
 * matching the 1280 of content it sits behind.
 *
 * max-width:none because the global img rule caps images at 100%, which here
 * would shrink the wash on any viewport under 1440.
 */
.deep-blur {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: var(--deep-blur-w);
  max-width: none;
  height: auto;
}

/*
 * Where each one sits, and each is anchored to the edge its own file is cut
 * against — which is the part worth knowing before moving any of them.
 *
 * All three hold the same 507-radius circle, but their canvases differ, and the
 * difference is where Figma cropped them:
 *
 *   1  1440×1464, circle at y 557.  Filter box starts at -350, so 350px of glow
 *                                  is cut off the top. Its top edge is the
 *                                  section's top edge.
 *   2  1440×1814, circle at y 907.  Nothing cropped — 1814 is exactly the blur's
 *                                  full extent, and 907 is dead centre of it.
 *   3  1440×1026, circle at y 907.  Cut off at the bottom. Its bottom edge is
 *                                  the section's bottom edge.
 *
 * So 1 pins to the top, 3 pins to the bottom, and 2 — the only uncropped one —
 * centres. That lands its glow on the 03/04 rule, which is where the comp puts
 * it, and keeps it there however much the copy reflows.
 */
.deep-blur--1 {
  top: var(--deep-blur-1-top);
}

/* The only one that needs its own transform: translateX alone would leave it
   hanging from its top edge at the halfway mark rather than straddling it. */
.deep-blur--2 {
  top: var(--deep-blur-2-top);
  transform: translate(-50%, -50%);
}

.deep-blur--3 {
  bottom: var(--deep-blur-3-bottom);
}

.deep-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-section-head);
  text-align: center;
  margin-bottom: 56px;
}

/* The lede carries the gap down to the grid now that it has left the head —
   see .deep-head-row for why it was moved out of the sticky block. */
.deep-lede-row {
  text-align: center;
}

.deep-lede {
  margin: 0;
  color: #000;
  text-align: center;
  font-family: "Product Sans";
  /* Token, not a literal — see the note on .stat-lede. --fs-deep-lede was dead
     for the same reason and never stepped on a phone. */
  font-size: var(--fs-deep-lede);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh-lede);
}

/* ── The ruled grid ──────────────────────────────────────────────────────── */

/*
 * The rules are borders on the rows, not a background or an image. One pixel of
 * grey is the kind of thing that survives a border and goes soft in a bitmap,
 * and it has to land on the same pixel as the diamonds sitting on top of it.
 *
 * Position is relative so the closing diamond can be hung off the bottom edge —
 * every other diamond belongs to the row beneath it.
 */
/*
 * The grid is a closed box, not just a set of internal rules: the comp draws an
 * outer edge all the way round it, which is what makes the last row read as a
 * complete rectangle rather than a row with two of its sides missing.
 *
 * Three sides here rather than four. The bottom is already drawn by row 06's
 * own cells — every cell paints its bottom edge — and adding it here as well
 * would stack two lines on the same pixel and leave the closing rule heavier
 * than the five above it.
 *
 * Real borders rather than the pseudo-element the top line used to use, so the
 * corners meet: a pseudo positioned left:0 right:0 sits inside the border box
 * and stops a pixel short at each end.
 */
/*
 * No rules of its own, and that is deliberate.
 *
 * Every row draws its own top, and the last one draws the closing line as well
 * — both from .deep-row::after, which sits above the cells and so cannot be
 * painted over by the artwork half's white. A background here would be under
 * them, which is exactly how the closing line went missing over the picture.
 *
 * No sides either. All four was what made the six rows read as a bordered
 * component sitting on the page rather than as a ruled list running through it;
 * rows 04 and 06 close their outer edge on their own and say so further down.
 */
.deep-grid {
  position: relative;
}

/*
 * AN OUTER FRAME, STACKED ONLY — top, left and right.
 *
 * On the desktop grid the section needs none: the centre line, the row rules
 * and the markers give it all the structure it has, and a box around it made
 * the six rows read as a bordered component sitting on the page. Stacked there
 * is no centre line and no marker, so the entries have a rule above and below
 * and nothing holding their ends — which reads as a list that has lost its
 * margins rather than as a section.
 *
 * No bottom: row 06's own closing rule is already there, and a border here
 * would paint that same edge a second time.
 *
 * 991.98 and not 991, because 991.98 is where the columns actually stack. At a
 * flat 991 there is a pixel of width where the frame is drawn around a layout
 * that is still two columns.
 */
@media (max-width: 991.98px) {
  .deep-grid {
    border-top: 1px solid var(--deep-rule);
    border-inline: 1px solid var(--deep-rule);
  }
}

/*
 * A Bootstrap .row of two .col-lg-6, matching how every other section on this
 * page is built rather than being the one place with a hand-rolled grid.
 *
 * g-0 in the markup zeroes the gutter. The default 24 would put 12px of air
 * either side of the rule down the middle, and that rule is the point of the
 * whole layout — the two halves have to meet on it.
 *
 * Which half goes first is stated per row in the markup with order-lg-*, not
 * worked out here by counting. Rows get reordered and inserted; a selector
 * that counts them silently mirrors the wrong ones when that happens.
 */
.deep-row {
  position: relative;
}

/*
 * THE TWO SURFACES LIVE ON THE ROW, NOT ON THE CELLS THAT MOVE.
 *
 * The reveal animates .deep-copy and .deep-shot themselves — opacity 0 to 1 and
 * a 34px translate with a .97 scale. The artwork's opaque white was on
 * .deep-shot, so for the whole of that range it was neither opaque nor still:
 * part-faded, the wash below showed straight through the artwork; part-
 * travelled, the white slid off its own half and opened a strip of wash at one
 * edge. That is the wash appearing to move with the row.
 *
 * .deep-row carries no animation and no transform, so a surface painted here
 * cannot fade and cannot travel. The artwork half gets --wash-base, the same
 * white .deep and .deep-shot already use; the copy half is left transparent, so
 * .deep-blurs goes on showing through it exactly as before. Nothing about the
 * wash itself changes — only which element is responsible for covering it.
 *
 * .deep-shot keeps its own white. It now slides over an identical white instead
 * of over the wash, so the motion is invisible rather than wrong.
 *
 * The hard stop is at 50%, which is the col-lg-6 boundary — and the 1px centre
 * divider is drawn on that exact line by .deep-row::after, so any half-pixel
 * seam at a fractional container width is underneath it.
 *
 * Painting order is what makes this work and none of it is new: .deep-blurs is
 * at z-index -1 inside .deep's stacking context, this background is a
 * position:relative descendant above it, .deep-row::after is the row's last
 * child so the divider still paints over both cells, and the stars hold
 * z-index 1 above everything.
 *
 * From lg only. Stacked there are no halves to split.
 */
@media (min-width: 992px) {

  /*
   * White under the picture, clear under the copy.
   *
   * The white is what keeps .deep-blurs off the artwork — that layer sits at
   * z-index:-1 inside .deep's stacking context, above the section's background
   * and below every cell, so anything that does not paint over it lets it
   * through. The copy's half is left clear on purpose: the same blurs are the
   * wash behind the copy, and they should reach it.
   *
   * On the row and not on the cells, because .deep-copy and .deep-shot are what
   * the reveal animates — a surface there fades in and slides 34px, and the
   * background visibly travels with the row.
   */
  .deep-row {
    background: linear-gradient(to right, transparent 50%, var(--wash-base) 50%);
  }

  /* Rows 02, 04 and 06 put the picture on the left, so the white goes with it. */
  .deep-row--mirror {
    background: linear-gradient(to left, transparent 50%, var(--wash-base) 50%);
  }

  /*
   * ROWS 04 AND 06 CLOSE ON THEIR OUTER EDGE. No other row does.
   *
   * The comp encloses the copy on these two: the boundary above it, the
   * boundary below it and the centre line already draw three of the four sides,
   * and this is the fourth. Their copy sits on the right, so the fourth side is
   * the section's right-hand edge.
   *
   * Drawn as a background layer on top of the wash the row already carries, not
   * as a border-right. A border is laid out: it would take a pixel off the
   * row's content box and move its 50% — and with it the centre line — half a
   * pixel out of step with the other four rows. This paints in the same place
   * and costs the layout nothing.
   *
   * It is under both cells, which is where it has to be to read as an edge
   * rather than a line over the copy. .deep-copy paints no background of its
   * own, so it shows through.
   */
  .deep-row--4,
  .deep-row--6 {
    background:
      linear-gradient(var(--deep-rule), var(--deep-rule)) right / 1px 100% no-repeat,
      linear-gradient(to left, transparent 50%, var(--wash-base) 50%);
  }
}

/*
 * The mark where the rules cross — a four-pointed star, not the rotated square
 * this used to draw. The shape has concave sides and no CSS primitive makes
 * that, so it comes from the comp as an SVG.
 *
 * Inlined as a data URI rather than a seventh request for 850 bytes, and used
 * as a mask rather than a background image: a mask takes only the shape and
 * lets background-color supply the colour, so the star reads from --deep-rule
 * like every line it sits on. As an image its #E5E5E5 would be baked into the
 * URI and would quietly stop matching the moment that token changed.
 *
 * It sits on the crossing rather than punching a hole in it: the rules are at
 * their strongest here and fade away from this point, so the star reads as
 * where they come from.
 */
.deep-row::before,
.deep-grid::after {
  content: "";
  position: absolute;
  left: 50%;
  /* Above both rules — the centre line and the row boundary are both at 1 — so
     the marker stays the thing you see at a crossing rather than the pixel
     where two lines meet. */
  z-index: 2;
  width: var(--deep-star);
  height: var(--deep-star);
  pointer-events: none;
  background-color: var(--deep-rule);
  -webkit-mask: var(--deep-star-shape) center / contain no-repeat;
  mask: var(--deep-star-shape) center / contain no-repeat;
}

/* One on the top edge of every row … */
.deep-row::before {
  top: 0;
  transform: translate(-50%, -50%);
}

/* … and one on the bottom edge of the grid, which has no row below it to
   carry that one. */
.deep-grid::after {
  bottom: 0;
  transform: translate(-50%, 50%);
}

/* ── Cells ───────────────────────────────────────────────────────────────── */

/*
 * The rules are drawn on pseudo-elements rather than set as borders.
 *
 * Not for how they look — a 1px solid border would look the same — but for what
 * happens around them. The cells swap sides every other row, so a border would
 * have to be declared on a different side per row and cleared on the old one;
 * and two stacked rows drawing a full border each would paint the edge they
 * share twice, leaving one line heavier than the others until a negative margin
 * pulls them back together.
 *
 * A pseudo-element sidesteps both: it moves with the cell it belongs to, and
 * only ever draws the bottom edge, so nothing is ever painted twice.
 */
.deep-copy,
.deep-shot {
  position: relative;
  padding: var(--deep-cell-pad);
}

/*
 * The horizontal rule: the cell's bottom edge, the same weight from end to end.
 * Each cell draws its own half and the two meet under the star.
 *
 * Only the bottom edge, never the top, so two stacked rows cannot paint the
 * same pixel twice and leave one line heavier than the rest. The line above
 * row 01 is added back below, on the wrapper.
 */
.deep-copy::after,
.deep-shot::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  pointer-events: none;
  background: var(--deep-rule);
}

@media (min-width: 992px) {

  /*
   * THE CENTRE LINE — ONE ELEMENT, THE FULL HEIGHT OF THE GRID.
   *
   * It was six, one per row. They abutted and read as continuous, but they were
   * six chances to disagree, and the reason they were never the cells' inner
   * edges still holds: those are two different boxes, and at a fractional
   * column width they round off in opposite directions — one row gets a crisp
   * pixel, the next gets it spread over two at half strength, and the pattern
   * follows the mirroring.
   *
   * One line also puts it where the comp has it: unbroken from the rule above
   * row 01 to the one under row 06, with a marker at each crossing.
   *
   * z-index 1, and not more. The rows paint an opaque white half that begins at
   * exactly this offset, so left at auto the line would be underneath it. 1 is
   * the layer the markers already use, and they come after this in tree order,
   * so they still sit on top of it.
   */
  .deep-grid::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    z-index: 1;
    width: 1px;
    pointer-events: none;
    background: var(--deep-rule);
  }

  /* Each cell drew half the row's boundary; .deep-row draws the whole of it. */
  .deep-copy::after,
  .deep-shot::after {
    content: none;
  }
}

/*
 * min-height, not height, and the distinction is the whole rule.
 *
 * Figma reports 500 because that is what the cell measured in the comp. In
 * the browser the row is as tall as its tallest half, and that is almost
 * always the picture: the six exports are 539 wide by 377-481 tall, and once
 * scaled into a 560px column with 40 of padding either side, five of the six
 * rows land between 538 and 580. Pinning the copy cell to a flat 500 would
 * leave it short of its own row in those five — the wash stopping 40px above
 * the rule underneath it, with the copy centred in a box that is not the one
 * you can see.
 *
 * As a floor it does the job Figma's number is actually for: row 05 has the
 * shortest picture of the six and would otherwise sit noticeably shallower
 * than the rest. Everything above it stretches to its row, because a grid
 * item does that by default and min-height does not take it away.
 */
.deep-copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: var(--gap-deep-copy);
  min-height: var(--deep-cell-min-h);
}

/*
/*
 * THE ROW'S OWN BOUNDARY — one line, across both columns, on an element that
 * does not move.
 *
 * It was two halves, drawn by .deep-copy::after and .deep-shot::after. Those
 * two cells are what the reveal animates, and they do not arrive together: the
 * copy runs from entry 5% and the picture from entry 12%, and they travel 34px
 * in OPPOSITE directions on the way in. So through the whole reveal the line
 * existed on the copy's half and not yet on the picture's — a rule that looked
 * like it stopped at the middle of the row.
 *
 * .deep-row carries no animation and no transform, so this cannot fade out of
 * step with itself or slide.
 *
 * Absolute rather than border-bottom, which is the other way to put a line on
 * the row: a border is laid out, so it would add a pixel to every row and six
 * to the section, and it would leave the centre line a pixel short at every
 * crossing. This costs nothing and crosses cleanly.
 *
 * Painted as the row's last child, so it sits over both cells — including the
 * artwork cell's opaque white — rather than under them.
 */
.deep-row::after {
  content: "";
  position: absolute;
  /*
   * Stretched to the whole row, because it draws TWO rules, not one: the copy
   * block is ruled above AND below, and both of them land on the copy's own
   * half.
   *
   * That is what makes the boundary between two rows whole. The copy swaps
   * sides every row, so at any boundary the row above rules one half and the
   * row below rules the other — row 01's copy closes the left of the 01/02
   * line, and row 02's copy opens the right of it. Neither half is ever drawn
   * twice, and the picture above feature-02 gets the rule it was missing while
   * only row 01 had a bottom.
   *
   * The box is transparent apart from its two 1px layers, and pointer-events
   * are off, so covering the row costs nothing.
   */
  inset: 0;
  /*
   * z-index, and it is not decoration — .deep-row is a flex container, and a
   * flex container paints its children in ORDER-MODIFIED document order. The
   * mirrored rows carry order-lg-1 and order-lg-2 on their two cells; this
   * pseudo has no order, so it counts as 0 and was painted BEFORE both of them.
   * .deep-copy has no background of its own, so the line showed through on the
   * copy's half — and .deep-shot's opaque white covered it on the picture's.
   * That is why 02, 04 and 06 had half a line and 01, 03 and 05 had all of it.
   *
   * 1 lifts it clear of both cells whatever their order. The markers sit at 2,
   * so the crossings still read the same way round.
   */
  z-index: 1;
  pointer-events: none;

  /*
   * The same treatment the rule above row 01 has: it covers the COPY's half
   * only, solid against the middle and dissolving as it reaches the section's
   * outer edge. The picture's half of the line is open.
   *
   * It covers the COPY's half and stops at the middle. The picture's half of
   * this edge is open, which is the whole of why feature-01.svg has no rule
   * above it and none beside it — the artwork simply ends at the section's
   * measure.
   *
   * Rows 01, 03 and 05 keep their copy on the left, so both rules do too.
   */
  background:
    var(--deep-rule-fade-l) top left / 100% 1px no-repeat,
    var(--deep-rule-fade-l) bottom left / 100% 1px no-repeat;
}

/* 02, 04 and 06 keep theirs on the right. Same shape, turned around. */
.deep-row--mirror::after {
  background:
    var(--deep-rule-fade-r) top left / 100% 1px no-repeat,
    var(--deep-rule-fade-r) bottom left / 100% 1px no-repeat;
}

/*
 * 04 AND 06 OWN THEIR WHOLE BOUNDARY — both halves of it, and solid.
 *
 * These two are closed on their outer side, so their rules run the full width
 * rather than stopping at the middle — the artwork gets the same line the copy
 * does. Each end is then finished the way that end deserves: square into the
 * right-hand edge, dissolving at the open left.
 *
 * Their neighbours stand down accordingly, just below — the boundary each of
 * these draws is a boundary somebody else used to draw half of, and one line
 * painted twice is a heavier line.
 */
.deep-row--4::after,
.deep-row--6::after {
  background:
    var(--deep-rule-closed-r) top left / 100% 1px no-repeat,
    var(--deep-rule-closed-r) bottom left / 100% 1px no-repeat;
}

/* 03 keeps its top and gives up its bottom: 04 draws the 03/04 line now. */
.deep-row--3::after {
  background: var(--deep-rule-fade-l) top left / 100% 1px no-repeat;
}

/* 05 sits between the two of them and draws neither edge. */
.deep-row--5::after {
  background: none;
}

/*
 * A pale wash, and it can be transparent again now.
 *
 * This cell held a flat --wash-base because it had to be the thing covering
 * .deep-blurs: that layer sits at z-index:-1 inside .deep's stacking context, so
 * it paints above the section's background and below every positioned cell, and
 * a half-transparent picture cell let it through under the artwork.
 *
 * No background at all, and that is deliberate.
 *
 * The artwork sits on clean white and nothing else. The white comes from
 * .deep-row, which paints it under this half — not from here, because the reveal
 * animates THIS element: opacity 0 to 1 and a 34px translate, so any surface on
 * it fades in and slides, and the background visibly travels with the row.
 */
.deep-shot {
  display: flex;
  align-items: center;
  justify-content: center;
}

/*
 * THREE KNOBS FOR THE ARTWORK, so a row that sits wrong against the comp is
 * three numbers in one place rather than a new selector.
 *
 * All six exports share the same box: 539 wide, and the painted card inside it
 * sits at L28 T16 R36 B48 every time — the heights differ (377-481) but only
 * because the card is taller, never because the padding moved. So a row that
 * looks off is almost never that row's asset; check the global default first
 * and only reach for these when one row genuinely differs.
 *
 * --shot-w   drawn width. 100% fills the cell between its padding.
 * --shot-x   nudge right, negative for left.
 * --shot-y   nudge down, negative for up.
 *
 * The nudges are translate, not margin: they move the picture without changing
 * the cell's height, so a tweak here can never shift the rule underneath the
 * row or re-centre the copy across from it.
 *
 * To dial one in: open the row in DevTools, put these on .deep-row--N, and
 * arrow the numbers until it sits right. Then paste the same lines into that
 * row's block under "Per-row hooks" — the empty blocks are already there.
 */
.deep-shot img {
  width: var(--shot-w, 100%);
  height: auto;
  translate: var(--shot-x, 0) var(--shot-y, 0);
}

/*
 * The mirrored rows — 02, 04 and 06. The copy stays first in the source on
 * every row, so the heading is still read before the picture it names; only
 * the painting order changes, and only from lg up.
 */

/* ── Per-row hooks ─────────────────────────────────────────── */

/*
 * Every row carries its own class — .deep-row--1 through .deep-row--6 — so a
 * single row can be given a border or a background of its own without a
 * selector that counts positions and without touching the other five.
 *
 * Two ways to use them, and the first is usually the one you want:
 *
 * 1. Override a token on the row. The cells inside already read these, so one
 *    line changes the whole row and keeps it consistent:
 *
 *      .deep-row--3 {
 *        --deep-line:  #D8D8D8;
 *        --deep-rule:  #D8D8D8;
 *      }
 *
 * 2. Write the property straight onto a cell, when a row needs something the
 *    tokens do not cover:
 *
 *      .deep-row--4 .deep-shot { background: #FAFBFF; }
 *
 * The blur washes behind the section are untouched by either — they sit on
 * .deep itself, below all of this, and go on showing through wherever a cell
 * background is transparent or absent.
 */

/* 01 — Web Application Firewall */
.deep-row--1 {
  /* per-row overrides go here */
}

/* 02 — Malware Scanner (mirrored) */
.deep-row--2 {
  /* per-row overrides go here */
}

/* 03 — Login Protection & 2FA */
.deep-row--3 {
  /* per-row overrides go here */
}

/* 04 — Auto-Quarantine (mirrored) */
.deep-row--4 {
  /* per-row overrides go here */
}

/* 05 — Threat Scoring */
.deep-row--5 {
  /* per-row overrides go here */
}

/* 06 — Hardening Rules (mirrored) */
.deep-row--6 {
  /* per-row overrides go here */
}

/* ── Copy ────────────────────────────────────────────────────────────────── */

/*
 * A FLEX ROW OF TWO PARTS, not one line of mixed spans.
 *
 * The number and the name are separate now - .deep-num and .deep-text - and the
 * heading is the row that holds them. The 12px gap replaces the margin the
 * number used to carry, so the space between them is the layout's rather than
 * the number's.
 *
 * align-items: flex-start so a name that wraps to two lines keeps its first line
 * level with the number, instead of the number drifting to the middle of the
 * block beside it.
 *
 * The type stays HERE rather than on .deep-text, and that is deliberate: both
 * children have to be 38px, and stating it once on the parent they both inherit
 * from is one number instead of two that have to be kept equal.
 */
.deep-title {
  display: flex;
  align-items: flex-start;
  gap: 12px;

  /* The cell's flex gap sets the space under this now. */
  margin: 0;
  font-family: "Product Sans";

  /*
   * Through the token, not as a literal.
   *
   * 38px was pasted straight from Figma and --fs-deep-title was sitting three
   * hundred lines above it, stepping down to 22px below lg — wired to nothing.
   * So this heading stayed 38px at every width, and on a phone that made a ROW
   * heading larger than the SECTION heading above it (30px), which is what it
   * looked like: "01. Web Application Firewall" shouting over "22 features.
   * Here's exactly what six of them do."
   *
   * The base is 38 so every width the comp was drawn at is untouched.
   */
  font-size: var(--fs-deep-title);
  font-style: normal;
  font-weight: 400;
  line-height: 110%;
}

/*
 * BOTH PARTS CARRY THE SAME GRADIENT, and each one runs it across its own width
 * rather than sharing one across the line.
 *
 * So the number goes dark to green over its own few characters, and the name
 * does the same over however wide it is. That is what two elements with the same
 * background-clip give you, and it is what the comp draws: two pieces of one
 * idea, each resolving to the brand green at its end.
 *
 * -webkit-background-clip is not a legacy duplicate. Safari and every Chromium
 * build still need the prefixed property for text clipping; the unprefixed one
 * is what newer engines read. Both, in that order.
 *
 * .deep-accent IS GONE with this, and its markup with it. It painted the last
 * word green by hand, which the gradient's own end now does - and left in place
 * it would have made that word INVISIBLE: -webkit-text-fill-color is inherited,
 * so the span would have gone transparent while having no background of its own
 * to show through.
 */
.deep-num,
.deep-text {
  background: linear-gradient(90deg, #151515 21.54%, #008744 100%);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

/*
 * Windows High Contrast throws the author's colours away but keeps
 * -webkit-text-fill-color, so clipped text renders as nothing at all. The
 * gradient goes and the fill comes back to whatever the system chose - the same
 * escape hatch .hero-title, .diff-title, .plans-title and .faq-title carry.
 */
@media (forced-colors: active) {

  .deep-num,
  .deep-text {
    background: none;
    -webkit-text-fill-color: currentColor;
  }
}

/* ── What / How / Why ────────────────────────────────────────────────────── */

.deep-defs {
  margin: 0;
  display: flex;
  width: 472px;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}

.deep-content {
  display: flex;
  padding-bottom: 22px;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px;
  align-self: stretch;
  border-bottom: 1px solid var(--deep-line);
}

/*
 * The last passage closes the block, so it carries neither the rule nor the
 * space the rule needs: a border there draws a line with nothing under it, and
 * the padding would push the copy away from the row's own bottom edge.
 */
.deep-content:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.deep-heading {
  margin: 0;
  color: var(--Grey, #646464);
  font-family: "Product Sans";
  font-size: 18px;
  font-style: normal;
  text-transform: uppercase;
  font-weight: 700;
  line-height: 150%;
}

.deep-paragraph {
  margin: 0px;
  color: var(--Grey, #646464);
  font-family: "Product Sans";
  font-size: 18px;
  font-style: normal;
  font-weight: 400;
  line-height: 150%;
}

/* ── Closing action ──────────────────────────────────────────────────────── */

.deep-cta {
  display: flex;
  justify-content: center;
  margin-top: 56px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   AI CODE GUARDIAN
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The section itself is white; the dark is the card inside it. Keeping them
 * separate is what lets the card have rounded corners and sit inside the page's
 * margins, which a full-bleed dark band could not do.
 */
.guardian {
  padding-block: var(--section-pad-y);
}

/*
 * width:1240 in the comp, max-width here. The card is the widest thing in the
 * section and a fixed width would keep it at 1240 on a 1024 screen, pushing
 * the page sideways — the container it sits in is fluid below 1400, and the
 * card has to be too. At full size the cap is what the comp draws, and margin
 * auto keeps it centred once the container grows past it.
 */
.guardian-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-guardian-card);
  width: 100%;
  /*
   * 1240 is Figma's width for this card inside a 1440 frame — i.e. a 100px page
   * margin, which is Figma's margin for THIS band and nobody else's. Held here
   * it put the card's edge at x=100 while deep, geo and engine sat at x=72 and
   * the dashboard below sat at x=120: five bands, five rails, all of them
   * perfectly centred and none of them agreeing.
   *
   * The container is the rail now. The card fills it, so its edge is the same
   * edge every other section has.
   */
  /* max-width: var(--guardian-card-w); */
  margin-inline: auto;
  padding: var(--guardian-card-pad);
  border-radius: var(--guardian-card-radius);
  background: var(--guardian-card-bg);
}

/*
 * align-items:flex-start above sizes each child to its own content, which is
 * right for the head — the headline sets its own measure — and wrong for the
 * row, which would collapse its two columns to the width of their text. This
 * is Figma's 'fill container' on that one child.
 */
.guardian-row {
  align-self: stretch;
}

/* ── Head ────────────────────────────────────────────────────────────────── */

/*
 * The headline sits in its own row now, so its measure can be set in column
 * classes against the comp rather than only through the token below.
 *
 * Same reason .guardian-row needs it: align-items:flex-start on the card sizes
 * every child to its own content, and a row left at that width collapses its
 * columns to the width of their text.
 *
 * It spans the full 12 by default, which renders exactly as it did before the
 * row existed — a .row's -12px margin and its column's 12px padding cancel, so
 * the headline still starts flush with the card's padding edge and flush with
 * the lede underneath it.
 *
 * NOTE: --guardian-title-measure still caps the headline at 730px. Narrowing
 * the column past 730 takes effect; widening it past 730 does nothing until
 * that token is raised or dropped.
 */
.guardian-head-row {
  align-self: stretch;
}

.guardian-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-guardian-head);
}

.guardian-title {
  margin: 0;
  max-width: var(--guardian-title-measure);
  font-family: var(--font);
  font-size: var(--fs-guardian-title);
  font-weight: var(--fw-regular);
  line-height: var(--lh-guardian-title);
  color: var(--guardian-ink);
}

/* ── Copy column ─────────────────────────────────────────────────────────── */

.guardian-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-guardian-copy);
}

/*
 * Through the token, which until now was dead.
 *
 * --fs-guardian-lede is declared 18px at the top of the file and 16px in the
 * 575 block, and this rule then pasted a literal 20px over both — so the lede
 * rendered 20px at every width, neither token value was ever read, and this was
 * the one single-tier section lede on the page that did not match the others.
 * Same fault .faq-a already carries a note about.
 *
 * 18 at desktop is what every other section lede is; 16 on a phone is what the
 * 575 block was always asking for and never got.
 */
.guardian-lede {
  margin: 0;
  color: #FFF;
  font-family: "Product Sans";
  font-size: 22px;
  font-style: normal;
  font-weight: 400;
  line-height: 150%;
}

/* ── Claims ──────────────────────────────────────────────────────────────── */

.guardian-claims {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/*
 * The tick is drawn here rather than typed into the markup. A literal ✓ in the
 * text is announced before every item — "check mark, trained on real WordPress
 * malware" — which is three words of noise per claim for something that carries
 * no meaning a list does not already carry.
 *
 * Indented with padding and pulled back by the same amount, so a claim that
 * wraps lines up under its own first word rather than under the tick.
 */
.guardian-claims li {
  position: relative;
  padding-left: 26px;
  color: #FFF;
  font-family: "Product Sans";
  font-size: 18px;
  font-style: normal;
  font-weight: 400;
  line-height: 145%;
}

/*
 * The comp's tick, masked so it takes its colour from --green.
 *
 * It was two borders on a rotated box — a serviceable check, but a geometric one
 * with square ends, and the comp draws a stroke that tapers. No transform now,
 * so nothing here is rotated into place.
 *
 * Centred on the first line rather than nudged with a round em value: the line
 * box is 1.45em and the artwork is 12px, so half the difference puts it on the
 * optical middle at any font size this list is ever set in.
 */
.guardian-claims li::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc((1.45em - 12px) / 2);
  width: 11px;
  height: 12px;
  background-color: var(--green);
  -webkit-mask: var(--guardian-tick) center / contain no-repeat;
  mask: var(--guardian-tick) center / contain no-repeat;
}

/* ── Call to action ──────────────────────────────────────────────────────── */

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.guardian-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 22px;
  border-radius: 8px;
  background: var(--green);
  color: var(--white);
  font-family: var(--font-ui);
  font-size: var(--fs-btn);
  font-weight: var(--fw-medium);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color .18s ease;
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.guardian-cta:hover {
  background: var(--green-hover);
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.guardian-cta:hover .btn-icon {
  transform: translateX(3px);
}
*/

/* ── Code panel ──────────────────────────────────────────────────────────── */

.guardian-visual {
  display: flex;
  justify-content: flex-end;
}

/*
 * 450 wide in a 540 column, sitting against the right of it — so its right
 * edge lines up with the card's padding and the gap opens between it and the
 * copy rather than at the card's edge. Filling the column would close that gap
 * and leave the panel reading as a second column of text.
 */
.guardian-panel {
  width: 100%;
  max-width: var(--guardian-panel-w);
  margin: 0;
  border: 1px solid var(--guardian-panel-line);
  border-radius: var(--guardian-panel-radius);
  background: var(--guardian-panel-bg);
  overflow: hidden;
}

/* figcaption, so the file name is the panel's label rather than another line of
   code that happens to sit above the rest. */
.guardian-panel-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--guardian-panel-line);
  background: var(--guardian-bar-bg);
  font-family: var(--font-ui);
  font-size: var(--fs-guardian-code);
  color: var(--guardian-ink-faint);
}

.guardian-panel-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--guardian-ink-faint);
  flex-shrink: 0;
}

/*
 * Albert Sans, not a monospace stack — the comp draws this panel in the page's
 * own accent face rather than in code type, so it reads as a picture OF code
 * rather than as a code block the visitor could edit.
 *
 * The face, size and weight are set once here and inherited by every line and
 * every token inside. Figma repeats them on each .tok-* layer because a design
 * tool has no inheritance; the browser does, and four copies of the same three
 * declarations is four places to miss when one of them changes.
 */
.guardian-panel-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  background: #182234;
  padding: 24px;
  font-family: var(--font-accent);
  font-size: var(--fs-guardian-code);
  font-weight: 300;
  line-height: normal;
  color: var(--guardian-ink-soft);
}

/*
 * Each line is its own block, and the block is only as wide as its own text —
 * align-items:flex-start on the body above.
 *
 * That is the change the comp asks for. The tinted row used to run the full
 * width of the panel, which needed the side padding to live on the lines
 * rather than on the body; now it hugs the line it flags, so the padding sits
 * on the body where it belongs and the lines carry none.
 */
.guardian-line {
  display: block;
  white-space: pre;
  overflow-x: auto;
}

/*
 * The flagged row gets the padding and the corners the plain lines do not, so
 * the tint reads as a box drawn around that line rather than as a stripe
 * behind it.
 */
.guardian-line--flagged {
  padding: 6px 10px;
  border-radius: 8px;
  background: var(--guardian-flagged-bg);
}

.tok-fn {
  color: var(--guardian-tok-fn);
}

.tok-kw {
  color: var(--guardian-tok-kw);
}

.tok-str {
  color: var(--guardian-tok-str);
}

.tok-bad {
  color: var(--guardian-tok-bad);
}

/*
 * The flag. Prose, not code — so it is a paragraph in the page's own UI face
 * rather than the monospace around it, which is also how the product draws it.
 */
.guardian-flag {
  display: flex;
  align-items: center;
  gap: 8px;
  /*
   * No margin. It used to inset itself from panel edges it no longer touches —
   * the body's 24px does that now, and the flag sizes to its own content like
   * every line above it.
   */
  margin: 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--guardian-flag-bg);
  font-family: var(--font-ui);
  font-size: var(--fs-guardian-code);
  line-height: 1.4;
  color: var(--guardian-flag-ink);
}

.guardian-flag-icon {
  display: inline-flex;
  flex-shrink: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PRODUCT IN ACTION
   ═══════════════════════════════════════════════════════════════════════════ */

.action {
  position: relative;
  isolation: isolate;
  padding-block: var(--section-pad-y);
  background: var(--wash-base);

  /* The wash is wider than a narrow viewport and its blur runs past the top of
     the section. Without this it would push the page sideways and lengthen it. */
  overflow: hidden;
}

/*
 * THE BLUR IS ALREADY IN THE FILE. Do not add filter: blur() to this, and do
 * not point it back at the .svg — see the note above .deep-blurs for why the
 * washes are baked to .webp and how to regenerate them.
 *
 * Figma reports this layer as 1200 × 513.749 and the file is 1440 × 951. Both
 * are right: 1200 × 513.749 is the box the four shapes occupy before they are
 * blurred, and 1440 × 951 is what they occupy after — three are blurred at
 * stdDeviation 200 and one at 150, and a Gaussian blur of sigma spreads about
 * 2 sigma past the shape every way. Figma exports the whole thing, spread
 * included, because that is what has to be painted.
 *
 * So the file is sized at 1440, not at 1200. Forcing it to 1200 would shrink
 * the colour inside it to about 950 and pull the composition in with it.
 *
 * Centred and held at the comps width rather than stretched to the viewport:
 * it is soft-edged and already transparent well before its box ends, so a wider
 * screen shows no seam, while stretching would grow the glow with the window
 * and stop it matching the 1200 dashboard it sits behind.
 *
 * max-width:none because the global img rule caps images at 100%, which here
 * would shrink the wash on any viewport under 1440.
 */
.action-wash {
  position: absolute;
  top: var(--action-wash-top);
  left: 50%;
  transform: translateX(-50%);
  z-index: -1;
  width: var(--action-wash-w);
  max-width: none;
  height: auto;
  pointer-events: none;
}

.action-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-action-head);
  margin-bottom: 56px;
  text-align: center;
}

.action-lede {
  margin: 0;
  color: #000;
  text-align: center;
  font-family: "Product Sans";
  /* Token, not a literal — same dead-token fix as .deep-lede above. */
  font-size: var(--fs-action-lede);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh-lede);
}

/* ── The dashboard ───────────────────────────────────────────────────────── */

/*
 * 1200 wide, which is its own measurement — wider than the hero's shot and
 * narrower than the page container, so it gets a token rather than borrowing
 * either. Centred, and capped rather than fixed so it comes in with the page
 * below 1400 instead of pushing it sideways.
 */
.dash-wrap {
  width: 100%;
  /* 1200 was Figma's width in a 1440 frame — a 120px page margin that put this
     band 48px inside the sections above and below it. Same reason as
     .guardian-card: the container owns the rail, this fills it. */
  /* max-width: var(--dash-w); */
  margin-inline: auto;
}

/* Sits above the card and to its left edge, labelling it the way a legend
   labels a figure rather than being a heading of its own. */
.dash-label {
  margin: 0 0 12px;
  font-family: var(--font);
  font-size: var(--fs-dash-title);
  font-weight: var(--fw-bold);
  color: var(--green);
}

.dash {
  border: 1px solid var(--dash-border);
  border-radius: var(--dash-radius);
  background: var(--dash-bg);

  /* The tab strip's tint has to stop at the card's rounded corner rather than
     square it off. */
  overflow: hidden;
}

/* ── Tabs ────────────────────────────────────────────────────────────────── */

/*
 * Scrollable rather than wrapping. Five tabs do not fit on a phone, and a tab
 * strip that wraps to two lines stops reading as a strip — the row of tabs is
 * the thing that says "these are alternatives to each other".
 */
.dash-tabs {
  display: flex;
  padding: 18px 24px 0 24px;
  align-items: flex-start;
  gap: var(--gap-dash-tabs);
  margin: 0px;

  align-self: stretch;
  border-radius: 16px 16px 0 0;
  border-bottom: 1px solid var(--color-gray-200, #E5E7EB);
  background: var(--color-gray-50, #F9FAFB);
}

.dash-tabs::-webkit-scrollbar {
  display: none;
}

/*
 * The underline is a transparent border in the resting state, not no border —
 * so the open tab does not become 2px taller than the other four and shunt the
 * strip down when it changes.
 */
/*
 * No padding across, only down. In the comp the underline is exactly as wide
 * as the word above it — 66px under a 64px "Overview" — so the tab box is the
 * text box, and the space between tabs is the strip's gap rather than padding
 * inside each one. Padding here would widen every underline by twice it.
 */
.dash-tab {
  padding: 0 0 12px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  /*
   * Inter, not the page's own face. This whole block is a picture of the
   * product's interface, and the comp draws it in the interface's type rather
   * than in the type the page around it is set in.
   */
  font-family: var(--font-dash);
  font-size: 15px;
  line-height: normal;
  /* Grey and medium at rest; the open tab takes the ink and the weight. */
  font-weight: var(--fw-medium);
  color: var(--color-gray-500, #6B7280);
  white-space: nowrap;
  cursor: pointer;

  transition: color .15s ease;
}

/*
 * Every tab is already as wide as its own label IN BOLD, whether or not it is
 * the open one.
 *
 * Without this the strip moves every time someone changes tab: bold text is
 * wider than medium, so the tab that opens grows and shoves the ones to its
 * right along — and the underline, which is exactly as wide as the word above
 * it, slides with them. Five tabs, five different jumps.
 *
 * The reservation is a hidden copy of the label at the bold weight. It has no
 * height, so it costs the strip nothing vertically; it only ever sets a floor
 * under the width, which the real text then sits inside at whichever weight
 * the state calls for.
 *
 * content: attr(...) / "" — the empty string after the slash is the generated
 * text's alternative text, which is what keeps a screen reader from announcing
 * every tab's name twice. A browser too old to understand that syntax drops
 * the whole declaration and simply gets the old shifting back; nothing breaks.
 */
.dash-tab::after {
  content: attr(data-label) / "";
  display: block;
  height: 0;
  overflow: hidden;
  font-weight: var(--fw-bold);
  visibility: hidden;
  pointer-events: none;
}

.dash-tab:hover {
  color: var(--dash-tab-ink-active);
}

.dash-tab.is-active {
  font-weight: var(--fw-bold);
  color: var(--dash-tab-ink-active);
  border-bottom-color: var(--dash-tab-underline);
}

/* ── Panels ──────────────────────────────────────────────────────────────── */

/*
 * All five panels share one grid cell, so the card is always as tall as the
 * tallest of them and switching tabs changes nothing about the page's layout.
 * Overview has five rows and Malware Scan has three; sized individually, the
 * card would jump every time someone moved between them.
 *
 * visibility rather than display: it keeps the cell sized and still takes the
 * closed panels out of the accessibility tree, so a screen reader is read the
 * open one rather than all five in a row.
 */
.dash-body {
  display: grid;
  padding: var(--dash-pad);
}

.dash-panel {
  grid-area: 1 / 1;
  visibility: hidden;
  opacity: 0;
  transition: opacity .2s ease, visibility 0s linear .2s;
}

.dash-panel.is-active {
  visibility: visible;
  opacity: 1;
  transition: opacity .2s ease, visibility 0s;
}

.dash-panel:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 4px;
  border-radius: 6px;
}

/* ── Panel head ──────────────────────────────────────────────────────────── */

.dash-panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
}

/* Groups the heading and its line so the pair sits as one flex child against
   the pill. min-width:0 because a flex item will not shrink below its content
   otherwise, and a long subtitle would push the pill off the card. */
.dash-panel-titles {
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 7px 14px;
  align-items: flex-start;
  gap: 6px;
}

.dash-title {
  margin: 0 0 4px;
  color: var(--color-gray-900, #111827);
  font-size: 20px;
  font-style: normal;
  font-weight: 700;
  line-height: normal;
}

.dash-sub {
  margin: 0;
  color: var(--color-gray-500, #6B7280);
  font-size: 14px;
  font-style: normal;
  font-weight: 400;
  line-height: normal;
}

.dash-pill {
  display: flex;
  padding: 7px 14px;
  align-items: center;
  gap: 6px;
  margin: 0;
  border-radius: 100px;
  border-radius: 999px;
  background: var(--color-green-50, #ECFDF5);
  color: var(--color-green-700, #15803D);
  font-family: Inter;
  font-size: 13px;
  font-style: normal;
  font-weight: 600;
  line-height: normal;
  white-space: nowrap;
}

.dash-pill-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* ── Rows ────────────────────────────────────────────────────────────────── */

/*
 * A list, because that is what it is: five statuses of equal standing. The
 * rules between them are borders on the rows rather than a divider element,
 * and the first row carries none so the box's own edge is not doubled.
 */
.dash-rows {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--dash-border);
  border-radius: 10px;
  overflow: hidden;
}

/*
 * Three columns: the name takes the slack, the value and the timestamp take
 * only what they need. 1fr on the name is what keeps the value column from
 * moving between rows whose labels are different lengths.
 */
.dash-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 20px;
  min-height: var(--dash-row-h);
  padding: 12px 20px;
  font-size: var(--fs-dash-row);
}

.dash-row+.dash-row {
  border-top: 1px solid var(--dash-border);
}

.dash-row-name {
  color: var(--color-gray-900, #111827);
  font-family: Inter;
  font-size: 13px;
  font-style: normal;
  font-weight: 400;
  line-height: normal;
}

.dash-row-value {
  color: var(--color-green-700, #15803D);
  text-align: right;
  font-family: Inter;
  font-size: 13px;
  font-style: normal;
  font-weight: 600;
  line-height: normal;
}

.dash-row-value--bad {
  color: var(--dash-bad-ink);
}

/*
 * Green says resolved, red says stopped. A few rows are neither — a scan hit
 * somebody has already looked at, a report already sent — and those are stated
 * in the plain ink of the page rather than being pushed into one of the two
 * colours that carry a verdict.
 */
.dash-row-value--muted {
  color: var(--dash-neutral-ink);
}

/* Right-aligned and given a floor, so the timestamps line up as a column
   instead of tracking the width of the value beside them. */
.dash-row-time {
  min-width: 72px;
  font-size: var(--fs-dash-sub);
  color: var(--dash-time-ink);
  text-align: right;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LIVE THREAT INTELLIGENCE
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Dark edge to edge, which is why the colour is on the section and not on a
 * card inside it. The AI Code Guardian block above does the opposite, and the
 * difference is deliberate in the comp: that one is an object sitting on the
 * page, this one is a change of ground.
 */
.geo {
  padding-block: var(--section-pad-y);
  background: #050A14;
  color: var(--geo-ink);
}

/* ── Head ────────────────────────────────────────────────────────────────── */

.geo-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-section-head);
  margin-bottom: var(--gap-geo-block);
  text-align: center;
}

.geo-eyebrow {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--fs-geo-eyebrow);
  font-weight: var(--fw-medium);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--geo-ink);
}

/*
 * Not .section-title: that one is black on white and this section inverts.
 * The measure holds the break after the first sentence, which is where the comp
 * puts it — wider and "Shut" joins line one, narrower and it becomes three.
 */
.geo-title {
  margin: 0;
  max-width: var(--geo-title-measure);
  font-family: var(--font);
  font-size: var(--fs-geo-title);
  font-weight: var(--fw-regular);
  line-height: var(--lh-geo-title);
  color: var(--geo-ink);
}

/*
 * One sentence per line, the way the comp breaks it.
 *
 * Left as one run of text the line lands mid-sentence — "…come from. Shut the"
 * and then "door on any country in a click." — which reads as a heading that is
 * off centre even though it is not: measured against the render, the heading,
 * the map and the three definitions below it agree on their centre to within
 * 8px. It is the ragged split that looks wrong, not the position.
 *
 * inline-block rather than a <br>. A <br> would hold the break at every width,
 * including a phone where each sentence needs the whole screen and then some;
 * this keeps each sentence together while there is room for it and lets one
 * wrap inside itself when there is not.
 */
.geo-title-part {
  display: inline-block;
}

/* ── Map ─────────────────────────────────────────────────────────────────── */

/* Labels the map the way "Your Site" labels the dashboard: a legend above it,
   not a heading in the document's outline. */
.geo-label {
  margin: 0 0 24px;
  font-family: var(--font-ui);
  font-size: var(--fs-geo-label);
  font-weight: var(--fw-regular);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--geo-ink);
}

/*
 * The map is a live SVG, so every country is addressable and its state is a
 * class rather than a colour baked into a file. Changing which countries are
 * blocked is a class swap in the markup; when the data goes live it becomes the
 * same class swap from a script, with nothing here to change.
 */
.geo-map {
  margin: 0 0 var(--gap-geo-block);
}

/* The positioning context the pins are placed against, and the box that gives
   the map its height before the paths are laid out. */
.geo-map-inner {
  position: relative;
  aspect-ratio: var(--geo-map-ratio);
}

.geo-map-svg {
  display: block;
  width: 100%;
  height: 100%;
}

/*
 * The comp insets the map from the container; the legend goes with it, because
 * the legend's whole job is to sit at the map's left edge.
 *
 * Done in CSS rather than by wrapping both in a row and a col like the heading
 * and the trio. The map is four hundred lines of inline SVG, and a wrapper
 * means re-indenting every one of them — a whitespace diff that would bury the
 * two lines that actually change anything. The result is the same box.
 *
 * From lg only, and 992 is not an arbitrary choice: it is where the trio stops
 * being three columns and the legend stops being flush left. Below it the
 * section is one centred stack and the map should have the whole width.
 */
@media (min-width: 992px) {

  .geo-label,
  .geo-map {
    max-width: var(--geo-map-rail);
    /* After the margin shorthand each already sets, so the bottom gaps stand
       and only the sides become auto. */
    margin-inline: auto;
  }
}

/* ── Countries ───────────────────────────────────────────────────────────── */

/*
 * The hairline is the section's own background, not a grey: it separates
 * neighbours by cutting the ground between them rather than by drawing a line
 * on top, which is what keeps a continent reading as one mass at this size.
 *
 * vector-effect keeps that hairline one device pixel at any width. Without it
 * the stroke scales with the viewBox, and on a wide screen the borders thicken
 * into a visible mesh over the whole map.
 */
.geo-country {
  fill: var(--geo-country);
  stroke: var(--geo-bg);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.geo-country.is-customer {
  fill: var(--geo-customer);
}

.geo-country.is-blocked {
  fill: var(--geo-blocked);
}

/*
 * Hover lights the country under the cursor. The transition is on fill alone
 * — it is a paint, so it costs no layout, which matters when the thing being
 * hovered is one of 176 paths sharing a canvas.
 *
 * No pointer cursor: nothing happens when you click yet, and a hand promises
 * that something will.
 */
.geo-country {
  transition: fill .18s ease;
}

.geo-country:hover {
  fill: var(--geo-country-hover);
}

.geo-country.is-customer:hover {
  fill: var(--geo-customer-hover);
}

.geo-country.is-blocked:hover {
  fill: var(--geo-blocked-hover);
}

/* ── Pins ────────────────────────────────────────────────────────────────── */

.geo-pins {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  pointer-events: none;
}

/*
 * Placed by percentage, so the pin holds its spot on the country as the map
 * scales. The translate is what centres the dot on its coordinate instead of
 * hanging it down and to the right of it.
 */
.geo-pin {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--geo-pin-size);
  height: var(--geo-pin-size);
  transform: translate(-50%, -50%);
  border-radius: 50%;
}

/* All three kinds name their own colour rather than two of them overriding a
   default, so the markup reads the same for every pin and none of them is the
   one that happens to inherit. */
.geo-pin--ok {
  background: var(--geo-pin-ok);
}

/*
 * The ring each pin sends out. One ping every --geo-pulse-dur, expanding from
 * the dot and fading as it goes, which is what makes the map read as reporting
 * rather than as a drawing of some pins.
 *
 * background:inherit so the ring is whatever colour its pin is — green, red or
 * purple — without the colour being written three more times here.
 *
 * .geo-pin carries a transform, which makes it a stacking context, so z-index
 * -1 puts the ring behind the dot without escaping the pin. Painted in front it
 * would wash the dot out for the first frames of every cycle.
 *
 * Only scale and opacity animate. Both are compositor properties, so ten rings
 * running forever cost no layout and no repaint — which is the difference
 * between a detail and a section that makes the page feel heavy while it sits
 * on screen.
 *
 * The delay is per pin, set in the markup, so the ten come round in turn.
 * Without it they fire in unison and the map blinks rather than ticks.
 */
@keyframes geo-ping {
  0% {
    transform: scale(1);
    opacity: .55;
  }

  70% {
    transform: scale(3.4);
    opacity: 0;
  }

  100% {
    transform: scale(3.4);
    opacity: 0;
  }
}

.geo-pin::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: 50%;
  background: inherit;
  animation: geo-ping var(--geo-pulse-dur) cubic-bezier(0, 0, .2, 1) var(--d, 0s) infinite;
}

.geo-pin--bad {
  background: var(--geo-pin-bad);
}

.geo-pin--count {
  background: var(--geo-pin-count);
}

/*
 * The label sits beside its dot and is taken out of the flow, so a long one
 * cannot push the dot off its coordinate. It is the dot that has to stay
 * accurate; the label only has to be next to it.
 */
.geo-pin-label {
  position: absolute;
  left: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-ui);
  font-size: var(--fs-geo-pin);
  font-weight: var(--fw-regular);
  line-height: 1;
  color: var(--geo-ink);
  white-space: nowrap;
}

/*
 * Two of the five labels have a short form, and only those two need one.
 *
 * Measured against a 360px screen, where the map is 336 x 151: "Blocked",
 * "Blocked" and "Counting" all clear their neighbours, but "2.4k customers" is
 * 84px and runs straight over the United Kingdom's dot, and "Australia 1k
 * customers" is 132px — most of the width of the map.
 *
 * display:none on the form not in use, rather than hiding one visually: that
 * keeps it out of the accessibility tree too, so a screen reader is read one
 * label per pin and not both.
 */
.geo-pin-short {
  display: none;
}

/* One label sits above its dot rather than beside it — there is no room to its
   right at the edge of the map. */
.geo-pin--above .geo-pin-label {
  left: 50%;
  top: auto;
  bottom: calc(100% + 10px);
  transform: translateX(-50%);
}

/*
 * Present for a screen reader, absent for everyone else. clip-path rather than
 * display:none or visibility:hidden, both of which would take it out of the
 * accessibility tree along with everything else — which is the opposite of what
 * this is for.
 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── What / How / Why it's safe ──────────────────────────────────────────── */

/*
 * A Bootstrap .row and nothing else, and each of the four declarations that
 * were here is left off for a reason:
 *
 *   display:flex          .row already is one.
 *
 *   align-items:center    This is what broke the rules. The default is stretch,
 *                         which makes all three columns as tall as the tallest —
 *                         and the rules are border-lefts ON those columns, so
 *                         stretch is what gives them one shared height. Centred,
 *                         each rule was only as tall as its own passage and the
 *                         three sat at three different heights.
 *
 *   justify-content       Three col-lg-4 come to exactly 100%; there is no free
 *                         space left to distribute.
 *
 *   gap:20px              The columns come to 100% between them, so gap here puts
 *                         the row wider than the column holding it. The space
 *                         between the passages is the columns own gutter, inside
 *                         them, where it cannot push the row out.
 *
 * row-gap stays: it is the space between the three when they stack.
 */
.geo-defs {
  margin: 0;
  row-gap: 40px;
}

/*
 * The rules sit between the columns, so they are drawn on every column but the
 * first — which is what keeps them between things rather than around them, and
 * means a fourth column would be ruled correctly without anyone counting.
 *
 * They disappear with the columns at the stacking breakpoint: a rule down the
 * left of a full-width block is not a divider, it is a quote mark.
 */
.geo-def {
  text-align: center;
}

/*
 * BOTH RULES ON THE MIDDLE PASSAGE, and only there.
 *
 * It was a border-left on every passage but the first, which draws the same two
 * lines — but written that way the 2|3 boundary belongs to the third passage
 * while the 1|2 boundary belongs to the second, and the pair are only ever in
 * step by coincidence.
 *
 * Here the middle one owns both of its edges. Note what is NOT done: a
 * border-right on the first as well would land on the same pixel as this one's
 * border-left, because the columns abut — two 1px lines stacked into a 2px
 * divider on one side and not the other.
 */
.geo-def:nth-child(2) {
  border-inline: 1px solid var(--geo-rule);
}

/*
 * Paragraphs with classes, not a description list. Both labels and passages are
 * <p>, which keeps WHAT / HOW / WHY out of the document's heading outline —
 * eighteen of them across this page would otherwise sit in the list a screen
 * reader offers for jumping around it, and none of them names a section.
 *
 * Prefixed .geo- like every other class in this file. A bare .heading would be
 * claimed by the first other section that wanted one.
 */
.geo-heading {
  margin-bottom: 12px;
  color: var(--geo-def-ink);
  text-align: center;
  font-family: var(--font);
  font-size: var(--fs-geo-def);
  font-style: normal;
  font-weight: 700;
  line-height: var(--lh-geo-heading);

  /* The comp types these in capitals; done here instead so the text a screen
     reader is handed is still "What", not four spelled-out letters. Identical
     on screen either way. */
  text-transform: uppercase;
}

.geo-paragraph {
  margin: 0;
  color: var(--geo-def-ink);
  text-align: center;
  font-family: var(--font);
  font-size: var(--fs-geo-def);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh-geo-paragraph);
}

/*
/*
 * Once the section stacks, EVERYTHING in it is centred — and .geo-label was
 * the one thing that was not.
 *
 * On a desktop it is right to be left: it labels the map the way "Your Site"
 * labels the dashboard, and it sits at the left edge of a map that runs the
 * full width under it. Stacked, there is no full-width map to sit against any
 * more — there is a centred title above it and a centred What/How/Why trio
 * below it, and one flush-left line between them reads as a mistake rather
 * than as a legend.
 *
 * 991.98 because that is where the trio stops being three columns, which is
 * the same moment the section stops being a row of things and becomes one
 * centred stack.
 */
@media (max-width: 991.98px) {
  :root {
    --gap-geo-block: 44px;
  }

  .geo-label {
    text-align: center;
  }

  /*
   * These three moved out of a max-width:991px block, 28/09/2026.
   *
   * 991.98 is where the columns actually stack — .col-lg-3 stops applying at
   * 992. At a flat 991 there was a pixel of width where the three passages were
   * ALREADY stacked and full-width but this rule had not fired yet, so the
   * middle one kept the rules down both its sides: two vertical lines through a
   * single-column list. The label above it centred at 991.98 and the margins
   * closed at 991, so the section changed shape twice on the way down.
   */
  .geo-def:nth-child(2) {
    /* border-inline, to match what the rule above sets — border-left alone would
       leave the right-hand one standing once the passages stack. */
    border-inline: 0;
  }
}

/* The step from the heading to the legend, and from the map to the passages.
   It was 64 with one step to 44 and nothing below that, so a phone kept the
   tablet's spacing. */
@media (max-width: 575.98px) {
  :root {
    --gap-geo-block: 32px;
  }

  /*
   * THE SENTENCES STOP BEING BLOCKS AND GO BACK TO BEING TEXT.
   *
   * inline-block keeps each sentence whole, which is the point of it from lg up:
   * one sentence, one line, the way the comp breaks it. At this width neither
   * sentence fits on a line of its own, so each one wrapped INSIDE itself and
   * the first left "from." stranded on a line by itself.
   *
   * Inline, the two run together as one passage and the break can fall wherever
   * the measure wants it. balance then evens the lines out, so the block reads
   * as a shape rather than as three full lines and a short one — which is what
   * the inline-block was trying to buy in the first place, by a means that only
   * works while a sentence fits.
   *
   * A browser too old for text-wrap: balance simply wraps normally, which is
   * still the orphan gone.
   */
  .geo-title-part {
    display: inline;
  }

}

/* ═══════════════════════════════════════════════════════════════════════════
   WHY LEADX SECURITY
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The tint is on the section, so it runs the full width of the window the way
 * the comp draws it. The content inside sits in the same container-aligned box
 * every other section below the hero uses, so the headline and the card row line
 * up with the sections above rather than with the band's own edges.
 *
 * That split is the point: ground full-bleed, content on the page's rail. One
 * element cannot do both — it forces a choice between a band that stops short of
 * the window and content that lines up with nothing.
 */
.diff {
  display: flex;

  /*
   * padding-block, not the flat `padding: 80px` Figma reports.
   *
   * Figma measures that 80 from the edge of a 1440 frame, so the number already
   * contains the page margin. Applied here it is charged twice: the section pays
   * 80 a side, then the .container inside it pays the page margin again, and the
   * headline and card row land 50px inside every other section on the page.
   * Measured at a 1200 viewport: this band 1016 wide where deep, guardian, action
   * and geo were all 1116. That step in and out at every band is what read as
   * "sections not centred".
   *
   * The horizontal margin belongs to the container — the one rail the whole page
   * shares. The 80 vertical is real and stays.
   */
  /*
   * Through the token, not as a literal.
   *
   * --diff-panel-pad-y was already stepping down — 48 below lg, 36 on a phone
   * — and wired to nothing, because this rule carried Figma's 80 as a number.
   * So the band kept 80px above and below at every width while every section
   * around it dropped to 64, and on a phone that reads as one section padded
   * differently from the rest of the page.
   *
   * The token's base becomes 80, the comp's own value, so nothing the comp was
   * drawn at moves.
   */
  padding-block: var(--diff-panel-pad-y);
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 56px;
  align-self: stretch;
  border-radius: 52px;
  background: #F8F9FB;
}

/* ── Head ────────────────────────────────────────────────────────────────── */

.diff-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-diff-head);
  margin-bottom: var(--gap-diff-cards);
  text-align: center;
}

/*
 * The page's ink gradient clipped to the glyphs, the same treatment the hero
 * headline uses — dark where the sentence starts, lighter where it ends. The
 * measure is what holds the break after "Dashboards."; without it the two lines
 * fall in the wrong places at almost every width.
 *
 * box-decoration-break so each line gets its own copy of the gradient. Without
 * it the browser paints one strip across both lines and slices it, which hands
 * line two the faded half and leaves "One Number You Can Trust." grey.
 */
.diff-title {
  margin: 0;
  max-width: var(--diff-title-measure);
  font-family: var(--font);
  font-size: var(--fs-diff-title);
  font-weight: var(--fw-regular);
  line-height: var(--lh-diff-title);
  color: var(--text-heading);
  background: var(--title-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

@media (forced-colors: active) {
  .diff-title {
    background: none;
    -webkit-text-fill-color: currentColor;
  }
}

/* ── Cards ───────────────────────────────────────────────────────────────── */

/*
 * 26 between cards, not Bootstrap's 24. Set through --bs-gutter-x rather than a
 * gap of our own, because that is the variable the columns already pay their
 * padding out of — change it here and the column padding, the row's negative
 * margins and the space between cards all move together. A separate gap would
 * stack on top of the gutter and give 50.
 *
 * Measured: the comp puts four 300px cards in a 1278 row, and 4×300 + 3×26 is
 * exactly 1278.
 */
.diff-cards {
  --bs-gutter-x: var(--gap-diff-card);

  /*
   * margin-block, not a flat `margin: 0`. This is a <ul>, so the zero is there to
   * drop the list's default top and bottom margin — but written flat it also wiped
   * the negative side margin every Bootstrap .row carries, and that margin is what
   * pulls the columns back out so their CONTENT lines up with the container.
   *
   * Without it the first card's white edge started at x=85 while the headline
   * above it started at 72 — a 13px step, symmetric so the row still looked
   * centred, just never flush with anything.
   */
  margin-block: 0;
  margin-inline: calc(var(--bs-gutter-x) * -.5);
  padding: 0;
  list-style: none;
  row-gap: var(--gap-diff-card);
}

/*
 * A box inside the column rather than the column itself. The column keeps
 * Bootstrap's half-gutter of padding, which is what separates one card from the
 * next, and the card fills what is left.
 *
 * It was the column for a while, with the gutter rebuilt as a margin and
 * flex:1 1 0 to make the four share the row. That flex beat .col-12's own
 * width on specificity, so the cards stayed four abreast at every width and
 * never stacked on a phone — the column classes were in the markup doing
 * nothing. Letting Bootstrap own the columns costs one wrapper and gets the
 * breakpoints back.
 *
 * height:100% is what keeps the four level: the row stretches its columns, and
 * this passes that height down to the card inside.
 */
.diff-card {
  display: flex;

  /* Same again: --diff-card-pad steps to 24 on a phone and was never read,
     because this said 32. 32 stays as the token's base. */
  padding: var(--diff-card-pad);
  flex-direction: column;
  align-items: flex-start;
  gap: 22px;

  /*
   * This is the line that levels the four, and it is the one Figma does not
   * give you. The row stretches its columns to the tallest of them, so every
   * <li> is already the same height; height:100% is what passes that down to the
   * card sitting inside it. Without it each card is only as tall as its own
   * copy, and the one with four lines of body text hangs below the other three.
   *
   * flex:1 0 0 and align-self:stretch below are Figma's auto-layout, carried
   * over from the paste. Both are inert here — they only mean something to a
   * flex ITEM, and this card is a block child of a block <li>. Left in place
   * rather than stripped: they cost nothing, and they are what the comp says.
   */
  height: 100%;
  flex: 1 0 0;
  align-self: stretch;

  border-radius: 24px;
  border: 1px solid #EFD8FF;
  background: #FFF;
}

/* ── Icon tile ───────────────────────────────────────────────────────────── */

/*
 * A spacer, nothing more. The tile is the exported SVG — it carries its own
 * rounded background, its own tint and its own ink — so there is no fill, no
 * radius and no per-card modifier class to set here.
 *
 * The four tint/ink pairs that used to live in this file are inside the four
 * exports now. That is the trade: one place to change a colour becomes four, and
 * in return the tile is what the designer exported rather than an approximation
 * of it kept up to date by hand.
 *
 * The card already spaces its children with its own flex gap, so this carries no
 * margin either.
 */
.diff-icon {
  display: block;
  margin: 0;
  line-height: 0;
  flex-shrink: 0;
}

/* The export states 70 square in its own attributes; this only stops anything
   else on the page from having an opinion about it. */
.diff-icon svg {
  display: block;
  border-radius: 20px;
  width: var(--diff-icon-size);
  height: var(--diff-icon-size);
}

/* ── Card copy ───────────────────────────────────────────────────────────── */

/*
 * The break in each heading is typed as a <br>, because the comp breaks all four
 * at the same point and a measure cannot guarantee that across four different
 * strings. It is removed below the breakpoint, where the column is a different
 * width and the comp's break no longer applies.
 */
.diff-card-title {
  margin: 0px;
  color: #151515;
  font-family: "Product Sans";
  font-size: 26px;
  font-style: normal;
  font-weight: 400;
  line-height: 110%;
}

.diff-card-body {
  margin: 0;
  color: #646464 !important;
  font-family: "Product Sans";
  font-size: 20px;
  font-style: normal;
  font-weight: 400;
  line-height: 150%;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PLANS
   Static for now. The prices and feature lists come from the LeadX platform
   later, so every value that will move has an element of its own — wiring it up
   should be a text swap, not a rewrite.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The page-wide section padding, not one of its own — every other section
   takes --section-pad-y and this should move with them. */
.plans {
  /*
   * position + isolate because .plans-wash sits at z-index -1 inside it. The
   * isolation is what keeps it there: without a stacking context of its own, a
   * negative z-index child paints behind the section AND behind anything the
   * page has painted under it, which on a section with no background of its own
   * means it can disappear entirely.
   */
  position: relative;
  isolation: isolate;

  padding-block: var(--section-pad-y);

  /*
   * The wash is 1440 wide and its blur runs past the top of the section, so on
   * anything narrower it would widen the page and lengthen it.
   *
   * clip, not hidden. Both crop identically, but hidden makes this a scroll
   * container - which is what silently killed position: sticky and the
   * animation-timeline reveals elsewhere on this page. Nothing in Plans depends
   * on either today; the next thing added to it might.
   */
  overflow: clip;
}

/*
 * Centred on the section and held at the comp's frame width rather than
 * stretched to the viewport - the colour inside it is already transparent well
 * before the edge of the file, so a wider screen shows no seam, while
 * stretching would grow the glow with the window and stop it matching the
 * content it sits behind.
 *
 * max-width: none because the global img rule caps images at 100%, which here
 * would shrink the wash on every viewport under 1440.
 */
.plans-wash {
  position: absolute;
  top: var(--plans-wash-top);
  left: 0;
  z-index: -1;

  /*
   * Full-bleed, and stretched rather than scaled. This one does NOT follow
   * .action-wash and .deep-blurs, and the reason is the file.
   *
   * Those two are transparent well before their bounding box ends, so holding
   * them at the comp's 1440 and centring them shows no seam on a wider screen.
   * This file is not built that way - its colour runs all the way to the canvas
   * edge. Held at 1440 on a 1920 monitor it stopped dead at x 240 and x 1680,
   * and the two vertical edges where the wash met white read as borders down
   * both sides of the section. On a 1440 laptop they sat exactly on the viewport
   * edge, which is why it looked fine there and wrong on a desktop.
   *
   * height is the file's own 1376 and object-fit is fill, so only the WIDTH
   * changes with the viewport. That matters more than it sounds: with height:
   * auto the image would grow taller as it got wider - 1835 at 1920 - and carry
   * the colour down with it, so --plans-wash-top would only be right at one
   * screen size. Fixing the height keeps the vertical geometry identical
   * everywhere, and one offset stays true at every width.
   *
   * Stretching four heavily blurred blobs horizontally is not visible. Nothing
   * in this file has an edge sharp enough to show the distortion.
   */
  width: 100%;
  max-width: none;
  height: var(--plans-wash-h);
  object-fit: fill;

  pointer-events: none;
}

/*
 * The step between the desktop 50 and the tablet 34.
 *
 * Without it the heading stayed at 50 all the way down to 992, where the
 * container is 960 wide and the measure is 950 - so the two lines ran the full
 * width of the rail with 28px of air either side, while at 1440 they sit in 950
 * of a 1320 rail with 245. Same type size, completely different reading: a
 * heading that fills its rail edge to edge reads as the page running out of room
 * rather than as a chosen line length.
 *
 * Top level on purpose. Written inside the 991.98 block it would have ANDed with
 * it and only ever applied below 992, which is the one range that was already
 * correct. It sits above the 34px step so that one still wins below 992.
 *
 * 42 is the midpoint. It is not a Figma value - the comp only draws 1440 - so it
 * is here to keep the ramp even rather than to match a number.
 */
@media (max-width: 1199.98px) {
  :root {
    --fs-plans-title: 42px;
  }
}

/* ── Head ────────────────────────────────────────────────────────────────── */

.plans-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-plans-head);
  margin-bottom: var(--gap-plans-grid);
  text-align: center;
}

/* The page's ink gradient, the same treatment every other section title takes. */
.plans-title {
  max-width: var(--plans-title-measure);
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-plans-title);
  font-weight: var(--fw-regular);
  line-height: var(--lh-plans-title);
  color: var(--text-heading);
  background: var(--title-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

@media (forced-colors: active) {
  .plans-title {
    background: none;
    -webkit-text-fill-color: currentColor;
  }
}

/* ── The three cards ─────────────────────────────────────────────────────── */

.plans-grid {
  /*
   * stretch, not center.
   *
   * Centred columns each hugged their own content, so the three cards were three
   * different heights and sat on three different baselines - a plan with a
   * description stood taller than one without, and the row read as ragged rather
   * than as a set. Stretch makes every column the height of the tallest, .plan
   * has height: 100%, and the auto gaps inside it absorb the difference, so the
   * feet line up and no card has to be padded by hand to match its neighbours.
   *
   * It also stops the card heights moving every time a plan is edited in
   * LeadXForms admin, which is the whole point of the section being dynamic.
   */
  align-items: stretch;
  row-gap: var(--gap-plans-card);
}

/*
 * THE INACTIVE BILLING GRID. Without this rule both grids draw at once, which is
 * exactly what happened the first time the toggle shipped.
 *
 * plans-render.php marks the hidden grid with the `hidden` ATTRIBUTE, which is
 * the right thing to use - it takes the grid out of the accessibility tree and
 * out of tab order, not just out of sight. But `hidden` only works through the
 * UA stylesheet's `[hidden] { display: none }`, and a UA rule loses to ANY
 * author rule that sets display. This grid has two of them: Bootstrap's
 * `.row { display: flex }`, which applies at every width, and `.plans-grid
 * { display: flex }` in the 992 block below. Both outrank the UA rule, so the
 * attribute was set correctly and drew nothing.
 *
 * The attribute selector takes the specificity to 0-2-0, which beats both
 * (0-1-0) on their own terms rather than by !important or by source order - so
 * it holds wherever the two display rules move to.
 */
.plans-grid[hidden] {
  display: none;
}

/*
 * The 24px between cards, from the comp - "display: inline-flex; align-items:
 * flex-start; gap: 24px" on the Plans frame.
 *
 * A Bootstrap row cannot draw it. The gap between two cards there is the gutter
 * PLUS whatever slack each card leaves in its own column, and .plan is 360 wide
 * inside a 440 column, so the 24px gutter was arriving as 32 + 24 + 32 = 88.
 * Raising or lowering --bs-gutter-x only moves one of those three numbers.
 *
 * So above lg the row becomes what the comp says it is: a flex line that centres
 * itself and puts exactly 24px between its children. inline-flex is not needed
 * for that - justify-content: center does the hugging part, without the row
 * losing its block behaviour. The columns keep their Bootstrap classes so the
 * stack below lg is untouched; they are only stripped of the gutter padding and
 * the negative margin that pairs with it, both of which would double-count here.
 *
 * flex-basis 360 with shrink allowed, capped at 360: the cards sit at their
 * drawn width wherever there is room, and narrow together between 992 and 1200
 * where three 360s plus two gaps do not fit. They never grow past 360.
 *
 * NOTE: the comp says align-items: flex-start, which would let each card hug its
 * own content again. Kept as stretch on purpose - equal heights with the feet
 * lined up was a separate decision, and flex-start would undo it.
 */
@media (min-width: 992px) {
  .plans-grid {
    display: flex;
    flex-wrap: nowrap;
    justify-content: center;
    gap: var(--gap-plans-card);
    margin-inline: 0;
  }

  .plans-grid>[class*="col-"] {
    flex: 1 1 var(--plan-w);
    max-width: var(--plan-w);
    padding-inline: 0;
  }
}

/*
 * height: 100% so the three match whatever the longest one needs. The comp lines
 * their feet up, and a feature list is the one thing here certain to change
 * length once this is wired to the platform.
 */
.plan {
  /*
   * Restored. .plan-badge is position: absolute, so without a positioned
   * ancestor here it resolved against the viewport instead: the Most Popular
   * pill rendered at the very top of the PAGE (top: -14px) while its card sat
   * 13,479px down. It costs nothing visually - it only gives the badge
   * something to be absolute against.
   */
  position: relative;

  display: flex;

  /*
   * 360 as a ceiling, not a fixed size. As a fixed width the card kept its 360
   * inside a 290px column on a 320px phone and hung 55px past the screen edge;
   * .page-wrap { overflow-x: clip } then cropped the overrun, so nothing
   * scrolled sideways and the card simply lost its right edge - which is why a
   * scrollWidth sweep called it clean. The desktop card is still exactly 360.
   */
  width: 100%;
  max-width: var(--plan-w);

  /*
   * Centred in its column. A 360px card in a 440px column sits left by default,
   * which put 12px between the rail and the first card and 68px between the last
   * card and the rail - the row looked shunted left even though the columns were
   * even. The cap is what creates the slack, so the cap has to say where it goes.
   */
  margin-inline: auto;

  height: 100%;
  padding: 32px;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--plan-pad);
  border-radius: 20px;
  border: 1px solid #BED1C7;
  background: #FFF;
}

/*
 * The featured plan stands taller and carries the only filled CTA on the row.
 * The lift is padding, not a transform: a scaled card blurs its own text.
 */
.plan--featured {
  padding-block: var(--plan-pad-featured);
  border-color: var(--plan-line-featured);
  box-shadow: var(--plan-shadow);
}

.plan-badge {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: var(--plan-badge-pad);
  border-radius: 50px;
  background: var(--hf-ink);
  color: #fff;
  font-family: var(--font-ui);
  font-size: var(--fs-plan-badge);
  font-weight: var(--fw-medium);
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
}

.plan-content-section {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 18px;
  align-self: stretch;
}

.plan-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
  align-self: stretch;
}

.plan-name {
  margin: 0px;
  font-family: var(--font);
  font-size: var(--fs-plan-name);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  color: var(--text-heading);
}

/*
 * THE PRICE.
 *
 * There was none. The card rendered name, description, feature count and CTA,
 * and the note on render_home_plans_cards() pointed at the annual band below
 * the grid as the place a figure appears - but that band carries d-none, so the
 * section reached visitors with no price anywhere in it.
 *
 * `baseline`, not `center`, and that is the whole reason the symbol, the figure
 * and the period are three elements rather than one string. At three different
 * sizes, centring would float the small parts in the middle of the figure's
 * height; sitting them on a shared baseline is what makes "A$20" read as one
 * price rather than three pieces of text. It is also what leadxforms.com does,
 * which matters more than the typography here - the two sites should quote a
 * price the same way.
 */
.plan-price {
  display: flex;
  align-items: baseline;
  gap: var(--gap-plan-price);
  margin: 0;
  font-family: var(--font);
  color: var(--text-heading);
}

.plan-price-symbol {
  /*
   * Centred against the figure rather than sitting on its baseline, which is
   * what the rest of this row does.
   *
   * align-self, not a change to the parent: the period still wants the
   * baseline. Only the symbol steps out of it, so .plan-price keeps
   * align-items: baseline and this one child overrides it.
   *
   * The flex line's cross size is the figure's 40px (line-height 1), so this
   * centres the 22px symbol inside that 40. Both have line-height: 1, so each
   * box is its own type size and there is no leading on either side skewing
   * where the middle falls.
   */
  align-self: center;
  font-size: var(--fs-plan-price-symbol);
  font-weight: var(--fw-bold);
  line-height: 1;
}

.plan-price-figure {
  font-size: var(--fs-plan-price);
  font-weight: var(--fw-bold);
  /* 1, so the figure's box is its own height and the gap above and below the
     price is the flex gap the card already sets - not leading. */
  line-height: 1;
}

/*
 * Pushed off the figure by its own margin rather than by the flex gap, because
 * the gap also sits between the symbol and the figure, where 2px is right and
 * 8px is not.
 */
.plan-price-period {
  margin-left: 8px;
  font-size: var(--fs-plan-price-period);
  font-weight: var(--fw-regular);
  line-height: 1;
  color: var(--text-muted);
}

/*
 * The struck-through "was" figure, drawn only when the package is really
 * discounted - plans-render.php omits the span entirely otherwise, so there is
 * no empty-state to style here.
 *
 * line-through rather than a decorative rule: it has to survive a stylesheet
 * that fails to load, and "A$18" sitting beside "A$17" with no strike reads as
 * a second price rather than a former one.
 */
.plan-price-was {
  margin-left: 10px;
  font-size: var(--fs-plan-price-period);
  font-weight: var(--fw-regular);
  line-height: 1;
  color: var(--text-micro);
  text-decoration: line-through;
}

/*
 * "A$210 billed annually", under the per-month figure on the yearly grid only.
 *
 * It is the number that has to match the invoice, which is why it is printed at
 * all: the headline above it is a per-month equivalent nobody is ever charged.
 */
.plan-price-annual {
  margin: 6px 0 0;
  font-size: var(--fs-plan-price-period);
  font-weight: var(--fw-regular);
  line-height: 1.4;
  color: var(--text-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE BILLING TOGGLE
   ═══════════════════════════════════════════════════════════════════════════

   Ported from LeadXForms' own resources/js/frontend/css/pricing-cards.css,
   class names and values included, because the two sites sell the same packages
   and a visitor moving between them should not have to learn a second control.
   The one substitution is its --lxf-primary, which is #008744 - the same colour
   this file already calls --green.

   Values are literals rather than new tokens on purpose: each is read exactly
   once, and they are a faithful copy of another project's component rather than
   a part of this page's own scale. Changing them here would put the two
   controls out of step, which is the thing this port exists to prevent.

   A SEGMENTED CONTROL, NOT A SWITCH. A switch says "off or on" - it has a
   default and a deviation - and a billing period is neither: monthly and yearly
   are two equal choices, and which one is selected has to be readable at a
   glance rather than inferred from which side a knob is sitting on.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * THE HEADING AND THE TOGGLE ARE ONE GROUP.
 *
 * Same six declarations as .action-head, and that is the point: the toggle sits
 * below the plans heading exactly the way that section's lede sits below its
 * own. 20px between them, 56px from the group to the content underneath.
 *
 * A WRAPPER RATHER THAN MARGINS, and the difference is not cosmetic. As two
 * siblings the spacing needed :has() to work at all, because .plans-head's own
 * margin-bottom is the gap to the CARDS - and on production, where LeadXForms
 * returns monthly packages only, no toggle is drawn and that gap still has to
 * be the full one. A flex column answers that by existing: with one child the
 * gap has nothing to apply to and collapses on its own, and margin-bottom is
 * the same 56 either way. No second rule, no :has(), no empty-state to style.
 *
 * .plans-head keeps its own margin-bottom for the cards it no longer precedes -
 * it is zeroed below rather than removed, because the rule is shared with the
 * no-toggle case through this same wrapper.
 */
.plans-intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-section-head);
  margin-bottom: 56px;
  text-align: center;
}

/* The group owns the space under the heading now. Left in place rather than
   deleted from .plans-head, because that rule is also what centres the heading
   and sets its own internal gap. */
.plans-intro .plans-head {
  margin-bottom: 0;
}

.plans-duration-wrap {
  display: flex;
  justify-content: center;
}

.billing-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;

  /* The 4px padding is what makes the active pill read as sitting inside a
     track rather than replacing it - without it the fill meets the outer edge
     and the control looks like two buttons glued together. */
  padding: 4px;
  background: #F3F4F6;
  border-radius: 999px;
}

.billing-seg {
  display: inline-flex;
  align-items: center;
  gap: 9px;

  border: 0;
  background: transparent;
  padding: 10px 22px;
  border-radius: 999px;

  font-family: var(--font);
  font-size: 15px;
  font-weight: 500;
  line-height: 1;
  color: #6B7280;
  cursor: pointer;
  white-space: nowrap;

  /* Colour only. A transform or width change here would shift the segment
     beside it every time the selection moved, and the two segments are
     different widths already because one carries a badge. */
  transition: background-color .22s ease, color .22s ease;
}

.billing-seg:hover:not(.is-active) {
  color: #111827;
}

.billing-seg.is-active {
  background: var(--green);
  color: #fff;
}

/* Offset outward so the ring clears the pill rather than sitting on top of the
   green. The page's global :focus-visible rule would otherwise draw it at the
   default 3px offset against a 999px radius and clip oddly. */
.billing-seg:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.billing-save {
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 8px;
  background: #B3E8CA;
  color: #04552F;
  transition: background-color .22s ease, color .22s ease;
}

/* On the green fill the mint disappears into the background, so the badge goes
   white and keeps the dark green text. Deliberately not white-on-green text: at
   12px that is the first thing to go illegible, and this badge is carrying the
   number the whole toggle exists to advertise. */
.billing-seg.is-active .billing-save {
  background: #fff;
  color: #03543F;
}

@media (max-width: 420px) {
  .billing-seg {
    padding: 9px 14px;
    font-size: 14px;
    gap: 7px;
  }

  .billing-save {
    font-size: 10px;
    padding: 4px 6px;
  }
}

.plan-desc {
  margin: 0px;
  font-size: var(--fs-plan-desc);
  line-height: var(--lh-lede);
  color: var(--text-muted);
}

.plan-count {
  margin: 0px;

  /*
   * A floor, not the gap. Figma marks the three spaces below this line Auto:
   * they take whatever height the card has left over. An auto margin gives
   * exactly that, but collapses to nothing when there is no slack, which would
   * let the rule sit on the word. The padding here and on the list below is the
   * minimum those gaps can ever be; the auto margins add the rest.
   */
  padding-bottom: 15px;
  font-size: var(--fs-plan-count);
  font-weight: var(--fw-bold);
  color: var(--text-heading);
}

/*
 * Its own element rather than a border on .plan-count, because Figma puts an
 * Auto gap on BOTH sides of it. A border cannot hold a margin of its own, so as
 * part of the count it could only ever have a fixed space above it.
 */
.plan-rule {
  height: 1px;

  /*
   * The auto margins here are commented out rather than kept.
   *
   * Figma marks all three gaps below "N features" Auto, and that was right for a
   * card that hugged its content: the slack was nil, so Auto and a fixed gap
   * drew the same thing. Now that .plans-grid stretches every card to the
   * tallest, there IS slack - and three auto gaps split it evenly, which put a
   * ~60px hole between the count and the rule on the two plans that have no
   * description. A line floating in the middle of empty space reads as a
   * mistake; the same space below the feature list reads as breathing room.
   *
   * So the slack all goes to one place: .plan-cta keeps margin-top: auto and
   * collects it above the button. The floors on .plan-count and .plan-features
   * are unchanged, so the spacing the comp specifies is exactly what a card with
   * no slack still draws.
   *
   * margin-block: auto;
   */
  background: var(--plan-line);
}

/*
 * The tick is drawn by CSS rather than typed, so a screen reader is read four
 * features and not "check mark" four times.
 */
.plans .plan-features {
  margin: 0;
  padding: var(--gap-plan-count) 0 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--gap-plan-feature);
}

.plans .plan-features li {
  position: relative;
  padding-left: 24px;
  font-size: var(--fs-plan-feature);
  line-height: 1.5;
  color: var(--text);
}

.plans .plan-features li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .45em;
  width: 12px;
  height: 7px;
  border-left: 2px solid var(--green);
  border-bottom: 2px solid var(--green);
  transform: rotate(-45deg);
}

/* margin-top: auto pins the CTA to the foot, so three cards of different
   lengths still line their buttons up. */
/*
 * The gap belongs to the list, not to the button.
 *
 * .button carries a fixed height, so padding-top on it cannot make room — the
 * box does not grow and the label is pushed against its own edge while the
 * button still sits on the last feature. margin-top: auto is what pins it to
 * the foot of the card, so the space above has to come from the list.
 */
.plans .plan-features {
  padding-bottom: 15px;
}

/*
 * .plans, not .plan-cta alone.
 *
 * .button opens with `all: unset`, which resets margin-top to 0, and it is
 * declared further down this file than .plan-cta was - same specificity, later
 * rule, so it won and the auto margin never applied. The card then had free
 * space with nowhere to go, justify-content: space-between split it evenly
 * across all three gaps, and the divider floated in the middle of a ~29px hole
 * on every plan with no description.
 *
 * The extra class puts this at 0,2,0 so it outranks .button without !important.
 * Anything else this card sets on .button will need the same treatment.
 */
.plans .plan-cta {
  width: 100%;
  margin-top: auto;
}

/* ── The yearly offer ────────────────────────────────────────────────────── */

.plans-yearly {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-plans-yearly);
  margin-top: var(--gap-plans-yearly-top);
  padding: var(--plans-yearly-pad);
  border-radius: var(--plans-yearly-radius);
  background: var(--plans-yearly-bg);
}

.plans-yearly-copy {
  flex: 1 1 var(--plans-yearly-copy-min);
}

.plans-yearly-badge {
  display: inline-block;
  margin: 0 0 var(--gap-plans-yearly-badge);
  padding: var(--plan-badge-pad);
  border-radius: 50px;
  background: var(--green);
  color: #fff;
  font-family: var(--font-ui);
  font-size: var(--fs-plan-badge);
  font-weight: var(--fw-medium);
  letter-spacing: .08em;
  text-transform: uppercase;
}

.plans-yearly-title {
  margin: 0 0 var(--gap-plans-yearly-title);
  font-family: var(--font);
  font-size: var(--fs-plans-yearly-title);
  font-weight: var(--fw-regular);
  line-height: 1.2;
  color: #fff;
}

.plans-yearly-body {
  max-width: var(--plans-yearly-measure);
  margin: 0;
  font-size: var(--fs-plans-yearly-body);
  line-height: var(--lh-lede);
  color: var(--plans-yearly-ink);
}

.plans-yearly-offer {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--gap-plans-price);
  text-align: right;
}

/* baseline, so the unit sits on the figure's baseline and not on its box. */
.plans-price {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0;
  color: #fff;
}

.plans-price-figure {
  font-family: var(--font);
  font-size: var(--fs-plans-price);
  font-weight: var(--fw-bold);
  line-height: 1;
}

.plans-price-unit {
  font-size: var(--fs-plans-price-unit);
  color: var(--plans-yearly-ink);
}

.plans-price-note {
  margin: 0;
  font-size: var(--fs-plans-price-note);
  color: var(--plans-yearly-ink);
}

/* ── Footnote ────────────────────────────────────────────────────────────── */

.plans-foot {
  margin: var(--gap-plans-foot) 0 0;
  font-size: var(--fs-plans-foot);
  text-align: center;
  color: var(--text-muted);
}

.plans-foot a {
  color: inherit;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FAQ AND CLOSING CTA
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The section is full width. The 900 Figma reports belongs to the LIST, not to
 * this — it is the width of one accordion row — and putting it here pinned the
 * whole section at 900px on every screen: narrower than every other section on a
 * laptop, and wider than the viewport on a phone, where it would have pushed the
 * page sideways if .page-wrap were not clipping it.
 *
 * The 75px gap moved with it. It was spacing the headline from the list AND the
 * list from the CTA, on top of the headline's own margin — so the headline sat
 * 147px above the first question rather than the 75 the comp shows.
 *
 * 80px top and bottom is Figma's, and it is this section's own rather than the
 * page's 96 — the green card at the end already carries a lot of air.
 */
.faq {
  padding-block: var(--faq-pad-y);
}

/* The page's ink gradient, same as the hero and the differentiation headline. */
.faq-title {

  margin: 0 auto var(--gap-faq-list);
  font-family: var(--font);
  font-size: var(--fs-faq-title);
  font-weight: var(--fw-regular);
  line-height: var(--lh-faq-title);
  text-align: center;
  color: var(--text-heading);
  background: var(--title-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

@media (forced-colors: active) {
  .faq-title {
    background: none;
    -webkit-text-fill-color: currentColor;
  }
}

/* ── The accordion ───────────────────────────────────────────────────────── */

/*
 * 900 rather than the container's full width. A question is one line of text and
 * a chevron; run across 1128 the chevron ends up a long way from the words it
 * belongs to, and the answers under it get a measure nobody wants to read.
 */
.faq-list {
  display: flex;
  flex-direction: column;
  gap: var(--gap-faq-item);
  max-width: var(--faq-list-w);
  margin-inline: auto;
}

/*
 * Figma's own layout for the row, verbatim: a flex column, 16px of padding top
 * and bottom, and 8px between the question and its answer.
 *
 * <details> as the flex container is what lets the gap do that work — closed,
 * there is only the summary and the gap has nothing to act on; open, it is the
 * one thing separating question from answer. No margins on either child.
 */
.faq-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /*
   * The row's padding moves onto the <summary>, and the item keeps none.
   *
   * The summary is the thing a tap opens, and with the padding out here it was
   * only as tall as its text — measured on a Pixel 7, a 35px target, under both
   * platform minimums. The 16px band above each question looked like part of the
   * row and did nothing when touched.
   *
   * The spacing is unchanged by this, not approximately but exactly: the 16 that
   * was above the question and the 8 that separated it from the answer are now
   * the summary's own top and bottom padding, and the 16 below the answer is the
   * answer's. Item height comes out the same, and the whole row is the target.
   */
  gap: 0;
  padding-block: 0;
  border-bottom: 1px solid #CBCBCB;
}

/*
 * The row. list-style:none on both the element and ::marker because the two
 * engines disagree about which one draws a <summary>'s triangle, and leaving
 * either one out leaves it showing in a browser somebody uses.
 */
.faq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;

  /*
   * `gap: auto` was here, and `auto` is not a value gap accepts — the whole
   * declaration was dropped and the row ran on gap: normal, which is 0. It read
   * as deliberate, so it is worth being explicit: nothing separated the question
   * from the chevron except space-between, and space-between only holds them
   * apart while the text is SHORTER than the room available. A question that
   * filled its line would have sat against the chevron with nothing between.
   */
  gap: 16px;
  width: 100%;
  color: #151515;
  font-family: "Product Sans";

  /*
   * Through the token. --fs-faq-q was already stepping to 16px on a phone and
   * wired to nothing, because this rule carried Figma's 20 as a number — so a
   * question stayed 20px on a 370px column and ran to three lines. The base is
   * 20, so nothing the comp was drawn at moves.
   */
  font-size: var(--fs-faq-q);
  font-style: normal;
  font-weight: 400;

  /*
   * Through the token, for the third time in this component — --fs-faq-q and
   * --fs-faq-a were both found wired to nothing, and this is the same bug in
   * the same rule: --lh-faq-q was declared, documented, and never read, because
   * a hardcoded 110% sat here instead.
   *
   * 110% is a HEADLINE's leading, and it only looked survivable while every
   * question fitted on one line, where leading has nothing to do. Under 768px
   * they wrap, and 16px text on a 17.6px line box put two lines all but touching
   * — which is the "squeezed together" this section was reported for.
   *
   * The token's own note derives 1.6 from the comp: 25 of padding, a 32px line
   * box, 25 again, which is the 82px row pitch the comp measures. At 110% the
   * row came out 72. So reading the token does not just fix the phone, it puts
   * the desktop row back on the number it was drawn at.
   */
  line-height: var(--lh-faq-q);
  cursor: pointer;
  list-style: none;

  /*
   * The row's padding lives here, not on .faq-item — see that rule. The summary
   * is what a tap opens, and with the padding outside it the target was only as
   * tall as its text: 35px on a Pixel 7, under both platform minimums.
   *
   * Top is the row's inset; bottom is what used to be the item's gap to the
   * answer, so the spacing is unchanged.
   */
  padding-block: var(--faq-row-pad) var(--gap-faq-qa);
}

/*
 * A closed row has no answer, so the answer's half of the inset is not there to
 * close it — the summary takes it back. Without this a closed row came out 8px
 * shorter than it used to be and sat 16 above its text against 8 below.
 *
 * Open rows are untouched: the 8 stays the gap to the answer, and the answer's
 * own 16 finishes the row.
 */
.faq-item:not([open]) .faq-q {
  padding-bottom: var(--faq-row-pad);
}

.faq-q::-webkit-details-marker {
  display: none;
}

.faq-q::marker {
  content: "";
}

.faq-q:hover {
  color: var(--green);
}

/* The ring belongs on the row, not on the text, because the row is what the
   keyboard lands on and what a click opens. */
.faq-q:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 4px;
  border-radius: 6px;
}

.faq-chevron {
  flex-shrink: 0;
  transition: transform .25s ease;
}

.faq-item[open] .faq-chevron {
  transform: rotate(180deg);
}

/* No margin and no padding: the item's gap is what sits between this and the
   question above it, and the item's padding is what holds it off the rule. */
.faq-a {
  max-width: var(--faq-answer-measure);
  margin: 0;

  /* The other half of the row's inset — see .faq-item. */
  padding-bottom: var(--faq-row-pad);
  color: #868686;
  font-family: "Product Sans";

  /*
   * One font-size, not two.
   *
   * This rule declared font-size twice — var(--fs-faq-a) and then a pasted 18px
   * — and line-height twice with it. The later literal won, so the tokens were
   * dead and the answer stayed 18px/150% at every width. The tokens now carry
   * the values that were actually rendering, which is what keeps desktop
   * identical, and the phone step below is free to move them.
   */
  font-size: var(--fs-faq-a);
  line-height: var(--lh-faq-a);
  font-style: normal;
  font-weight: 400;
  align-self: stretch;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CLOSING CTA
   Its own section rather than the tail of the FAQ, which is how the comp draws
   it: the green card is the page's last word, and the giant LeadXForms wordmark
   under it is the brand signing off. Neither belongs to a list of questions.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Top padding only. The wordmark's cut IS the bottom of the section in the comp
 * — the glyphs run straight into whatever follows — so any bottom padding here
 * puts a band of white under it and the crop stops reading as a crop.
 */
.cta {
  /*
   * Through the token, and block-only.
   *
   * --cta-pad-y steps to 64 below lg and was read by nothing, because this rule
   * carried Figma's 80 as a flat shorthand — so this band kept 80 above and
   * below at every width while every section around it dropped to 64.
   *
   * The inline half of that shorthand was setting 0, which is also what the
   * container wants, so nothing is lost by naming only the block axis: the page
   * rail comes from .container and does not want a section overriding it.
   */
  padding-block: var(--cta-pad-y);
}

.cta-card {
  display: flex;

  /*
   * max-width, not the flat `width: 1122px` Figma reports. A fixed width is a
   * width at every screen size, so the card kept its 1122 on a 390px phone and
   * dragged a horizontal scrollbar across the WHOLE page — measured 1150px of
   * document in a 992px window. Same fault as `.faq { width: 900px }` had.
   *
   * 1122 is still the number; it is now a ceiling instead of a floor.
   */
  width: 100%;
  max-width: var(--cta-card-w);
  margin-inline: auto;

  flex-direction: column;
  align-items: center;
  gap: var(--gap-cta-card-rows);

  /*
   * Figma gives 80px 0px. The 0 works there because the headline's own 760px
   * measure is what insets it inside a 1122px card — but on a phone the card is
   * 366 wide, the measure never bites, and the text would sit against the green
   * edge. So the x is 0 here and real at the phone breakpoints below.
   */
  padding: var(--cta-card-pad-y) var(--cta-card-pad-x);
  border-radius: var(--cta-radius);
  background: var(--green);
  text-align: center;
}

/*
 * align-self, for the same reason .cta-content carries it: .cta-card is a flex
 * column with align-items:center, which sizes every child to its own content. A
 * row left at that width collapses its column to the width of the text inside
 * it, and the centred rail stops being a rail.
 */
.cta-head-row {
  align-self: stretch;
}

.cta-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  align-self: stretch;
}

.cta-title {
  background: linear-gradient(90deg, #FFF 50%, #FFF 100%);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  margin: 0px;
  text-align: center;
  font-family: "Product Sans";

  /*
   * Through the token. --fs-cta-title was already stepping — 28 below lg, 22 on
   * a phone — and read by nothing, because this rule carried Figma's 42 as a
   * number. So the headline stayed 42px on a 334px column and ran to four lines.
   * The base becomes 42, the value that was actually drawing.
   */
  font-size: var(--fs-cta-title);
  font-style: normal;
  font-weight: 400;
  line-height: var(--lh-cta-title);
}

.cta-lede {
  max-width: 602px;
  margin: 0px;
  color: var(--White, #FFF);
  text-align: center;
  font-family: "Product Sans";

  /* Same again — --fs-cta-lede was never read. 18 is what this was drawing. */
  font-size: 22px;
  font-style: normal;
  font-weight: 400;
  line-height: 150%;
  /* 27px */
}

/*
 * Dark on green, which is the one pairing on this page where the button is
 * darker than what it sits on. It has to be: a white button on this green reads
 * as a gap in the card, and a green one disappears into it.
 */
/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.cta-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: var(--cta-btn-h);
  padding: 0 var(--cta-btn-pad-x);
  border-radius: var(--cta-btn-radius);
  background: var(--cta-btn-bg);
  color: var(--white);
  font-family: var(--font-ui);
  font-size: var(--fs-cta-btn);
  font-weight: var(--fw-medium);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color .18s ease;
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.cta-btn:hover {
  background: var(--hf-ink-hover);
}
*/

/* Superseded — this CTA is the header .button component now. Kept as the
   record of what it used to carry. */
/*
.cta-btn:hover .btn-icon {
  transform: translateX(3px);
}
*/

/* ── The wordmark ────────────────────────────────────────────────────────── */

/*
 * The brand name, full-bleed and cut off by the bottom of the section, exactly
 * as the comp draws it. It is decoration, not content — the footer already
 * carries the real logo — so it is an empty span with aria-hidden rather than
 * text a screen reader would read out a second time.
 *
 * Drawn as a MASK over a background-colour, not as a coloured SVG: that way the
 * tint is a CSS token anyone can change, and the one asset works if the section
 * ever sits on a dark ground. The file is the LeadXForms logo with the mascot
 * paths dropped and the box trimmed to the glyphs (see assets/wordmark.svg).
 *
 * The crop is done by the box being SHORTER than the glyphs rather than by
 * overflow: mask-size 100% auto makes the mask as wide as the box and as tall as
 * it needs to be, mask-position top anchors it, and whatever falls past the
 * bottom is simply never painted. No clipping context, no negative margins.
 */
.cta-wordmark {
  display: block;
  margin-top: var(--gap-cta-wordmark);
  aspect-ratio: var(--cta-wordmark-ratio);
  background-color: var(--cta-wordmark-ink);
  -webkit-mask: url('../images/wordmark.svg') top center / 100% auto no-repeat;
  mask: url('../images/wordmark.svg') top center / 100% auto no-repeat;
}

/* ═══════════════════════════════════════════════════════════════════════════
   HEADER AND FOOTER
   Ported from LeadXForms rather than designed here, so the two products wear
   the same chrome. Every measurement below is that stylesheet's — the bar's
   20px band, the 200px logo, the menu's 25px gap and 17px links, the pill
   buttons at 50px tall with their 2px border and 50px radius.

   The values are inlined as tokens rather than left as literals so the rest of
   this file can read them, but they are not open design decisions: change one
   and this page stops matching LeadXForms, which is the whole point of it.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── The bar ─────────────────────────────────────────────────────────────── */

/*
 * Transparent at the top of the page, solid once it scrolls — the hero's wash
 * runs under the bar rather than stopping at it, and the bar only becomes a
 * strip when there is content that would otherwise pass behind it.
 *
 * style.css says background:#fff here; MasterLayout.vue overrides it to
 * transparent, and the override is what actually renders. Measuring the live
 * header confirms it — the bar reads #EAF5EF there, which is that page's own
 * hero wash showing through, not a colour the header sets.
 *
 * Absolute, not fixed, until .header-sticky makes it fixed. site.js adds that.
 */
.main-header {
  background: transparent;
  padding: var(--hf-bar-pad) 0;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 999;
  transition: all .3s ease-in-out;
}

.header-sticky {
  position: fixed;
  background: #fff;
  box-shadow: 0 2px 20px rgba(1, 41, 112, .08);
  padding: var(--hf-bar-pad-sticky) 0;
}

/*
 * Every section the primary nav can jump to.
 *
 * assets/js/site.js carries no anchor handler, so the browser's native jump does this
 * and it parks the section's top edge at the very top of the viewport — which is
 * behind the bar above, because .header-sticky is fixed. Measured before this
 * rule, with the sticky bar ending at 74px, four of the six targets put their
 * eyebrow — the section's actual first line — at 50px:
 *
 *   #how-it-works              "How Protection Works, In Real Time"   50px
 *   #features                  "Feature Deep-Dives"                   50px
 *   #live-threat-intelligence  "Live Threat Intelligence"             50px
 *   #plans                     "Plans"                                50px
 *
 * Each one was sliced by the bar, so a reader clicking Features landed on a
 * section that appeared to start at its heading with its label missing.
 *
 * This was nearly dismissed on the grounds that the headings cleared the bar at
 * 87px and so nothing was wrong. They did — but the heading is not the top of
 * the section, the eyebrow above it is, and measuring the h2 is what hid it.
 *
 * --header-h-sticky, not --header-h: the bar is in its scrolled state by the
 * time any of these lands, and the two are different numbers (74 against 94).
 * The section's own 50px of top padding then does the breathing, which is what
 * it was drawn for.
 *
 * #top is in the nav as well and needs nothing: it is <body>, and at scroll 0
 * the bar is not fixed yet.
 */
#how-it-works,
#features,
#ai-code-guardian,
#live-threat-intelligence,
#why-leadx-security,
#plans,
#faq {
  scroll-margin-top: var(--header-h-sticky);
}

.logo {
  display: block;
  width: var(--hf-logo-w);
  transition: all .3s ease;
}

.header-sticky .logo {
  width: var(--hf-logo-w-sticky);
}

.logo img {
  width: 100%;
  height: auto;
}

/*
 * 28×28 was the whole tap target — the glyph and nothing else. The padding takes
 * it to 44×44, the size a thumb is designed against, and the negative margin
 * hands every one of those pixels back to the layout, so the button sits exactly
 * where it did with more of it to hit.
 */
.menu-btn {
  all: unset;
  box-sizing: border-box;
  display: block;
  font-size: 28px;
  line-height: 28px;
  padding: 8px;
  margin: -8px;
  color: var(--hf-ink);
  cursor: pointer;
}

/* ── Menu ────────────────────────────────────────────────────────────────── */

/* The two actions, hard against the right of the bar. LeadXForms spells this
   out with Bootstrap utilities; only the grid half of that framework is loaded
   here, so the same three declarations are stated directly. */
.other-nav-links {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

.main-menu {
  display: flex;
  align-items: center;
  gap: var(--hf-menu-gap);
}

.main-menu a {
  color: var(--hf-ink);
  font-size: var(--fs-hf-menu);
  text-decoration: none;
  white-space: nowrap;
  transition: all .3s;
}

/*
 * :focus-visible, not :focus — and the difference is what makes .active mean
 * anything.
 *
 * .active is set by the scroll-spy in assets/js/site.js and marks the section the
 * reader is actually in. With plain :focus in this list, a MOUSE click left the
 * clicked item green for as long as it kept focus, so after clicking Home and
 * scrolling to Features the bar showed two green items and neither one told the
 * truth. :focus-visible only paints when the focus came from the keyboard, which
 * is the case where the ring is wanted anyway.
 */
.main-menu a:is(:hover, :focus-visible, .active) {
  color: var(--green);
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */

/*
 * `all: unset` first, because this class is worn by both <a> and <button> and
 * the two arrive with different defaults. Everything the button is is then
 * stated, so neither element can bring anything of its own.
 */
.button {
  all: unset;

  /*
   * Restated immediately after `all: unset`, which is what makes it necessary.
   * `all` sets every property to inherit or initial, and box-sizing is not an
   * inherited one — so it lands on initial, which is content-box. The page's
   * global border-box rule is on `*` at specificity 0,0,0 and loses to this
   * class's own declaration.
   *
   * LeadXForms never sees this because its reset carries !important, which
   * outranks `all` — so the same rule behaved differently the moment it was
   * moved to a stylesheet whose reset is polite.
   *
   * Cost of getting it wrong: height:50 became 50 of CONTENT plus 12 of padding
   * plus 4 of border — a 66px pill. And the icon disc is height:calc(100% - 12px)
   * against a fixed 50px width, so it went from 50×38 to 50×54: a horizontal
   * lozenge turned into a vertical one, which is the shape that was reported.
   */
  box-sizing: border-box;

  position: relative;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  flex-shrink: 0;
  gap: 10px;
  /*
   * min-height, not height, and that distinction is the whole bug.
   *
   * With a hard 50px the pill could not grow, so a label that wrapped was not
   * laid out taller — it overflowed a 46px content box by 7px and `overflow:
   * hidden` above cut it. Both lines were still drawn, one bleeding off the top
   * edge and one off the bottom, which is what "the button breaks" looked like.
   * It bit exactly two labels at 390px — the hero's "Get My Free Security
   * Assessment" and the guardian card's "See how AI Code Guardian works" — and
   * would bite any label long enough on any narrower screen.
   *
   * min-height keeps every single-line button at exactly 50px, because 20.5px
   * of line box plus this padding and the 4px border comes to 48.5 and the
   * minimum wins. Only a wrapped label grows the pill, which is what it should
   * have done from the start.
   *
   * The padding stays at 6px, and that is measured rather than assumed. The
   * line box here is 24px, so 12 of padding and 4 of border come to 40 — well
   * under the 50 minimum, which is why every single-line button on the page
   * still lays out at exactly 50px and nothing that was already right moves.
   * Raising it to 12px was tried first and pushed all of them to 52.
   */
  min-height: var(--hf-btn-h);
  padding: var(--hf-btn-pad-y) var(--hf-btn-pad-x);

  /*
   * ONE LINE, ALWAYS. A wrapped label breaks the pill's shape, and the shape is
   * the component — so the label is never allowed to wrap and the button is
   * sized to hold it instead.
   *
   * min-height above is the safety net, not the plan: with nowrap the label
   * cannot wrap, so the pill only ever grows if something else in it does.
   *
   * What makes this work is that the button's OWN cost comes down on a phone
   * rather than the text having to fit around it. Measured at 18px, the two long
   * labels are 263px and 259px, and the fixed padding+border was 93px — on a
   * 320px screen the guardian card's button is 242px wide altogether, so 93 of
   * chrome left 149px for 259px of text. No font size fixes that. The phone
   * block near the end of this file cuts the chrome and steps the type; see it
   * for the numbers.
   */
  white-space: nowrap;
  border: 2px solid var(--hf-ink);
  border-radius: 50px;
  background-color: var(--hf-ink);
  color: #fff;
  font-family: var(--font);
  font-weight: var(--fw-regular);
  font-size: var(--fs-hf-btn);
  text-decoration: none;
  cursor: pointer;
  overflow: hidden;

  /*
   * color is in this list now, and that is the whole fix.
   *
   * Every variant already carried the same transition declaration, so the three
   * filled ones looked right - their label is white before and after the hover,
   * so a missing color transition had nothing to show. .button-outline is the
   * one that changes it, ink -> white, and with color left out the label jumped
   * to white on the first frame while the background behind it was still half
   * way through its half second. The button read as two different animations
   * running at once, which is what made the plan CTAs feel unlike the navbar.
   *
   * Same duration and easing as the rest of the list, so nothing about the
   * buttons that were already correct changes.
   */
  transition:
    transform .5s cubic-bezier(.76, 0, .24, 1),
    background-color .5s cubic-bezier(.76, 0, .24, 1),
    border-color .5s cubic-bezier(.76, 0, .24, 1),
    color .5s cubic-bezier(.76, 0, .24, 1),
    box-shadow .5s cubic-bezier(.76, 0, .24, 1);
}

.button:is(:hover, :focus) {
  background-color: var(--hf-ink-hover);
  border-color: var(--hf-ink-hover);
  color: #fff;
  transform: translateY(-2px);
  box-shadow: 0 10px 20px rgba(0, 0, 0, .12);
}

.button:active {
  transform: translateY(0) scale(.98);
}

/*
 * The header carries both a signed-out and a signed-in pair of buttons and hides
 * one with the `hidden` attribute. .button sets display: inline-flex, and an
 * author rule beats the UA stylesheet's [hidden] { display: none } - so without
 * this the hidden pair draws anyway. Same cause as .plans-grid[hidden] above.
 *
 * The attribute selector takes it to 0-2-0, which clears .button (0-1-0) on
 * specificity rather than on source order.
 */
.button[hidden] {
  display: none;
}

.button-dark {
  background-color: var(--hf-ink);
  border-color: var(--hf-ink);
  color: #fff;
}

/*
 * The third variant, for a card whose own ground is already white.
 *
 * The header ships two, dark and green, and both are filled. On a white plan
 * card a filled button would be the loudest thing in it, and the comp draws the
 * two secondary plans outlined so the featured one is the only filled CTA on
 * the row. Everything else is the component: the pill, the height, the 2px
 * border and the icon disc. Same button, different fill.
 */
.button-outline {
  background-color: transparent;
  border-color: var(--hf-ink);
  color: var(--hf-ink);
}

.button-outline .button-icon-span {
  background-color: var(--hf-ink);
  color: #fff;
}

.button-outline:is(:hover, :focus) {
  background-color: var(--hf-ink);
  color: #fff;
}

.button-outline:is(:hover, :focus) .button-icon-span {
  background-color: #fff;
  color: var(--hf-ink);
}

.button-primary {
  background-color: var(--green);
  border-color: var(--green);
}

.button-primary:is(:hover, :focus) {
  background-color: var(--hf-green-hover);
  border-color: var(--hf-green-hover);
  color: #fff;
  box-shadow: 0 10px 25px rgba(51, 159, 105, .3);
}

/*
 * The icon sits in a white disc pinned to the right of the pill rather than
 * riding in the flex line, which is why the label is pushed left and the button
 * reserves 65px on that side for it.
 */
.button.button-icon {
  justify-content: flex-start;
  padding-right: var(--hf-btn-icon-pad-r);
}

.button-icon-span {
  position: absolute;

  /*
   * Held at the single-line size and centred, rather than stretched to the
   * pill. It used to be top:6px + height:calc(100% - 12px), which tracked the
   * pill's height — fine while the pill could not grow, wrong the moment
   * min-height let it. A wrapped label made the pill 69px and the disc went
   * 50x34 to 50x57: the horizontal lozenge turned vertical, which is the exact
   * shape the box-sizing note above records as having been reported once
   * before.
   *
   * 16px, not 12: the percentage height resolved against the PADDING box
   * (50 - 4 of border = 46), so the disc was 34px, not 38. Keeping 34 is what
   * makes this a no-op for every button that was already correct.
   *
   * `translate`, not `transform`, because :hover below sets transform: scale()
   * and the two would overwrite each other. The individual property composes
   * with it instead.
   */
  top: 50%;
  translate: 0 -50%;
  right: 6px;
  width: var(--hf-btn-disc-w);
  height: calc(var(--hf-btn-h) - 16px);
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 50px;
  background-color: #fff;
  color: var(--hf-ink);
  /*
   * The disc needs the two colour properties for the same reason the pill needs
   * color. On .button-outline the disc inverts on hover - ink fill with a white
   * arrow becomes a white fill with an ink arrow - and with only transform and
   * box-shadow listed, that inversion landed in a single frame while the disc
   * was still easing through its scale. Listing them makes the disc fade with
   * the button instead of flipping ahead of it.
   */
  transition:
    transform .5s cubic-bezier(.76, 0, .24, 1),
    background-color .5s cubic-bezier(.76, 0, .24, 1),
    color .5s cubic-bezier(.76, 0, .24, 1),
    box-shadow .5s cubic-bezier(.76, 0, .24, 1);
}

.button:is(:hover, :focus) .button-icon-span {
  transform: scale(1.05);
  box-shadow: 0 4px 10px rgba(0, 0, 0, .08);
}

.button-icon-span svg {
  transition: transform .5s cubic-bezier(.76, 0, .24, 1);
}

.button:is(:hover, :focus) .button-icon-span svg {
  transform: rotate(-15deg) scale(1.1);
}

/*
 * SUPERSEDED — the button is one size now, on every screen.
 *
 * These were LeadXForms' own three steps, carried over with the component: the
 * disc narrowed 50 -> 40 -> 35 as the bar tightened and padding-right followed
 * it 65 -> 50, so the label kept the same clearance from the disc at every size.
 *
 * The trouble is that this page is not that bar. The same component is used
 * here for a hero CTA, a plan's Choose button and the header's Get Started,
 * and the steps made all of them shrink together — so a phone showed a visibly
 * smaller arrow than a desktop on buttons that had plenty of room for the full
 * one. Ahmed asked for the desktop button everywhere, and there is a second
 * reason to want it: 35px is under the 44px minimum for a touch target, and the
 * disc is the part of the button people aim at. The small size was landing on
 * exactly the devices where the target matters most.
 *
 * Nothing replaces them. .button-icon-span's own width: 50px and
 * .button.button-icon's padding-right: 65px above now apply at every width.
 *
 * @media (min-width: 992px) and (max-width: 1199px) {
 *   .button.button-icon .button-icon-span {
 *     width: 40px;
 *   }
 * }
 *
 * @media (max-width: 991.98px) {
 *   .button.button-icon .button-icon-span {
 *     width: 35px;
 *   }
 *
 *   .button.button-icon {
 *     padding-right: 50px;
 *   }
 * }
 */

/* ── Mega-menu feature cards ─────────────────────────────────────────────── */

.ac-card-nav {
  position: relative;
  background: #ffffff;
  border: 1px solid #E5E5E5;
  border-radius: 10px;
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  cursor: pointer;
  transition: box-shadow 0.2s, border-color 0.2s, transform 0.15s;
  z-index: 0;
}

.ac-card-nav:not(.disabled):hover {
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
  border-color: #bbf7d0 !important;
  transform: translateY(-1px);
}

/* The dropdown arrow. LeadXForms draws this with a Remix Icon glyph, which
   sits on the text baseline; an SVG does not, so it is nudged to the optical
   centre of the label beside it. */
.nav-chevron {
  flex-shrink: 0;
  margin-top: 1px;
}

/* ── The Get Started wave ────────────────────────────────────────────────── */

/*
 * Each letter is its own box with a copy of itself sitting directly underneath,
 * and hovering slides the whole strip up by exactly one line. The 0.03s per-index
 * delay is what turns ten letters flipping at once into a wave running left to
 * right.
 *
 * The button's overflow:hidden is what keeps the lower copy out of sight until
 * it is wanted.
 */
.button-text-wave {
  display: inline-flex;
  overflow: hidden;
  height: 24px;
  line-height: 24px;
}

.char {
  position: relative;
  display: inline-block;
  transition: transform .45s cubic-bezier(.76, 0, .24, 1);
  transition-delay: calc(var(--i) * .03s);
}

/* The second copy, drawn from the element's own data-char so the markup states
   each letter once. */
.char::after {
  content: attr(data-char);
  position: absolute;
  left: 0;
  top: 100%;
  width: 100%;
  height: 100%;
}

.button:hover .char {
  transform: translateY(-100%);
}

.char-space {
  display: inline-block;

  /*
   * em, not px. It was a flat 5px, which is right at the 18px this was built
   * at and wrong everywhere else — at the 15px the phone now uses, five of
   * them made the label measurably wider than the same text set normally, and
   * that was the last few pixels the guardian button was overflowing by.
   * 0.28em is 5.04px at 18, so desktop does not move.
   */
  width: 0.28em;
}

/*
 * The same wave, on a label built out of words rather than loose letters.
 *
 * The rules above clip at the strip: .button-text-wave is one line tall with
 * overflow hidden, which is all the header's "Get Started" ever needed. Every
 * other button on the page is wider or narrower than that one, so when site.js
 * builds a wave it groups the letters into words and moves the clip down to the
 * word. Each word is then its own one-line window and the effect is identical.
 * The header's hand-written markup has no .char-word and keeps the strip-level
 * clip, so nothing about it changes.
 *
 * flex-wrap IS THE ONE THAT MATTERS, and it is nowrap because a wrapped label
 * destroys the pill's shape.
 *
 * It read `wrap` before, back when a second line was the accepted outcome. That
 * is also why `white-space: nowrap` on .button was not enough on its own and the
 * guardian button still came out 64px tall at 360px: white-space governs line
 * breaking inside a text run, and there is no text run here — site.js has
 * replaced the label with a flex row of .char-word items, and a flex row breaks
 * on flex-wrap, not on white-space. The two have to agree.
 *
 * The word grouping stays, and still earns its place: every .char is an
 * inline-block, so without the wrapper the row's items would be LETTERS and any
 * future overflow would break "Guardian" in half rather than between words.
 */
.button-text-wave--words {
  height: auto;
  overflow: visible;
  flex-wrap: nowrap;
  justify-content: inherit;
}

.char-word {
  display: inline-flex;
  overflow: hidden;
  height: 24px;
  line-height: 24px;
}

/*
 * Out is not staggered. Running the wave backwards leaves the label still
 * settling long after the pointer has gone, which reads as lag rather than as
 * motion; dropping the delay brings every letter home together.
 */
.button:not(:hover) .char {
  transition-delay: 0s;
}

@media (prefers-reduced-motion: reduce) {
  .char {
    transition: none;
  }
}

/* ── Pricing, and the offer badge it carries ─────────────────────────────── */

/*
 * The badge is absolutely positioned against this link, so the link is the box
 * it hangs off. nowrap guards the word itself: a nav item that can fold in half
 * breaks the header rather than the item.
 */
.main-menu a.nav-pricing-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}

/*
 * A superscript on the link's top-right corner, hanging into header padding that
 * already exists — so it costs the nav row no width at all. That is the property
 * not to trade away: inline, this badge cost the row 83px and had to be paid for
 * by closing the gaps between every other nav item.
 *
 * left:62% rather than right:0. Anchored to the right edge the pill sits entirely
 * outside the word and drifts toward the buttons; overlapping the word's last
 * third reads as attached to it. bottom:calc(100% - 6px) laps it over the cap
 * height rather than floating it clear, which is what makes it a superscript
 * rather than a second row.
 */
.nav-offer-badge {
  position: absolute;
  bottom: calc(100% - 6px);
  left: 62%;
  top: auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 6px;
  border: 0;
  border-radius: 999px;
  background: #C4EFD8;
  color: #056B3E;
  box-shadow: none;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .02em;
  line-height: 1;
  white-space: nowrap;
  transition: background-color .3s, color .3s;
  animation:
    nav-offer-in .5s ease .8s backwards,
    nav-offer-flash 2.4s linear 1.3s infinite;

  /* A label, not a target: the cursor passes through to the link underneath, so
     hovering the pill hovers "Pricing". One target, which is why the hover rules
     below hang off the link rather than off this. */
  pointer-events: none;
}

/*
 * Withdrawn below 1200. The badge adds 83px to a row that is already over budget
 * at those widths, and a badge attached to a clipped link cannot be read anyway.
 */
@media (max-width: 1199.98px) {
  .nav-offer-badge {
    display: none;
  }
}

/*
 * Both colours are restated. Without the second line the badge inherits
 * .main-menu a's hover colour and the text shifts to a green the badge never
 * asked for while its background moves somewhere else.
 */
.main-menu a.nav-pricing-link:is(:hover, :focus, .active) .nav-offer-badge {
  background: #B3E8CA;
  color: #04552F;
}

/*
 * Reaching for the link stops the flash — someone with a cursor on "Pricing" has
 * already been got by it, and continuing to flash makes the offer harder to read
 * at the moment they are trying to read it.
 *
 * animation:none rather than play-state:paused, which would freeze the badge on
 * whatever frame it was passing through. Killing the animation hands the colours
 * back to the rule above, and the .3s transition carries them there.
 */
.main-menu a.nav-pricing-link:is(:hover, :focus) .nav-offer-badge {
  animation: none;
}

/*
 * The promotion and annual variants both wear the house promotion accent. Amber
 * is the only colour in the palette that is unmistakably neither the tint this
 * header wears at rest nor the white it turns once stuck, so it holds in both
 * states without needing a flash to carry it — which is why it drops the flash
 * and keeps only the fade-in.
 */
.nav-offer-badge--promotion,
.nav-offer-badge--annual {
  background: #FFB020;
  color: #0B1220;
  animation: nav-offer-in .5s ease-out .8s both;
}

.main-menu a.nav-pricing-link:is(:hover, :focus, .active) .nav-offer-badge--promotion,
.main-menu a.nav-pricing-link:is(:hover, :focus, .active) .nav-offer-badge--annual {
  background: #FF9800;
  color: #0B1220;
}

@keyframes nav-offer-in {
  0% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

/*
 * The beat, over 2.4s: a 0.24s attack, held to 0.82s while a halo pushes out and
 * fades, back to base at 0.96s, then 1.44s of rest so it reads as a beat rather
 * than a strobe.
 *
 * The colours STEP rather than cross-fade, and that is the load-bearing line.
 * Interpolating a fill one way while its text travels the other means the two
 * pass through each other — the worst frame measured 1.00:1, with the label
 * invisible twice per cycle. step-end makes that frame impossible rather than
 * merely brief.
 */
@keyframes nav-offer-flash {
  0% {
    background: #C4EFD8;
    color: #056B3E;
    box-shadow: 0 0 0 0 rgba(5, 107, 62, .38);
    animation-timing-function: step-end;
  }

  10% {
    background: #95E1B5;
    color: #04552F;
    box-shadow: 0 0 0 0 rgba(5, 107, 62, .34);
    animation-timing-function: ease-out;
  }

  34% {
    background: #95E1B5;
    color: #04552F;
    box-shadow: 0 0 0 8px rgba(5, 107, 62, 0);
    animation-timing-function: step-end;
  }

  40% {
    background: #C4EFD8;
    color: #056B3E;
    box-shadow: 0 0 0 0 rgba(5, 107, 62, 0);
  }

  100% {
    background: #C4EFD8;
    color: #056B3E;
    box-shadow: 0 0 0 0 rgba(5, 107, 62, .38);
  }
}

/* The page's own reduced-motion block already zeroes every animation, so the
   badge rests on its declared fill — legible, just not moving. */

/* ── Resources dropdown ──────────────────────────────────────────────────── */

.blog-preview-text {
  font-size: 13px;
  line-height: 1.4;
  color: #64748b !important;
}

.blog-action-link {
  width: max-content;
  padding-bottom: 2px;
  border-bottom: 2px solid #151823;
  font-size: 14px;
  font-weight: 700;
  color: #151823 !important;
  transition: color .2s ease;
}

.blog-action-link:hover {
  color: #008744 !important;
  border-color: #008744;
}

.mega-hdr i {
  font-size: 16px;
  color: #64748b;
}

@media (max-width: 991.98px) {}

/* ── The panel below xl ──────────────────────────────────────────────────── */

/*
 * Not a drawer: the bar is already the full width of the page, so the panel
 * opens under it rather than sliding in from a side. Hidden by default and
 * shown by .show, which site.js toggles alongside aria-expanded.
 */
/*
 * The panel sits OUTSIDE the header's .container — it is a sibling of it, not a
 * child — so it was getting no page gutter at all: measured at 390, every link
 * started at x=0 and both buttons ran 0..390, hard against the glass, while the
 * logo directly above them sat at 10 like the rest of the page.
 *
 * It takes the same --page-gutter every section uses, so the first letter of
 * "Home" lines up under the first pixel of the logo.
 */
.aside-menu {
  display: none;
  flex-direction: column;
  gap: 4px;
  padding: 20px var(--page-gutter) 8px;
}

.aside-menu.show {
  display: flex;
}

.aside-nav-menu {
  display: flex;
  flex-direction: column;
}

/* 18px, not the bar's 17px: LeadXForms sets no size on these either, so they
   take body's 18 while the desktop bar's links are explicitly 17. */
.aside-nav-menu a {
  padding: 12px 0;
  color: var(--hf-ink);
  font-size: var(--fs-hf-btn);
  text-decoration: none;
  transition: color .3s;
}

.aside-nav-menu a:is(:hover, :focus, .active) {
  color: var(--green);
}

.aside-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 12px;
}

.aside-actions .button {
  width: 100%;
}

/* ── Footer ──────────────────────────────────────────────────────────────── */

.main-footer {
  background-color: #fff;
  border-top: 1px solid var(--hf-line);
}

.footer-top {
  padding: 30px 0;
}

.footer-info {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.footer-logo {
  display: inline-block;
  width: var(--hf-footer-logo-w);
}

.footer-logo img {
  width: 100%;
  height: auto;
}

/* Full width rather than LeadXForms' 302px, which its own media queries
   override to 100% at every viewport anyway. */
.footer-info p {
  width: 100%;
  margin-bottom: 0;
  color: var(--hf-body);
  font-size: var(--fs-hf-body);
  line-height: 1.3;
}

.footer-head {
  margin-bottom: 35px;
}

.ft-heading {
  margin: 0;
  color: var(--hf-ink);
  font-size: var(--fs-hf-heading);
  font-weight: var(--fw-bold);
  line-height: normal;
}

.footer-links {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-links a {
  color: var(--hf-body);
  font-size: var(--fs-hf-body);
  font-weight: var(--fw-regular);
  text-decoration: none;
  transition: all .3s;
}

.footer-links a:is(:hover, :focus, .active) {
  color: var(--green);
}

/* ── Product badges ──────────────────────────────────────────────────────── */

.footer-link-with-badge,
.footer-link-disabled {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Two products carry a pill beside their name, so the pair is allowed to wrap
   onto a second line rather than push the column wider. */
.footer-link-with-badge {
  flex-wrap: wrap;
}

/*
 * A product that is named but not yet linkable. Not an <a>, because a link that
 * goes nowhere is worse than plain text for anyone navigating by links.
 *
 * 14px rather than the column's 16, and nowrap with it — both deliberate, and
 * LeadXForms says why in its own comment on this rule: the label is set smaller
 * than the other footer links specifically so a Coming Soon pill still fits
 * beside it in this narrow column. At 16px ‘LeadX Call Tracking’ folds and the
 * pill ends up alone on the second line, which is what it was doing here.
 */
.footer-link-disabled {
  color: var(--hf-body);
  opacity: .7;
  font-size: 14px;
  font-weight: var(--fw-regular);
  cursor: not-allowed;
  white-space: nowrap;
}

.footer-badge {
  padding: 3px 6px;
  border-radius: 4px;
  background-color: #e2fbf0;
  color: var(--green);
  font-size: 9px;
  font-weight: var(--fw-bold);
  line-height: 1;
  text-transform: uppercase;
}

/*
 * Smaller and quieter than the New pill — 8px against 9, grey against mint —
 * because it reports an absence rather than announcing something.
 *
 * text-transform:none is the one line that is not simply inherited: .footer-badge
 * above upper-cases, which suits NEW and turns this into COMING SOON — wider than
 * the column it has to fit in, and a shout about a product that is not shipping.
 * LeadXForms writes this badge inline and never applies the transform to it.
 */
.footer-badge--soon {
  padding: 2px 5px;
  border: 1px solid #e2e8f0;
  background-color: #f1f5f9;
  color: #64748b;
  font-size: 8px;
  font-weight: var(--fw-semibold);
  line-height: 1.4;
  text-transform: none;
  white-space: nowrap;
}

/* ── Socials and store badges ────────────────────────────────────────────── */

/*
 * The icons are drawn in currentColor rather than the black they carry in
 * LeadXForms' markup, so the hover below moves the glyph itself and not just its
 * opacity — and so a single colour change reaches all four.
 */
.social-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #000;
  transition: opacity .2s ease, color .2s ease;
}

.social-icon:hover {
  opacity: .7;
}

.footer-app-links .app-btn img,
.footer-mobile-app-stack .app-btn img {
  border-radius: 6px;
  object-fit: contain;
}

/*
 * The row under the columns: socials at one end, badges at the other. Shown from
 * lg up, where there is width for it; below that the same two blocks ride in the
 * "Follow Us" column instead, which is the only place a stacked layout has room
 * for them.
 */
.footer-bottom-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  margin-top: 56px;
}

/* LeadXForms puts these on the elements as mt-3 / mb-3; stated here because
   only the grid build is loaded and its spacing utilities are not the full
   set. 1rem either side, which is what mt-3 and mb-3 are. */
.footer-socials {
  margin-top: 1rem;
  margin-bottom: 1rem;
}

/* ── Bottom bar ──────────────────────────────────────────────────────────── */

.footer-bottom {
  padding: 20px 0;
  border-top: 1px solid #f1f5f9;
  background-color: #fff;
}

.footer-bottom-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 15px;
  text-align: center;
}

.copyright-text {
  margin: 0;
  color: var(--hf-body);
  font-family: var(--font-ui);
  font-size: var(--fs-hf-small);
  font-weight: var(--fw-regular);
  line-height: normal;
}

.footer-bottom-links {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px 12px;
}

.footer-bottom-links a {
  color: var(--hf-body);
  font-family: var(--font);
  font-size: 13px;
  font-weight: var(--fw-regular);
  line-height: 20px;
  text-decoration-line: underline;
  text-decoration-color: var(--hf-body);
  text-underline-offset: 3px;
}

.footer-bottom-links a:hover {
  color: var(--green);
  text-decoration-color: var(--green);
}

.link-separator {
  color: var(--hf-body);
}

/* At md and up the bar spreads: copyright left, links right. */
@media (min-width: 768px) {
  .footer-bottom-inner {
    flex-direction: row;
    justify-content: space-between;
    text-align: left;
  }

  .footer-bottom-links {
    justify-content: flex-end;
  }
}

/* ── Footer, below desktop ───────────────────────────────────────────────── */

/*
 * LeadXForms' own footer breakpoints, at its own widths rather than this file's.
 * They were missed on the first pass, which left a 224px logo in a viewport
 * narrower than that and the social row at its desktop size on a phone.
 */

@media (min-width: 992px) and (max-width: 1199px) {
  .footer-logo {
    width: 180px;
  }

  .ft-heading {
    font-size: 22px;
  }
}

@media (max-width: 991.98px) {
  .footer-logo {
    width: 180px;
    margin: 0;
  }

  .ft-heading {
    font-size: 20px;
  }

  /* The description runs the column's full width here — the 302px it carries on
     desktop is most of a phone screen. */
  .footer-info {
    align-items: flex-start;
    margin-bottom: 30px;
    text-align: left;
  }

  .footer-info p {
    width: 100%;
    max-width: 100%;
    text-align: left;
  }

  /* Badges stack under the icons rather than sitting beside them. */
  .footer-mobile-app-stack {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
    margin-top: 15px;
  }

  .footer-mobile-app-stack .app-btn img {
    display: block;
    width: 120px;
    height: auto;
  }
}

/* ── The Bootstrap utilities the mega-menus use ──────────────────────────── */

/*
 * LeadXForms loads the whole framework; this page loads only bootstrap-grid,
 * which is 51 KB against 227. The header markup reaches for 31 utility classes
 * that live in the other 176 KB, and these are those 31, copied at Bootstrap's
 * own values.
 *
 * Writing them out rather than loading the full build saves 176 KB of CSS for
 * roughly forty lines, and none of the rest of that file is used anywhere on
 * this page.
 */

.badge {
  display: inline-block;
  padding: .35em .65em;
  border-radius: .375rem;
  font-size: .75em;
  font-weight: 700;
  line-height: 1;
  text-align: center;
  white-space: nowrap;
  vertical-align: baseline;
}

.bg-white {
  background-color: #fff;
}

.border {
  border: 1px solid #dee2e6;
}

.border-0 {
  border: 0;
}

.fs-6 {
  font-size: 1rem;
}

.fw-semibold {
  font-weight: 600;
}

.fw-bold {
  font-weight: 700;
}

.gap-1 {
  gap: .25rem;
}

.gap-2 {
  gap: .5rem;
}

.gap-3 {
  gap: 1rem;
}

.h-100 {
  height: 100%;
}

.position-static {
  position: static;
}

.rounded {
  border-radius: .375rem;
}

.rounded-4 {
  border-radius: 1rem;
}

.text-dark {
  color: #212529;
}

.text-muted {
  color: #6c757d;
}

.text-secondary {
  color: #6c757d;
}

.text-decoration-none {
  text-decoration: none;
}

.text-uppercase {
  text-transform: uppercase;
}

/* ═══════════════════════════════════════════════════════════════════════════
   MEGA MENU
   LeadXForms' header dropdown system, ported whole from MasterLayout.vue's
   style block. Kept as it is written there rather than rewritten to this
   file's conventions — the point of it is to match, and a rule that has been
   tidied is a rule that has been changed.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ======================================================= */
/* 1. HEADER & STICKY GLOBAL RULES */
/* ======================================================= */
header {
  position: relative;
  /* relative boundary focus for full-width components */
}

/* ======================================================= */
/* 2. NAVIGATION ITEM BASE STYLING & POSITIONING */
/* ======================================================= */
.mega-menu-item {
  position: relative;
  /* Default context standard templates layout ke liye */
  padding: 15px 0;
}

/* Integrations menu helper: Relative scope remove karke pure screen border par push karne ke liye */
.mega-menu-item.position-static {
  position: static !important;
}

/* ======================================================= */
/* 3. DROPDOWNS TIMINGS & BEHAVIORS */
/* ======================================================= */
/* Base dropdown attributes */
.mega-dropdown {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%) translateY(15px);
  width: 650px;
  background: #fff;
  border-radius: 12px;
  z-index: 9999;
  opacity: 0;
  visibility: hidden;
  transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1) !important;
  /* Synced duration */
  text-align: left;
}

/* Full screen custom parameters for integrations */
.mega-dropdown-full {
  left: 0 !important;
  right: 0 !important;
  width: 100% !important;
  /* opacity: 1 !important; */
  /* visibility: visible  !important; */
  transform: translateY(0) !important;
  display: block !important;
  /* layout and design inspect karne ke liye */
  /* transform: translateY(15px) !important; */
  border-radius: 0px !important;
  /* Edge to edge visual screen sync */
  border-top: 1px solid #f1f5f9 !important;
  border-bottom: 1px solid #e2e8f0 !important;
}

/* ── The Industries mega-menu ─────────────────────────────────────────────
 *
 * Three bands: a header that says what the ten pages have in common, the
 * columns, and a footer for the reader none of them fit. The bands run
 * edge to edge and hold their own .container, so their rules line up with
 * the columns while their backgrounds do not stop at the container's edge.
 */

/*
 * Neutral at rest, brand green on the row's hover.
 *
 * Ten green tiles at once would make every row look equally urgent and the
 * column read as a wall — the colour is worth more spent on the one row the
 * pointer is actually on. The driver is .mega-featured-link-card:hover, which
 * every other menu already uses, so the tile lights with the row background
 * rather than on its own.
 */

.mega-featured-link-card:hover .ind-row-icon {
  background: #dcf0e5;
  color: #008744;
}

/* The shared rule next door keys off .fw-bold.text-dark, which these rows do
   not carry — they use one class instead of two utilities. */
.mega-featured-link-card:hover .ind-row-name {
  color: #008744;
}

/* A claim, not an ornament — see the note on `badge` in industryGroups. */

@media (prefers-reduced-motion: reduce) {}

/*
 * The Industries trigger is a button, because it opens a menu and does not go
 * anywhere — there is no hub page for it to point at. `all: unset` first, so it
 * arrives with none of the browser's button chrome, then it takes whatever the
 * <Link> triggers beside it are given: the .main-menu rules style those by
 * element, and a <button> is not an <a>.
 */
.main-menu .mega-menu-trigger {
  all: unset;
  box-sizing: border-box;
  cursor: pointer;
  font: inherit;
  color: inherit;
}

/*
 * Focus opens the dropdown as well as hover.
 *
 * These menus were hover-only, so nothing in them could be reached from a
 * keyboard at all — the links exist in the DOM, they are focusable, and the
 * panel holding them stays visibility:hidden while you tab through it. One
 * selector fixes all five, and costs a mouse user nothing.
 */
.mega-menu-item:focus-within .mega-dropdown,
.mega-menu-item:focus-within .mega-dropdown-full {
  opacity: 1;
  visibility: visible;
}

/* Trigger display states on hover */
.mega-menu-item:hover .mega-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}

.mega-menu-item:hover .mega-dropdown-full {
  opacity: 1;
  visibility: visible;
  transform: translateY(0) !important;
}

/* Mega-menu description lines.
   These render database copy of unbounded length. Without a clamp a single long
   description stretches its row to 4+ lines and the whole column goes ragged —
   invisible against short local seed data, obvious on production where real
   template descriptions run several sentences. Held to a single line, matching
   the integrations column, so every row is the same height whatever the copy.
   Deliberately no title attribute on these elements: a tooltip repeating the
   truncated text just covers the menu while the user is trying to read it. */

/* Titles stay on one line for the same reason — a long form name like
   "Customer Service Review Form" would otherwise wrap and push the row taller
   than its neighbours. Ellipsis rather than a hard cut so it reads as truncated. */

/* ======================================================= */
/* 4. HOVER TRANSITIONS (PADDING & BACKDROPS) */
/* ======================================================= */
/* Column 1 Links */

/* Column 2 Links */

.mega-featured-link .m-icon-box {
  transition: all 0.3s ease-in-out !important;
}

.mega-featured-link:hover .m-icon-box {
  border-color: #00874450 !important;
  box-shadow: 0 4px 12px rgba(0, 135, 68, 0.08) !important;
}

/* Icon + title + description row (e.g. Popular Templates) — the whole row
   highlights on hover instead of just the icon/text color changing. */
.mega-featured-link-card {
  /* Negative left/right margin cancels the card's own padding so its icon
       lines up flush with the column header above it (which has no side
       padding of its own) — the hover background still gets the padded,
       rounded-corner look without shifting the content to the right. */
  padding: 10px 12px;
  margin: 0 -12px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  transition: background-color 0.2s ease-in-out;
  text-decoration: none;
}

.mega-featured-link-card:hover {
  background-color: #EDF7F2;
}

.mega-featured-link-card:hover .fw-bold.text-dark {
  color: #008744 !important;
}

/* Category rows in the Templates mega-menu: the hovered row stays highlighted
   while the pointer moves across to read the templates it revealed in column 2,
   so it always stays obvious which category the list belongs to. */

.mega-cat-row-active .fw-bold.text-dark {
  color: #008744 !important;
}

/* Column 2 is re-keyed per category, so each swap re-mounts these rows and
   replays the fade — a plain swap read as a jarring flicker. */

@keyframes megaTplFade {
  from {
    opacity: 0;
    transform: translateY(4px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {}

/* Grid Separators */
.border-end {
  border-right: 2px solid #f1f5f9 !important;
}

/* ======================================================= */
/* 3a. FEATURES MEGA MENU — FEATURED PRODUCT CARDS (PREMIUM) */
/* ======================================================= */
.mega-feat-card {
  position: relative;
  border-radius: 14px;
  padding: 16px;
  border: 1px solid #edf1f5 !important;
  overflow: hidden;
  z-index: 0;
  transition: all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1) !important;
}

/* Soft brand-tinted sheen that fades in on hover, sits behind content */
.mega-feat-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(135deg, rgba(0, 135, 68, 0.06), rgba(21, 24, 35, 0.03));
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

/* Subtle arrow affordance revealed on hover, no extra markup needed */
.mega-feat-card::after {
  content: "\2192";
  position: absolute;
  right: 18px;
  top: 50%;
  transform: translate(6px, -50%);
  opacity: 0;
  color: #008744;
  font-size: 18px;
  font-weight: 700;
  transition: all 0.3s ease-in-out;
}

.mega-feat-card:hover {
  border-color: #d7f5e4 !important;
  box-shadow: 0 12px 28px rgba(15, 23, 42, 0.09);
  transform: translateY(-3px);
}

.mega-feat-card:hover::before {
  opacity: 1;
}

.mega-feat-card:hover::after {
  opacity: 1;
  transform: translate(0, -50%);
}

.mega-feat-card-icon {
  transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}

.mega-feat-card:hover .mega-feat-card-icon {
  transform: scale(1.08);
  box-shadow: 0 6px 16px rgba(15, 23, 42, 0.12);
}

/* ======================================================= */
/* 5. MOBILE VIEW PROTECTION CODE */
/* ======================================================= */
/* Screen resolutions check for megamenu display */
@media (max-width: 991.98px) {
  .mega-dropdown {
    display: none !important;
    /* Disables floating desktop panels in mobile layout */
  }
}

/* Vertical dividers inside dynamic grid */
.border-end {
  border-right: 1px solid #f1f5f9 !important;
}

body.down {
  padding-top: 80px;
}

.border-end {
  border-right: 1px solid #f1f5f9 !important;
}

.mega-hdr {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  opacity: 0.8;
}

.mega-featured-link .m-icon-box {
  background: #fff;
  border: 0px;
  width: 35px;
  height: 35px;
  display: flex;
  border-radius: 10px;
  gap: 3px;
  border: none;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.mega-featured-link .m-icon-box img {
  width: 35px;
  height: 35px;
  border-radius: 10px;
  object-fit: contain;
}

/*
 * LeadXForms leaves this class unstyled — the link picks up .main-menu a and
 * reads as dark, the same weight as the four cards above it. Given its own
 * rule here so the column's one real link is the one thing in it wearing the
 * brand colour, which is what it is for.
 */
.mega-see-all {
  font-size: 13px;
  font-weight: 600;
  color: var(--green);
}

.main-menu .mega-see-all:is(:hover, :focus) {
  color: var(--hf-green-hover);
}

@media (max-width: 991.98px) {
  .mega-dropdown {
    display: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE
   Two breakpoints, and each one opens with a :root block that redeclares the
   sizes for that width. Everything after it is reflow — stacking, alignment,
   anything that changes shape rather than scale.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 991.98px) {
  :root {
    /* Redeclared here rather than computed, so every size on the page is still
       a number someone can read. 66px of headline is 1/6 of a 390px screen. */
    --fs-h1: 44px;
    --fs-lede-strong: 19px;
    --fs-lede: 17px;
    --fs-stat-figure: 68px;

    --hero-pad-top: 64px;
    --gap-micro-trust: 44px;
    --gap-trust-shot: 40px;
  }

  :root {
    /*
     * 40 below lg, and the same 40 for every section that is not the hero, the
     * header or the footer.
     *
     * Four different tokens set this — --section-pad-y for six sections, and one
     * each for the tinted panel, the FAQ and the closing card — and they had
     * drifted to three different numbers on a phone: 64, 48 and 36. A page whose
     * gap between bands changes from band to band reads as unfinished however
     * carefully each band is built, which is the same reasoning that took the
     * side gutter to one value.
     *
     * They stay four tokens rather than becoming one, because above lg they are
     * genuinely different — 96 for a plain section against 80 for a panel — and
     * that is the comp's own decision.
     */
    --section-pad-y: 40px;
  }

  /* Stacked now, so the card no longer needs to hold a column's width — it
     centres instead of sitting against the left edge of a full-width row.

     It keeps --stat-card-w rather than taking max-width:100%, which is what it
     had: the artwork is drawn 294 across, so a full-width column stretched it
     to 360 on a 390px phone, and a chart 22% bigger than it was drawn reads as
     the loudest thing in the section — over the claim it is there to illustrate.

     That the file is now an SVG does not retire this cap, it just changes which
     half of the argument it answers. Stretching used to cost sharpness AND
     proportion; the vector fixed the sharpness and the proportion is untouched,
     so the cap is still what keeps the copy leading. */
  .stat-card {
    max-width: var(--stat-card-w);
    margin-inline: auto;
  }

  /* The card's 56px inset is a quarter of a tablet's width. */
  :root {
    --engine-card-pad: 32px;
    --fs-engine-title: 34px;

    /* The copy comes down with the heading. 22 was set against a 600px measure;
       the column is about half that here and the paragraph reads as oversized
       long before it stops fitting. */
    --fs-engine-desc: 18px;
    --fs-engine-list: 18px;
    --fs-engine-note-title: 18px;
    --fs-engine-note-body: 16px;

    /* The panel's own rhythm comes in with it. 35 between six stacked blocks is
       most of a phone screen spent on air. */
    --gap-engine-panel: 24px;
    --gap-engine-tabs: 28px;
    --engine-list-indent: 32px;
    --engine-note-indent: 14px;
  }

  /* Stacked, so the diagram follows the copy it illustrates rather than
     sitting beside a column that is now the full width. */
  .engine-visual {
    order: 1;
  }

  .engine-diagram {
    max-width: 480px;
  }

  :root {
    --gap-faq-list: 56px;
    --faq-pad-y: 40px;
    --gap-cta-card: 64px;
    --cta-pad-y: 40px;
    --cta-card-pad-y: 48px;
    --cta-card-pad-x: 24px;
    --cta-radius: 24px;
  }

  .faq-title,
  .faq-cta-title,
  .faq-cta-lede {
    max-width: none;
  }

  :root {
    --diff-panel-pad-y: 40px;
    --gap-diff-cards: 44px;
  }

  /*
   * Plans joins the house ramp. 50px is the desktop figure off the comp and
   * every other section title steps to 34 here (--fs-faq-title, --fs-diff-title
   * above, --fs-guardian-title at 36); left at 50 the heading was 180px tall at
   * 768 and 420px tall at 320 — four lines of a two-line headline.
   *
   * The measure goes with it. 950px was the comp's line length on a 1440 rail;
   * below lg the container is narrower than that, so the cap only stops the
   * heading centring against its own box.
   */
  :root {
    --gap-plans-grid: 44px;
    --gap-plans-yearly-top: 36px;

    /* 48px of inset on each side is a tenth of a tablet's width spent twice. */
    --plans-yearly-pad: 36px;

    /* The cards stack from md down, so the featured one no longer stands
       between two shorter neighbours — its extra inset only makes it taller
       than the two it is meant to match. */
    --plan-pad-featured: 32px;
  }

  .plans-title {
    max-width: none;
  }

  /*
   * One column, not two. col-md-6 put Basic and Premium side by side and left
   * Elite alone in a half-width row with an empty half beside it, while the
   * centred row staggered the two — the featured card is taller by design, so
   * a two-up row can never line their heads up. The cards are col-12 col-lg-4
   * now, and this caps the card rather than the column so it does not run to
   * 738px on a tablet; a plan card reads as a card, not a band.
   */
  /* Superseded - .plan centres itself at every width now, so this rule had
     nothing left to say.

  .plans-grid .plan {
    margin-inline: auto;
  } */

  .diff-title {
    max-width: none;
  }

  /* The comp breaks each heading after its second word, which only holds at the
     width the comp draws. In a wider column that break falls mid-phrase, so the
     <br> is taken out of play and the heading finds its own. */
  .diff-card-title br {
    display: none;
  }

  /*
   * col-12 takes the halves to full width on its own, so there is no middle to
   * the row any more. The centre line is not declared at this width at all — it
   * lives in the min-width: 992px block, on .deep-grid — so there is nothing to
   * switch off for it here.
   *
   * What does stand down is the row's own boundary. Stacked, the copy and the
   * picture sit one above the other and each closes with its own full-width
   * rule; the row drawing one more across its bottom would land on top of the
   * picture's and read as a heavier line on every boundary. So the cells own
   * the horizontal at this width and the row does not — the opposite of the
   * desktop arrangement, and one source either way.
   */
  .deep-row::after {
    display: none;
  }

  /*
   * The copy cell keeps its rule.
   *
   * It used to be hidden here on the grounds that a line between the copy and
   * the picture "cuts the entry in half" once they stack. On the desktop grid
   * the two cells sit side by side and each draws HALF of the row's bottom
   * rule; stacked, they sit one above the other, so those two halves become two
   * full-width rules — one closing the copy, one closing the picture. That is
   * the same ruled grid the comp draws, just read down a column instead of
   * across a row, and without it the entry loses half its structure.
   */

  /*
   * A wash that fades across the cell has nothing to fade toward once the cell
   * is the full width of the page.
   *
   * .deep-shot is no longer in this list. It was here because it used to carry
   * that fading gradient; it now carries a flat --wash-base, which is what keeps
   * the artwork off the section wash — and that is as true stacked as it is in
   * two columns. Clearing it here would have left the bug on a phone only.
   */
  .deep-copy,
  .deep-row--mirror .deep-copy {
    background: none;
  }

  /*
   * The diamonds stay.
   *
   * On the desktop grid they mark where the vertical rule crosses a horizontal
   * one. There is no vertical rule here, but they are still centred on each
   * horizontal one and the motif is what carries the section's look from one
   * width to the next — so they are kept rather than dropped with the rule that
   * happened to pass through them.
   */

  /*
   * The picture cell gives up its side padding; the artwork already has its own.
   *
   * Every feature-0*.svg carries a transparent margin for its drop shadow —
   * measured by scanning the painted pixels, 28 units of a 539-wide box on each
   * side, so 5.2% of however wide it is drawn. The cell was then adding 20px on
   * top of that, and the card's visible edge ended up well inside the text above
   * it: at 390 the copy started at x=20 and the card at x=36.
   *
   * It cannot be made exact — the artwork's inset is a PROPORTION of its width
   * while the text's is a fixed 20px, so one value cannot satisfy both at every
   * width. Dropping the cell's padding gets it close and never overshoots:
   *
   *     390 -> card at 18 against text at 20
   *     540 -> card at 26
   *     768 -> card at 37  (it was 55)
   *
   * It also buys the artwork the full width of the column, which is the whole
   * point of it on a phone.
   */
  .deep-shot {
    padding-inline: 0;

    /*
     * The bottom padding gives back what the artwork's own margin already takes.
     *
     * Those transparent margins are not symmetric: measured, 16 units above the
     * painted card and 29 below, of a 539-wide box. With equal padding top and
     * bottom the card therefore sits high in its cell — 13 units of a 539 box,
     * or 2.42% of however wide it is drawn, too far up.
     *
     * Subtracting that percentage balances it at EVERY width, which a fixed
     * number cannot: the artwork's margin is a proportion of its width, so the
     * correction has to be one too. 28px top against 19.7 at 390, 16.1 at 540
     * and 10.6 at 768 — all of which put equal air above and below the card.
     */
    padding-block: 28px calc(28px - 2.42%);
  }

  /*
   * The definitions block fills the cell instead of hugging its longest line.
   *
   * .deep-copy is a flex column with align-items: flex-start, so every child is
   * only as wide as its own content. On desktop the longest line is wider than
   * the padded cell, so the block filled it and nothing showed. Stacked it does
   * not: measured at 768, the cell is 720 wide and the block came out 602 — and
   * because the What/How/Why rules are drawn on this block, they stopped 118px
   * short of the column and read as unfinished.
   *
   * Stretching only moves the block's edges. The paragraphs already fit inside
   * 602, so no line gets longer; the rules just reach the end of the row.
   */
  .deep-defs {
    /*
     * width goes with it. align-self: stretch only applies when the cross size
     * is auto, so the desktop 472px would have won here and done two things
     * wrong at once: left the rules short of the column, and -- under 472 --
     * pushed the whole row wider than the screen.
     */
    width: auto;
    align-self: stretch;
  }

  /*
   * The outer border is kept on all four sides, as the comp has it.
   *
   * It was dropped here because the cells had lost their side padding, so the
   * copy ran to the very edge of the column and a border there would have had
   * the text sitting against it. The answer is the padding, not the border —
   * see --deep-cell-pad below, which gives the text its inset back.
   *
   * .deep-grid {
   *   border-left: 0;
   *   border-right: 0;
   * }
   */

  :root {
    /*
     * Side padding is back, because the border is. 0 was fine while the cells
     * had no edge to sit against; with the outer border restored the text would
     * touch it. 20 is the smallest inset that still reads as an inset at this
     * width — the page gutter outside the grid is already doing the heavy work.
     */
    --deep-cell-pad: 28px 20px;
    --fs-deep-title: 22px;

    /* One column, so a cell is only as tall as its own copy and there is no
       picture beside it to match. A 500px floor here would open a gap under
       every short entry. */
    --deep-cell-min-h: 0;
  }

  /* Three claims will not sit on one line here, and wrapping them 2 + 1 looks
     accidental. Left-aligning turns the row into a list, which reads as
     deliberate at this width. */
  .hero-trust {
    justify-content: flex-start;
    gap: 14px 32px;
  }

  /* The card's 80px inset is a fifth of a tablet's width, and the code panel
     drops under the copy it illustrates rather than squeezing beside it. */
  :root {
    --guardian-card-pad: 44px;
    --guardian-card-radius: 28px;
  }

  .guardian-row {
    row-gap: 40px;
  }

  /*
   * Stacked, the code panel fills the card instead of centring in it.
   *
   * Centring was the earlier answer to "there is no right edge to hang from",
   * but it gave the panel a rail of its own: measured at 768, the eyebrow, the
   * headline, the claims and the button all start at x=68 while the panel
   * started at 159 — 91px in from everything above it, because a 450px box was
   * being centred in a 632px card.
   *
   * Lifting the cap is the simpler answer. The panel is a code snippet; wider
   * only means the lines have more room, and both its edges then agree with the
   * copy. Below 540 it was already filling — 450 is wider than the card there —
   * so this only changes the band where the mismatch was visible.
   */
  /*
   * The dashboard's tab strip scrolls instead of spilling.
   *
   * site.js already scrolls the chosen tab into view and its comment says "on a
   * narrow screen, where the strip scrolls sideways" — but the CSS never made it
   * scrollable. .dash-tabs was overflow-x: visible, so the tabs simply spilled
   * past the card and .action's overflow: hidden cut them off. Measured at 390:
   * the five labels need 518px, the card gives 368, and "Login Activity" and
   * "Reports" were 150px of nothing.
   *
   * Same treatment as the Engine's layer strip, so the two read as one idea:
   * one row, scrolled, no scrollbar — the half-cut label at the edge is what
   * says there is more.
   */
  .dash-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;

    /*
     * Snapping has to be told about the strip's own padding.
     *
     * A snap point is aligned to the edge of the SCROLLPORT, which sits inside
     * the padding — so the browser scrolled the first tab's 24px inset away to
     * meet it and "Overview" ended up against the card's edge. Measured at rest:
     * scrollLeft 24, first tab 0px from the strip edge.
     *
     * scroll-padding holds that inset back. It mirrors the padding above, so the
     * two move together if either changes.
     */
    scroll-padding-inline: 24px;

    scrollbar-width: none;
  }

  .dash-tabs::-webkit-scrollbar {
    display: none;
  }

  .dash-tab {
    flex: 0 0 auto;
    scroll-snap-align: start;
  }

  /*
   * The card's copy centres once the columns stack.
   *
   * On the desktop card the copy is a left column with the code panel beside it,
   * so a left rail is what holds the two together. Stacked there is no second
   * column to hold anything to — the copy is simply the top of a card — and a
   * centred block reads as composed where a left-aligned one reads as a column
   * that lost its partner. Same treatment the Stats section already takes when
   * it stacks.
   *
   * .guardian-head sets align-items: flex-start, which pins each child to the
   * left edge whatever its text-align says, so that has to move too.
   */
  .guardian-head {
    align-items: center;
    text-align: center;
  }

  /*
   * Even the headline's lines instead of filling each one before breaking.
   *
   * Greedy wrapping packs every line to the margin and leaves whatever is left
   * on the last one — centred, at 390, that put the word "it." alone under two
   * full lines. text-wrap: balance asks the browser to even the lines out, which
   * is what a centred heading needs to look deliberate.
   *
   * Browsers without it (and any headline over ~6 lines, which this is not) just
   * wrap greedily as before, so there is nothing to fall back to.
   */

  .guardian-lede {
    text-align: center;
  }

  /*
   * The claims are the exception, and deliberately so.
   *
   * Each one is a tick and a sentence, and the tick is positioned against the
   * line box's left edge. Centring the TEXT would leave three ticks at three
   * different x positions, which reads as a broken list rather than a centred
   * one. So the list is centred as a BLOCK — shrink-wrapped to its longest claim
   * and placed in the middle — while each claim keeps reading from the left.
   */
  .guardian-claims {
    align-self: center;
    text-align: left;
  }

  :root {
    --guardian-panel-w: none;
  }

  /*
   * The code scrolls as one block, not one line at a time.
   *
   * .guardian-line carries its own overflow-x: auto, so every line was its own
   * scroll container. Nothing overflows on desktop — measured, 448px of panel
   * against 448px of code, exactly — so it never showed there. On a phone it
   * does: the widest line needs 336px and the panel gives 292 at a 390 viewport,
   * 262 at 360, 222 at 320. Per-line scrolling means reading that line pushes it
   * out of step with the ones above and below, and indentation is most of what
   * makes a snippet legible.
   *
   * Moving the scroller to the body keeps the lines locked together. The padding
   * stays on the lines so the inset travels with the code rather than being left
   * behind at the panel's edge.
   */
  .guardian-line {
    overflow-x: visible;
  }

  .guardian-panel-body {
    overflow-x: auto;
    overscroll-behavior-x: contain;
  }

  .guardian-visual {
    justify-content: flex-start;
  }

  :root {
    --dash-pad: 18px;
    --fs-dash-title: 18px;
  }

  /* The three passages stack, and the rules between them go with them: a rule
     down the left of a full-width block is not a divider, it is a quote mark.
     The centring stays — it is what the comp specifies, and the column is still
     narrow enough here for it to read. */
  :root {}

}

/*
 * "Australia 1k customers" is the longest label on the map and its dot is the
 * furthest right, so beyond this width the two together run past the map and
 * give the whole PAGE a horizontal scrollbar — the label is absolutely
 * positioned, so nothing wraps or clips it, it just widens the document.
 *
 * It flips to the dot's left rather than being shortened or hidden: the dot is
 * the thing that has to stay on its coordinate, and the label only has to be
 * next to it. Above 1200 it stays on the right, which is where the comp draws it.
 *
 * Measured at a 992 viewport before this rule: label ran to x=1003 in a 992px
 * window. Same cause as .geo-pin--above, which already flips the Kamchatka one
 * upward for want of room.
 */
@media (max-width: 1199.98px) {
  .geo-pin--flip .geo-pin-label {
    left: auto;
    right: calc(100% + 10px);
  }
}

@media (max-width: 575.98px) {
  :root {
    --fs-h1: 36px;
    --fs-lede-strong: 18px;
    /*
     * 18, asked for directly. Worth knowing what it costs: --fs-lede-strong is
     * also 18 here, so the strong lede and the plain one are now the same SIZE
     * on a phone and only the weight separates them. The note on
     * .hero-lede-strong describes the emphasis as "the weight and the two extra
     * points" — the two points are gone at this width. Raise this block's
     * --fs-lede-strong to 20 to put them back.
     */
    --fs-lede: 18px;
    --fs-stat-figure: 56px;

    --hero-pad-top: 48px;
  }

  /*
   * Superseded. The reasoning below was about layout and ignored what the card
   * actually is: a 294 x 360 RASTER. Lifting the cap did not let it fill the
   * column, it let the browser enlarge a fixed number of pixels —
   *
   *     390px screen -> drawn at 342, a 1.16x upscale
   *     540px screen -> drawn at 492, a 1.67x upscale
   *
   * and that is before the phone's own pixel ratio, which multiplies it again:
   * on a DPR 2 handset the 540 case is asking a 294px file to cover 984 physical
   * pixels. The legend inside the artwork is small type, so it is the first thing
   * to go soft — which is exactly what showed up in the screenshot.
   *
   * With the cap back the card draws at its own size and stays sharp. It is
   * already centred by the margin-inline: auto one breakpoint up, so on a phone
   * it sits centred above the copy rather than stretched across it.
   *
   * To have it fill the column and stay crisp the artwork needs a 2x export
   * (588 x 720) behind a srcset — then this cap can be lifted for real.
   *
   * .stat-card {
   *   max-width: 100%;
   * }
   */

  :root {
    --engine-card-pad: 24px;
    --engine-card-radius: 20px;
    --fs-engine-title: 30px;
    --fs-engine-desc: 16px;
    --fs-engine-list: 16px;
    --fs-engine-note-title: 16px;
    --fs-engine-note-body: 15px;
    --engine-list-indent: 32px;
    --engine-note-indent: 12px;

    /* Five tabs across a phone: 18 a side is 180px of the row spent on padding
       before a single word is drawn. */
    --engine-tab-pad: 10px 12px;
  }

  /*
   * The scroll sequence is turned off below this width, and that is deliberate.
   * Five screens of pinned scrolling on a phone reads as a page that has stopped
   * responding — the thumb moves and nothing arrives. The tabs already do the
   * same job by tapping, so the track collapses to the card's own height and the
   * section behaves like every other one.
   */

  /*
   * Five pills will not sit on one row at this width, so the strip wraps onto as
   * many rows as it needs.
   *
   * What was here came from Figma and describes the PILL, not the strip that
   * holds five of them — and applied to the strip it did something far worse
   * than look wrong:
   *
   *     height: 40px;
   *     padding: 6px 12px;
   *     flex-direction: column;
   *     justify-content: center;
   *
   * A COLUMN flex container that is allowed to wrap and is only 40px tall cannot
   * wrap downwards — there is no room — so it wraps SIDEWAYS instead, starting a
   * new column to the right for every pill. Measured at a 390 viewport: the five
   * tabs ran from x=60 to x=555 and put a horizontal scrollbar on the WHOLE page,
   * which made every other section look broken while it was being tested.
   *
   * The padding is dropped too: .engine-tab already carries its own 10px 18px,
   * and the strip adding 12 more only narrowed the space the pills wrap inside.
   */

  /* The scroll-at-native-scale workaround that was here belonged to the raster
     export. A vector stays sharp at any size, so the shot simply shrinks —
     and it has to, because a frame the page can scroll inside of is not a
     frame. The border thins so 9px does not eat a 350px screen. */
  .hero-shot {
    --shot-border: 5px;
    --shot-radius: 14px;
  }

  /* On a phone the card's own inset is most of what is left after the page
     margin, so it gives most of it back. */
  :root {
    --guardian-card-radius: 20px;
    --fs-guardian-claim: 15px;

    /* One point off the comp's 13, which takes the widest line from 336px to
       310 — most of the overflow, without making a snippet unreadable. */
    --fs-guardian-code: 12px;

    /* 32 between five labels is 128px of a 368px strip spent on gaps. 20 keeps
       them clearly separate and puts a whole extra tab in view. */
    --gap-dash-tabs: 20px;
    --gap-eyebrow-title: 16px;
    --gap-guardian-card: 36px;
  }

  :root {
    --fs-faq-q: 16px;
    --fs-faq-a: 15px;
    --cta-card-pad-y: 36px;
    --cta-card-pad-x: 18px;
  }

  /* The label is three words and the screen is not wide; let it wrap rather
     than run off the card. */

  /*
   * Phone step, matching --fs-faq-title / --fs-diff-title / --fs-guardian-title,
   * which all sit at 26 here. The panel gives back most of its inset for the
   * same reason the guardian card does: 36px twice out of a 290px panel leaves
   * 218px of usable line.
   */
  :root {
    --fs-plans-yearly-title: 22px;
    --fs-plans-price: 34px;
    --fs-plan-name: 22px;
    /* Steps with --fs-plans-price above, keeping the card's figure and the
       annual band's figure the same size on a phone as they are on a desktop.
       The period stays at 14 - it is already the smallest thing on the card. */
    --fs-plan-price: 34px;
    --fs-plan-price-symbol: 19px;
    --plan-pad: 24px;
    --plan-pad-featured: 24px;
    --plans-yearly-pad: 24px;
    --gap-plans-yearly: 24px;
  }

  :root {
    /* Superseded — every section shares 40 below lg now, and 36 would put this
       one band back out of step with the rest.

       --diff-panel-pad-y: 36px; */
    --diff-card-pad: 24px;

    /* A 70px tile is a fifth of the card's width at this size, sitting above a
       heading half its height. 56 keeps it the card's anchor without being the
       loudest thing in it. */
    --diff-icon-size: 56px;
  }

  /* The label is four words long and the screen is not. Wrapping it is better
     than a button that runs off the edge, so the nowrap from the base rule is
     lifted and the button takes the width it has. */
  /* Full width on a card this narrow. The label is five words, and at 390px it
      wraps however wide the button is — which used to be the problem, because
      the component's height was fixed and the second line was clipped. The
      component takes min-height now, so it grows instead; this rule is back to
      being only about width. */
  .guardian-card .button {
    /*
     * Off full width, and centred instead.
     *
     * It was 100%, which on a 298px card made the CTA the widest thing in the
     * section — wider than the claims above it — and a button that fills its
     * container reads as a form submit rather than as one more step in the copy.
     * auto lets it size to its own label.
     *
     * align-self and not margin-inline: .guardian-copy is a flex column with
     * align-items:flex-start, so without this the button would simply move from
     * filling the card to sitting hard against its left edge. align-self is the
     * one property that overrides that alignment for a single item.
     */
    width: auto;
    align-self: center;
  }

  :root {
    --dash-pad: 14px;
    --fs-dash-row: 14px;
    --fs-dash-tab: 14px;
  }

  /*
   * The row's three columns become two lines: the label above, the value and
   * the timestamp together below it. Held as a grid rather than allowed to wrap
   * freely, so the value still sits at the right edge and the timestamps still
   * line up as a column — which is the whole reason the row is a grid.
   */
  .dash-row {
    grid-template-columns: 1fr auto;

    /*
     * 8 down the row, not 4.
     *
     * The row's own padding is 12, so at 4 the status sat closer to the path
     * above it than the path sat to the rule above THAT — every row read as top
     * heavy, and the two lines inside one entry read as one block rather than as
     * a name and its state. 8 is still tighter than the padding, which is what
     * keeps the pair reading as one entry.
     */
    gap: 8px 12px;
    padding: 12px 14px;

    /*
     * baseline, not centre, for the second line.
     *
     * The status is 13px and the timestamp 14, and centring two different sizes
     * sits them on two different baselines — near enough to look like a mistake
     * rather than far enough to look deliberate. Aligning on the baseline is
     * what puts them on one line.
     *
     * It costs the first line nothing: .dash-row-name spans both columns, so it
     * has nothing to align against.
     */
    align-items: baseline;
  }

  .dash-row-name {
    grid-column: 1 / -1;
  }

  .dash-row-value {
    text-align: left;
  }

  /* The pill drops under the title rather than squeezing beside it. */
  .dash-panel-head {
    flex-direction: column;
    align-items: flex-start;
  }

  :root {
    --fs-geo-label: 14px;
  }
}

/* Respect a system-level request for less motion: every transition above is
   decorative, and none of them carry meaning. */
/* ═══════════════════════════════════════════════════════════════════════════
   HERO ON A PHONE
   Measured, not assumed. At a 390 viewport the hero was 1122px tall — the
   dashboard it is selling started below two full screens of scroll — and the
   eyebrow's separators had turned into bullets. These are the three things that
   caused it.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * 1. THE EYEBROW'S DOTS BECOME BULLETS WHEN IT WRAPS.
 *
 * The separator is drawn by span + span::before, so it belongs to the phrase
 * AFTER it. On one line that puts a dot between two phrases, which is the point.
 * When the row wraps, the phrase moves to a new line and takes the dot with it —
 * to the START of that line, where it reads as a list bullet. The first line
 * never gets one, so it does not even read as a consistent list.
 *
 * Measured: one line at 768 and up, two lines from 480 to 700, three at 414 and
 * below. So 768 is where it starts, and 768 is where the row becomes a column
 * and the dots go. Stacking is deliberate; two phrases sharing a line with no
 * separator between them would just look broken.
 *
 * Same reasoning .hero-trust already uses one breakpoint up.
 */
/* Superseded — the stack now starts at 480, and the separator moved to ::after
   so a wrapped line never begins with a dot. Kept as the record of why 768 was
   the wrong line: it was where wrapping starts, not where stacking helps.

@media (max-width: 767.98px) {
  .hero-eyebrow {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }

  .hero-eyebrow span + span::before {
    display: none;
  }
}
*/

@media (max-width: 575.98px) {

  /*
   * 2. THE DESKTOP RHYTHM ON A PHONE SCREEN.
   *
   * .hero-copy stacks five blocks with a 32px gap, and .hero-content puts 24
   * between the eyebrow and the headline. That is 152px of deliberate air — on a
   * 390px-wide screen it is most of a screenful, and it is why the shot sat so
   * far down. The proportions that make the desktop hero breathe are the ones
   * that make the phone hero scroll.
   */
  /*
   * The hero keeps its 32px gap all the way down. It was stepping to 20 here,
   * which is the gap every OTHER section's heading stack uses — so on a phone
   * the hero read like the rest of the page instead of opening it.
   */

  :root {

    /*
     * 3. THE TAP TARGET.
     *
     * 40px is the comp's pill height and it is right with a mouse. Under a
     * thumb it is below both platform minimums — 44pt on iOS, 48dp on Android —
     * and this is the page's primary action. It only moves on touch-sized
     * screens, so the comp still holds everywhere it was drawn for.
     */
    --btn-h: 48px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE PAGE RAIL BELOW 992
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Bootstrap's container steps; the viewport does not. Between two steps the
 * container holds still while the screen keeps growing, and the difference
 * turns into margin. Measured, content width and margin each side:
 *
 *     767px screen -> 516 content, 126 margin   <- a column in a field of nothing
 *     744px screen -> 516 content, 114 margin
 *     700px screen -> 516 content,  92 margin
 *     576px screen -> 516 content,  30 margin
 *     575px screen -> 551 content,  12 margin   <- and now it is too tight
 *
 * So the same file has both faults: a tablet wastes a third of its width, and a
 * phone has 12px between the text and the edge of the glass. 540 — the width
 * this was reported at — sits right in the middle of it.
 *
 * Below 992 the container goes fluid with one constant gutter instead. The
 * handover is smooth by measurement, not by luck: 991 gives 943 content against
 * 992's 936, so nothing jumps as the breakpoint is crossed.
 *
 * This is the one place this page's container deliberately differs from
 * LeadXForms'. Above 992 — every width the comps were drawn at — it is still
 * Bootstrap's, untouched.
 */
@media (max-width: 991.98px) {
  .container {
    max-width: none;
    padding-inline: var(--page-gutter);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   HERO EYEBROW SEPARATORS
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The dot moves from ::before to ::after — from the phrase that follows it to
 * the phrase that precedes it.
 *
 * It matters only when the row wraps, and then it decides everything. On
 * ::before the dot belongs to the phrase that moved, so it lands at the START of
 * the new line and reads as a list bullet — with no bullet on the first line, so
 * not even a consistent one. On ::after it stays at the END of the line it came
 * from, where a trailing separator reads as "this continues", which is what it
 * means.
 *
 * Nothing changes on one line: the dot still sits between two phrases.
 */
/*
 * The dot is Ahmed's 8x8 mark: <circle cx="4" cy="4" r="4" fill="black"/> in an
 * 8x8 box. It is drawn here rather than embedded as that SVG because a circle
 * of r=4 filling an 8x8 box IS border-radius:50% on an 8x8 box — the same
 * pixels, with no data URI to download, nothing to blur at fractional device
 * pixel ratios, and no second copy of the shape to keep in step.
 *
 * `black` rather than the currentColor it used to inherit, because the mark
 * specifies black. The eyebrow text is #151515, so the two are now a shade
 * apart; that is deliberate and it is the one thing to undo if the dot is ever
 * wanted on a dark surface, where a hard black would disappear and
 * currentColor would not.
 */
.hero-eyebrow span:not(:last-child)::after {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-left: 14px;
  border-radius: 50%;
  background: #000;
  vertical-align: middle;
}

.hero-eyebrow span+span::before {
  content: none;
}

/*
 * Below this two phrases stop fitting on one line together — measured, the first
 * two need 388px of column and 480 is where the column stops providing it — so
 * every line would be one phrase with a trailing dot. At that point the dots are
 * decorating nothing and a plain stack is cleaner.
 *
 * Above it the row wraps to two lines and keeps its separators, which is why the
 * stack no longer starts at 768: at 540 that cost a third line for nothing.
 */
@media (max-width: 479.98px) {
  .hero-eyebrow {
    display: none;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }

  .hero-eyebrow span:not(:last-child)::after {
    display: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   HERO STRUCTURE ON A PHONE
   Four blocks, in this order, and nothing else: headline, copy, two full-width
   buttons, the checklist. Every colour, size, weight and background below is
   this page's own — only the arrangement comes from the reference.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 575.98px) {

  /*
   * A. The page opens on the headline — but it keeps its trust line.
   *
   * .hero-eyebrow used to be hidden here alongside .hero-micro, for two reasons
   * that were not the same reason. The micro line's was duplication: it and the
   * eyebrow both said "No Credit Card", and one of the two had to go on a screen
   * this narrow. The eyebrow's was only cost — three lines above the fold — and
   * that is a trade, not a defect. It is wanted, so it is back.
   *
   * Nothing had to be built to bring it back, and that is worth knowing before
   * anyone "fixes" it further: the responsive behaviour was already written and
   * already measured. Above 480 it is a wrapped row that keeps its dots, below
   * it a plain stack that drops them — see the note on the 479.98 block, which
   * records why 480 is the line (two phrases stop fitting together at 388px of
   * column). display:none was sitting on top of all of it.
   *
   * .hero-micro stays hidden. Showing it again would put "No Credit Card" on the
   * screen twice, which is the one thing this rule was right about.
   */
  .hero-micro {
    display: none;
  }

  /*
   * B. Both actions are buttons, stacked, full width.
   *
   * The secondary was a bare text link with an arrow — right beside a filled
   * button on a desktop row, where two filled buttons would compete. Stacked on
   * a phone that reasoning inverts: a centred line of text under a solid green
   * bar does not read as the second of two choices, it reads as a caption. So it
   * gets a body of its own — white ground, our own --hf-line border, our own ink.
   *
   * The radius is the one thing taken from the reference, and it is not foreign:
   * 50px is what LeadXForms' own .button uses in the header of this very page.
   */
  .hero-actions {
    flex-direction: column;
    /*
     * THE PAIR IS AS WIDE AS ITS LONGEST LABEL, AND NO WIDER.
     *
     * fit-content sizes this column to its widest child, and stretch then gives
     * that width to BOTH — so the two buttons match without a min-width anyone
     * has to keep in step with the type. The label is what sets it, and the type
     * steps again at 360, so any number written here would be wrong at one of
     * the two sizes.
     *
     * margin-inline centres the pair; max-width is the floor under it, so on a
     * screen narrower than the longest label the group stops at the column
     * rather than running past it.
     *
     * It was align-items:stretch with width:100% — equal, but equal to the whole
     * column, which is a pair of bars rather than two buttons.
     */
    align-items: stretch;
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    gap: 12px;
  }

  /*
    * Width only.
    *
    * The pill radius, the height, the 2px border and the white-on-dark of the
    * secondary were all written out here when these were two bespoke buttons.
    * They are the header's component now and it carries every one of them, so
    * restating them would only be a second place to keep in step.
    */
  .hero-actions .button {
    /* The column's stretch sets the width now; nothing to declare here. */
    width: auto;
  }

  /*
   * C. The checklist closes the block.
   *
   * One claim per line, left-aligned on the same rail as everything above it.
   * The 56px column gap and the centring are a desktop row's, and a column
   * inherits neither usefully — justify-content in a column is vertical, so the
   * base rule's "center" was doing nothing here at all.
   */
  .hero-trust {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--gap-trust-row);

    /*
     * The list lives in its own .row, outside .hero-copy, so none of that
     * column's 20px gap reaches it — measured, it sat flush against the bottom
     * of the secondary button with a gap of exactly 0 and read as a fourth line
     * of that button rather than as the block after it.
     */
    margin-top: var(--gap-actions-trust);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   STATS, STACKED
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Below lg the chart and the copy stop being two columns and become two blocks,
 * and the order flips: the claim first, the picture that illustrates it second.
 * That is the markup's own order now — see the comment on the columns — so this
 * block only has to deal with how the copy sits.
 */
@media (max-width: 991.98px) {

  /*
   * The figure takes a line of its own. It is a <span> inside the heading, so
   * inline it shares a line with the words after it and "43% of the entire" runs
   * together at one size while the rest sits at another. As a block it is what it
   * reads as: the number, then the sentence about it.
   *
   * fit-content, and it is not cosmetic — it is what keeps the gradient right.
   *
   * .stat-figure paints its colour with background-clip: text, so the gradient is
   * sampled across the BOX, not across the glyphs. A plain block box is the full
   * column: measured, 360px at 390 and 738px at 768, against 108px and 132px of
   * actual text. The number therefore sampled 30% of the gradient on a phone and
   * 18% on a tablet, and both of those slices are taken from the MIDDLE, because
   * the heading is centred here — so neither the #0066DB the sweep starts at nor
   * the pale it ends at ever reached the screen. "43%" came out a flat mid-blue
   * with no sweep in it at all, which is what was reported.
   *
   * Shrinking the box to the text puts it back to what the inline version does
   * at desktop, where box and glyphs are the same 193px and the gradient runs
   * exactly once across the number. margin-inline keeps it on the centred axis
   * the rest of this block sets.
   */
  .stat-figure {
    display: block;
    width: fit-content;
    margin-inline: auto;
  }

  /*
   * Centred, and everything under it with it. A centred number over left-aligned
   * words would read as a mistake rather than as a choice, and the chart below is
   * centred already — so the whole stack shares one axis.
   */
  .stat-title,
  /*
   * The heading joins the centring the rest of this block already does.
   *
   * .stat-figure is centred just above and both .stat-lede paragraphs are
   * centred just below, but .stat-title carries text-align:left from its base
   * rule and nothing here was overriding it — so the figure sat in the middle,
   * the sentence under it sat hard left, and the two paragraphs came back to the
   * middle again. Desktop is untouched: there the copy is a left column and left
   * is right.
   */
  .stat-title {
    text-align: center;
  }

  .stat-lede {
    text-align: center;
  }

  /*
   * The comp breaks the sentence after "web" with a typed <br>. That break was
   * measured against a 856px column at desktop; centred in a phone's, it lands
   * mid-phrase and leaves one short line stranded under a long one. Let the text
   * find its own endings at this width — the <br> is still there for lg and up.
   */
  .stat-title-text br {
    display: none;
  }
}

/*
 * The sticky section heading that was here has been removed.
 *
 * It worked, but it cost 179px of every screen for the whole 3768px section —
 * and the stacking the rows now do needs that space: at a 900px viewport the
 * tallest row is 586 and the header takes 74, which leaves 240px for the whole
 * stack. A pinned heading would have eaten three quarters of that.
 *
 * It also never sat flush: .header-sticky animates the bar from 94px down to 74,
 * so whatever a script publishes as its height depends on which frame it read —
 * 94, 80 and 84 across three runs — and every one of them left a window between
 * the bar and the heading with a feature row sliding through it.
 *
 * --header-h-sticky (site.js) is left in place: it is the correct number for
 * anything that pins below the bar, and it is measured with a ResizeObserver
 * rather than guessed from a timer.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   THE MAP'S PINS ON A SMALL SCREEN
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The pins and their labels are fixed sizes on a map that is not.
 *
 * Measured, the widest label — "Australia 1k customers" — against the map it is
 * drawn on:
 *
 *     1440 -> map 1296x581, label 147px = 11% of it
 *      768 -> map  738x331, label 147px = 20%
 *      390 -> map  360x161, label 147px = 41%
 *
 * At 41% five labels cannot share a map without running into each other and off
 * the edge, whatever is done to their positions. They step down here, and below
 * 576 they go entirely: at 161px of map height there is no size at which five of
 * them fit, and a legible pin says more than an illegible caption.
 *
 * The pins stay, and so does what they mean — green for customers, red for
 * blocked, and the What / How / Why beneath the map spells it out in words.
 */
@media (max-width: 991.98px) {
  :root {
    --fs-geo-pin: 11px;
    --geo-pin-size: 9px;
  }
}

@media (max-width: 575.98px) {
  :root {
    --geo-pin-size: 8px;
    --fs-geo-pin: 10px;
  }

  /*
   * The labels stay. They were hidden outright here, which left a phone with ten
   * coloured dots and nothing saying what any of them meant — the section's
   * point, read off the map, was desktop-only.
   *
   * What could not fit was two of the five, so those two swap to their short
   * form and the other three are unchanged.
   */
  .geo-pin-full {
    display: none;
  }

  .geo-pin-short {
    display: inline;
  }
}

/*
 * The stacking-cards effect that was here has been reverted.
 *
 * It worked, and the reason it could not stay is the one thing stacking cannot
 * do without: every card has to be OPAQUE, or the rows behind show through and
 * the text piles into itself. LeadXForms' own version says the same in its
 * comment — "solid background ... so cards hide each other when stacking".
 *
 * This section's ground is not a flat colour. .deep-blurs sits at z-index -1
 * with three coloured blur SVGs on it, and the cells carry their own washes over
 * that. Painting each row white to make it stack painted all of that out — the
 * blurs were still there, measured 1440x1464, 1440x1814 and 1440x1026, simply
 * covered.
 *
 * So the two are mutually exclusive here: stacked rows OR the coloured ground,
 * not both. The ground is the section's design, so it wins. The scroll reveal
 * below stays — it never needed an opaque row, because it moves the halves
 * rather than hiding what is behind them.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   FEATURE DEEP-DIVES — THE REVEAL
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Each half of a row arrives from its own side as it scrolls in.
 *
 * Why not the stacking-card effect: these rows are not cards. They are one
 * ruled grid — each cell draws half of the rule that closes its row, and the
 * diamonds sit where those rules meet. Peeling them into a stack would mean
 * giving every row an opaque background and a shadow, and the grid that holds
 * the section together would be the first thing to go.
 *
 * What the layout already has is a zig-zag: copy left / picture right, then the
 * mirror, six times. So the reveal follows it — each half enters from the edge
 * it sits against, which reads as the row assembling itself rather than as a
 * generic fade someone applied to everything.
 *
 * Driven by animation-timeline: view(), so the browser ties it to the element's
 * own trip through the viewport. No scroll listener, no IntersectionObserver, no
 * JavaScript at all — and it runs on the compositor, which is the difference
 * between this and a scroll handler on a page that already has one.
 *
 * The picture trails the copy — it starts 7% later and finishes later — so the
 * two do not arrive as one block. Both are compositor-only properties (opacity
 * and transform); nothing here touches layout.
 */
@keyframes deep-in {
  from {
    opacity: 0;
    transform: translate(var(--deep-x, 0), var(--deep-y, 0)) scale(var(--deep-scale, 1));
  }
}

/*
 * @supports, and it matters: the rule sets opacity to 0 at the start of the
 * animation, so a browser that cannot run the animation must never see the rule
 * at all — otherwise it renders a section of invisible text. Inside the guard,
 * the only way to be hidden is to be animatable back.
 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    .deep-copy,
    .deep-shot {
      animation: deep-in linear both;
      animation-timeline: view();
    }

    .deep-copy {
      --deep-x: -34px;
      animation-range: entry 5% cover 26%;
    }

    .deep-shot {
      --deep-x: 34px;
      --deep-scale: .97;
      animation-range: entry 12% cover 34%;
    }

    /* Mirrored rows put the picture on the left, so the two swap sides. */
    .deep-row--mirror .deep-copy {
      --deep-x: 34px;
    }

    .deep-row--mirror .deep-shot {
      --deep-x: -34px;
    }

    /*
     * Stacked, there are no sides to come from — both halves are the full width
     * of the column, and sliding one sideways across a phone reads as a glitch
     * rather than as a reveal. They rise instead.
     */
    @media (max-width: 991.98px) {

      .deep-copy,
      .deep-shot,
      .deep-row--mirror .deep-copy,
      .deep-row--mirror .deep-shot {
        --deep-x: 0;
        --deep-y: 26px;
      }
    }
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE LAYER STRIP
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * 1400, not a phone breakpoint. Five pills at their natural widths need 495px of
 * strip, and measuring what the copy column actually gives:
 *
 *     1440 -> 580   one row
 *     1400 -> 580   one row
 *     1320 -> 490   TWO rows, and it misses by five pixels
 *     1200 -> 490   two rows
 *      992 -> 400   two rows
 *
 * So the ragged 3 + 2 was not a mobile problem at all — it was on every laptop
 * from 992 to 1399. Above 1400 the comp's own layout holds, tabs hugging their
 * labels with the rest of the column empty beside them, and that is left alone.
 */
@media (max-width: 1399.98px) {

  /*
   * A segmented control: five equal parts of one row, never two.
   *
   * Wrapping was the obvious fix and it looked like a mistake — five pills at
   * their natural widths need 495px and the card gives 294, so they broke 3 + 2
   * with a ragged second row and a gap where the sixth would be. Nothing about
   * that says "five layers"; it says the row did not fit.
   *
   * flex: 1 1 0 makes every tab exactly one fifth of whatever is there, so the
   * strip reads as one deliberate control at every width and the count — which is
   * the whole claim of this section — is visible at a glance. min-width: 0 is
   * required with it: a flex item will not go below its content width without it,
   * which is what would push the row wide again.
   */
  .engine-tabs {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: var(--gap-engine-tab);
    height: auto;
    padding: 0;

    /*
     * One row, and it scrolls rather than wraps or squeezes. Five labels need
     * 487px at this gap; the copy column gives 490 at 1200 and 294 on a phone —
     * so above a laptop's width nothing moves, and below it the strip carries
     * what does not fit instead of the card doing something about it.
     */
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;

    /* The bar itself is noise inside a control this small; the half-cut pill at
       the edge is what says there is more, and it says it without taking 15px. */
    scrollbar-width: none;
  }

  .engine-tabs::-webkit-scrollbar {
    display: none;
  }

  /*
   * 1 0 auto, not 1 1 0.
   *
   * Grow to share whatever room there is — that is what keeps the strip looking
   * like one control at 1200 and at 768 — but never shrink below the label,
   * which is what "0" in the shrink slot buys. With 1 1 0 the tabs squeezed to
   * a fifth of the card whatever was written on them, and on a phone that fifth
   * was 52px for a word that needs 62.
   */
  .engine-tab {
    flex: 1 0 auto;
    min-width: fit-content;
    display: inline-flex;
    align-items: center;
    justify-content: center;

    /*
     * The space between "Layer" and its number.
     *
     * The label is two spans so the number can be styled on its own, and the
     * space lives at the end of the first one. Making the button a flex
     * container turns both spans into flex ITEMS, and white-space processing
     * strips a trailing space at the end of a box — so the button rendered
     * "Layer1". The DOM still says "Layer 1", so this is only about what is
     * drawn; em-based so it tracks the font rather than a fixed 4px.
     */
    gap: .3em;
    min-height: var(--engine-tab-h);
    scroll-snap-align: start;
    white-space: nowrap;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE LAYER STRIP, STACKED
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The strip gets a track.
 *
 * Five outlined boxes sitting alone at the top of the card read as five separate
 * things that happen to be near each other — and with nothing above them to sit
 * against, as five things floating. A track turns them into ONE control: the
 * container carries the shape, the segments inside carry none, and the selected
 * one is a filled pill within it. That is the segmented control every phone OS
 * uses, and it is legible at a glance as "pick one of these".
 *
 * Only where the strip runs the full width of the card at the top of it. From
 * 1400 up the comp's own treatment holds — outlined pills hugging their labels.
 */
@media (max-width: 991.98px) {
  .engine-tabs {
    padding: var(--engine-strip-pad);
    border-radius: var(--engine-strip-radius);
    background: var(--engine-strip-bg);
  }

  .engine-tab {
    border-color: transparent;
    border-radius: var(--engine-seg-radius);
  }

  /*
   * The hover fill must never reach the selected tab, and on a touch screen it
   * must never happen at all.
   *
   * .engine-tab:hover and .engine-tab.is-active have the SAME specificity, and
   * this rule sits later in the file — so it was winning, and the chosen tab
   * turned grey the moment a pointer was over it. On a phone that is not a
   * moment: a tap leaves :hover stuck on the element until something else is
   * touched, so the tab you just picked sat grey while the green never showed.
   * Measured at 390: the active tab's background read rgba(255,255,255,.07).
   *
   * (hover: hover) keeps the affordance for devices that actually have a
   * pointer; :not(.is-active) keeps it off the selected tab on those.
   */
  @media (hover: hover) {
    .engine-tab:hover:not(.is-active) {
      background: var(--engine-seg-hover);
    }
  }
}

/*
 * The number-only treatment that was here is gone: the tab reads "Layer 3" at
 * every width now.
 *
 * It was a way of fitting five labels across a 294px card, and it did fit — but
 * a strip of bare numbers directly above a panel headed "01: PREVENT" says the
 * same number twice and neither of them says what it is. The strip scrolls
 * instead, and keeps the word.
 *
 * @media (max-width: 479.98px) {
 *   .engine-tab-num::before { content: "0"; }
 * }
 */

/* ═══════════════════════════════════════════════════════════════════════════
   THE ENGINE ON A PHONE — LENGTH
   Measured at 390: the section ran 1608px, about two screens. Every trim below
   is spacing or repetition, not content.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 991.98px) {

  /*
   * The panel grid sizes to the OPEN layer, not to the tallest of the five.
   *
   * All five share one grid cell, so the cell is as tall as the longest — and it
   * stays that tall whichever is open. Measured at 390: the cell is 631 (layer 3)
   * while layer 1 needs 578 and layer 5 needs 548, so between 53 and 83px of the
   * card is empty for four layers out of five.
   *
   * That was the right trade on desktop and it is written down there: a sticky
   * card takes its height from its content, so a height change mid-scroll moves
   * the thing the user is looking at. Below lg the card is not sticky — there is
   * nothing to jar — so the panels leave the flow instead of merely hiding.
   *
   * display, so the closed panels stop contributing height. The fade goes with
   * it; a display change cannot be transitioned, and on a tap rather than a
   * scroll there is nothing for a fade to smooth over.
   */
  .engine-panel {
    display: none;
  }

  /*
   * The fade comes back as an ANIMATION, not a transition.
   *
   * The closed panels leave the flow here — that is what lets the card size to
   * the open layer instead of the longest one — and `display` is not a property
   * a transition can interpolate. So the panel appeared at full opacity and the
   * layer switch snapped on a phone while it crossfaded on desktop. Measured by
   * sampling opacity through a switch: desktop ran 0.00 - 0.11 - 0.47 - 0.74 -
   * 1.00, a phone ran 1.00 the whole way.
   *
   * An animation on the element that BECOMES active needs no interpolation from
   * display:none — it simply starts when the class lands. The outgoing panel is
   * not faded out: both share one grid cell, so holding the old one visible
   * while the new one arrives would briefly size the card to whichever is taller
   * and jump its height. Fading only the arrival reads the same and costs
   * nothing.
   *
   * The page's reduced-motion block already caps animation-duration, so this
   * respects that setting without a rule of its own.
   */
  .engine-panel.is-active {
    display: flex;
    animation: engine-panel-in var(--engine-panel-fade) ease;
  }
}

@media (max-width: 575.98px) {
  :root {
    /* 24 between six stacked blocks is 120px of the card spent on air. */
    --gap-engine-panel: 16px;
    --gap-engine-tabs: 20px;
  }

  .engine-head {
    margin-bottom: 40px;
  }

  .deep-head {
    margin-bottom: 40px;
  }

  .guardian-lede {
    font-size: 15px;
  }

  .cta-lede {
    font-size: 15px;
  }

}

@keyframes engine-panel-in {
  from {
    opacity: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ENGINE, UNPINNED
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The scroll track collapses here rather than at 576, where it used to.
 *
 * The original reasoning still holds — five screens of pinned scrolling on a
 * phone reads as a page that has stopped responding, the thumb moves and nothing
 * arrives — but it was drawn at the wrong line. The mechanism needs the card to
 * FIT the viewport, because the card is what gets pinned while the track scrolls
 * past underneath it. Measured card heights:
 *
 *     1440 -> 764    fits
 *      992 -> 722    fits
 *      768 -> 1282   does not fit anything, and the tabs are off the bottom
 *
 * Between 576 and 991 the track therefore held 3600px of scroll for a card that
 * could not be seen in one piece: roughly 2300px of the section was empty travel.
 *
 * The tabs do the same job by tapping, so below lg the track collapses to the
 * card's own height and the section behaves like every other one.
 */
@media (max-width: 991.98px) {

  /*
   * padding-inline, not the flat `padding: 10px`.
   *
   * The shorthand sets all four sides, so it also replaced this section's
   * padding-block — 64px, the same as every section around it — with 10. The
   * result was measured: .stats ended at y=1807 and this section's heading
   * started at 1817, ten pixels later, while .stats and .deep both keep 64 above
   * and below. The band read as jammed against its neighbours.
   *
   * The 10 was only ever about the left and right, so that is all it sets now.
   */
  /*
   * Superseded — --page-gutter is the rail for every section now, so this one no
   * longer needs its own. Left as the record of where the number came from.
   *
   * .engine {
   *   padding-inline: 10px !important;
   * }
   */

  /*
   * ...and the container stops adding the page gutter on top of it.
   *
   * .container carries 24px of its own wherever it sits, so the card was
   * starting 34px from the glass — 10 from the section, 24 from the container —
   * and its own 24px of padding put the copy at 58. That is the "kafi zayada"
   * that was reported; the 10 was never reaching the edge.
   *
   * Zeroing the padding alone would break it, though: every Bootstrap .row
   * carries a negative side margin of half the gutter, and with no padding left
   * to absorb it the row would hang 12px OUTSIDE the container. So the gutter
   * goes to zero with it — that zeroes the row's negative margin and the
   * columns' padding in one move, and they cancel exactly instead of by luck.
   *
   * .engine-row's 48px row-gap is declared on its own and is untouched by this.
   */
  /*
   * Superseded with the rule above: the container's padding IS the 10px rail
   * now, so zeroing it here would take the Engine back to running edge to edge.
   *
   * .engine .container {
   *   padding-inline: 0;
   * }
   */

  /*
   * The gutter has to be zeroed on the ROW, not on the container above it.
   * Bootstrap's .row re-declares --bs-gutter-x on itself, so setting it on the
   * container never reaches the row — measured: the container read 0 while the
   * row still read 1.5rem, kept its -12px side margins, and hung 2px past the
   * viewport with nothing left to absorb it.
   *
   * 0px, not 0: the column padding is calc(var(--bs-gutter-x) * .5), and a
   * unitless zero makes that calc produce a number rather than a length, which
   * is an invalid declaration the browser drops.
   */
  /*
   * Superseded. This existed to stop the row's negative side margin hanging out
   * of a container with no padding left to absorb it. With the container back on
   * the page rail there is padding again, and .page-wrap's overflow-x: clip
   * takes the last couple of pixels — verified, no horizontal scroll at any
   * width.
   *
   * .engine .row {
   *   --bs-gutter-x: 0px;
   * }
   */

  /*
   * The tabs move above the panel they control.
   *
   * On the desktop card they sit at the foot of the copy column, level with the
   * bottom of the diagram beside it, and the whole card is in view — so tapping
   * one shows you the change. Stacked, that stops being true: measured at a
   * 390 x 844 viewport, the panel's top edge is 671px ABOVE the tab strip. Tap
   * Layer 3 with the tabs in view and everything that changed is off the top of
   * the screen; the page appears to do nothing.
   *
   * order, not markup: the control belongs after the panels in the DOM either
   * way (it already does on desktop), and this is purely about what a thumb can
   * see. margin-top: auto is what pinned it to the foot of the flex column, so
   * it has to go with it.
   */
  /*
   * The strip sits below the panel at this width, and it needs no rule to say
   * so any more: it is a child of .engine-panels now, after the five panels, so
   * the order is structural.
   *
   * Kept because the reasoning is the point. It was once moved ABOVE the panel,
   * on the grounds that tapping a tab changes copy that is above it and mostly
   * off-screen. That still happens — but the diagram sits directly below the
   * strip and its highlighted layer changes too, so a tap lands next to
   * something the thumb can already see. That is enough feedback.
   *
   * .engine-tags needed height:auto here for the same kind of reason, and no
   * longer does: the base rule is min-height now, not a flat 40.
   */

  .engine-track {
    height: auto;
  }

  .engine-sticky {
    position: static;
    min-height: 0;
  }
}

/*
 * The Engine unpins on a viewport that is too SHORT, whatever its width — the
 * same rule the 992 breakpoint applies for width.
 *
 * The mechanism pins the card and scrolls the track past it, so the card has to
 * fit between the header and the bottom of the screen. It is 764px tall and the
 * header takes 94, and measured against real viewport heights the tab strip —
 * the last thing in the card and the control for the whole section — leaves the
 * screen just under 802px:
 *
 *     900 tall -> tabs end at 823   fits
 *     820 tall -> tabs end at 802   fits
 *     800 tall -> cut by 2px
 *     740 tall -> cut by 62px
 *     660 tall -> cut by 142px
 *
 * A 1366x768 laptop gives about 640px of viewport, so this is not an edge case.
 * Below the threshold the track collapses and the tabs do the work, exactly as
 * they do on a phone. site.js reads .engine-sticky's computed position to decide
 * whether to drive the layers from scroll, so it follows this with no second
 * copy of the number.
 */
@media (max-height: 819.98px) {
  .engine-track {
    height: auto;
  }

  .engine-sticky {
    position: static;
    min-height: 0;
  }
}

@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;

    /* Without this a looping animation does not stop, it runs a hundred
       thousand times a second on a .01ms cycle — no motion to see and a core
       spinning to produce it. The map's pins are the first thing on this page
       to animate forever, which is what turned that into a real cost. */
    animation-iteration-count: 1 !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ONE SCALE BELOW DESKTOP
   ═══════════════════════════════════════════════════════════════════════════

   Every section heading and every section lede on the page takes its size from
   this one block once the viewport drops under 992. Desktop is deliberately not
   here: above 991 each section keeps the size it was drawn at, which is what
   was asked for and what the comp specifies.

   WHY IT IS ONE BLOCK AND NOT TWENTY-ONE DECLARATIONS. That is what it was.
   The seven heading tokens each stepped on their own, in seven different media
   blocks, to four different numbers — 38, 36, 34 and 28 at 991, then 30, 26 and
   22 at 575 — so the page showed five heading sizes on a phone and nobody could
   see that from any one place in the file. The ledes were worse: four of the six
   never stepped AT ALL, because .stat-lede, .stat-lede-strong, .deep-lede and
   .action-lede hardcoded their font-size and their tokens were dead. Measured on
   a 390px screen before this, the six ledes were 30, 22, 22, 22, 16 and 16.

   So the fix is not a new breakpoint, it is a single source of truth. Change the
   two numbers at the top of each group and the whole page follows.

   The 575 steps that used to set these are gone rather than overridden — a
   superseded declaration left in place is how this component got into trouble
   in the first place.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 991.98px) {
  :root {

    /* ── Section headings ─────────────────────────────────────────────────
       One size. The other six derive from --fs-section-title rather than
       repeating 30px, so there is exactly one number to change. */
    --fs-section-title: 30px;
    --fs-guardian-title: var(--fs-section-title);
    --fs-geo-title: var(--fs-section-title);
    --fs-diff-title: var(--fs-section-title);
    --fs-plans-title: var(--fs-section-title);
    --fs-faq-title: var(--fs-section-title);
    --fs-cta-title: var(--fs-section-title);

    /*
     * Same for their leading, which was as scattered as the sizes: 110%, 118%,
     * 120% and 124% across the seven. 1.1 is what four of them already used.
     *
     * UNITLESS, and that is not a style preference. A percentage line-height
     * computes to a fixed LENGTH on the element and inherits as that length;
     * unitless inherits as the ratio and every child recomputes it against its
     * own size. .stat-title is 30px here and carries .stat-figure, which is 56,
     * so 110% handed the figure a 33px line box for glyphs that need about 62 —
     * and because the figure paints itself with background-clip: text, a box
     * shorter than the glyphs clips them. "43%" came out sliced off top and
     * bottom. 1.1 gives it 61.6 instead, which is the same relationship the
     * desktop token has always had.
     */
    --lh-section-title: 1.1;
    --lh-guardian-title: var(--lh-section-title);
    --lh-geo-title: var(--lh-section-title);
    --lh-diff-title: var(--lh-section-title);
    --lh-plans-title: var(--lh-section-title);
    --lh-faq-title: var(--lh-section-title);
    --lh-cta-title: var(--lh-section-title);

    /* ── Section ledes ────────────────────────────────────────────────────
       15 for the paragraph, 20 for the emphasised one above it. The 5px gap is
       what carries the emphasis now that both are small. */
    --fs-stat-lede: 15px;
    --fs-deep-lede: var(--fs-stat-lede);
    --fs-action-lede: var(--fs-stat-lede);
    --fs-guardian-lede: var(--fs-stat-lede);
    --fs-cta-lede: var(--fs-stat-lede);

    --fs-stat-lede-strong: 20px;

    /* The stat ledes were the only pair on line-height:normal; 1.5 is what every
       other lede on the page already uses. Unitless for the reason above — none
       of these carries a child at a different size today, but --lh-section-title
       did not either until .stat-figure turned up inside it. */
    --lh-stat-lede: 1.5;
    --lh-lede: 1.5;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE BUTTON ON A PHONE — MAKING ONE LINE POSSIBLE
   ═══════════════════════════════════════════════════════════════════════════

   .button sets white-space:nowrap, so no label ever wraps. That is a promise
   the component cannot keep on its own, because two labels on this page are
   longer than the button they sit in:

     "Get My Free Security Assessment"   263px at 18px
     "See how AI Code Guardian works"    259px at 18px

   Against that, the room actually available, measured:

     viewport   hero button   guardian card button
        320px       290px            242px
        360px       330px            282px
        390px       360px            312px

   and the component was spending 93px of every one of those on itself — 24px of
   left padding, 65px reserved on the right for the arrow disc, 4px of border. On
   a 320px screen that left 149px of the guardian button for 259px of text. There
   is no font size that fixes a gap that size, which is why the label was wrapping
   and the pill was losing its shape.

   So the chrome comes down where it can, and the type makes up the rest.

   THE DISC IS NOT WHERE THE CHROME COMES FROM. It was, in a first pass — 50 ->
   36, which reads as the obvious saving because the 65px reserved for it is by
   far the biggest single number here. It is also the one thing in the button
   that must not move: the disc is 50 wide and 34 tall, a horizontal lozenge, and
   at 36 it becomes 36x34, which is a circle. That is a different component. The
   65px stays.

   What does come down is the side padding, 24 -> 12, and the guardian card's own
   inset, 24 -> 16, which is worth another 16px to the button sitting inside it.

   The rest comes off the type: 18 -> 14. That is an ordinary size for a button
   label on a phone — 18 was simply the desktop number carried down unchanged —
   and it is the price of keeping the disc's shape, which is the right trade.

   Under 360 there is a second step, and it is not pretty: a 320px screen is
   simply too narrow for a 31-character label at a comfortable size. 12px keeps
   it on one line with its shape intact, which is the thing that was asked for.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 575.98px) {
  :root {
    /*
     * The disc and the padding-right it reserves are NOT touched here. They
     * were, briefly — 50 -> 36 — and that was wrong: at 36 the disc is 36x34,
     * which is a circle, and the component draws a 50x34 horizontal lozenge.
     * The shape is the design, so the pixels have to come from somewhere else.
     */
    --hf-btn-pad-x: 12px;

    /*
     * 14, not 15, and the disc is why. Keeping it at 50 keeps 65px reserved on
     * the right, and that is 14px more chrome than the 36px disc was costing —
     * so the point has to come off the type instead. 14px is an ordinary size
     * for a button label on a phone; a circle where the design draws a lozenge
     * is not an ordinary shape.
     */
    --fs-hf-btn: 14px;
    /* Buys the button inside it 16px of width. */
    --guardian-card-pad: 16px;
  }
}

@media (max-width: 359.98px) {
  :root {
    --fs-hf-btn: 12px;
    --guardian-card-pad: 12px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE ENGINE'S TAG ROW — KEEPING IT TO ONE LINE
   ═══════════════════════════════════════════════════════════════════════════

   Each layer's panel carries three pale tags, and the comp draws them as one
   row. On a phone they were taking two, and the fifth panel worst of all —
   measured, with everything at its desktop size:

     panel                                 needs    360px   390px
     1  Block / Filter / Harden             266px     fits    fits
     3  Isolate / Review / Restore          295px   over 13   fits
     4  Measure / Explain / Report          303px   over 21   fits
     5  One View / White Label / Teams      346px   over 64  over 34

   .engine-tags keeps flex-wrap: wrap, and deliberately — the note on that rule
   records what nowrap did to the five-panel grid, which is that a nowrap row's
   min-content width became the grid's width and pushed every panel 9px out of
   its card. Wrapping is the release valve for that, and taking it away to force
   one line would buy this row at the cost of the whole section.

   So the row is made to FIT instead, and then it never reaches the wrap. The
   type goes 18 -> 13, which is the size the component's own token was written
   for before it turned out nothing was reading it, and the paddings and gap come
   in with it. At 320px that is 193px of tags in 214px of room; the desktop row
   is untouched.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 479.98px) {
  :root {
    --fs-engine-tag: 13px;
    --engine-tag-pad-y: 6px;
    --engine-tag-pad-x: 10px;
    --engine-tags-gap: 8px;
  }
}

/* 320px needs 7px more than the step above leaves it, and the tags themselves
   are already as small as they should go — so it comes out of the space
   BETWEEN and AROUND them rather than off the type again. */
@media (max-width: 359.98px) {
  :root {
    --engine-tags-gap: 6px;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE SIGNOUT CONFIRM
   ═══════════════════════════════════════════════════════════════════════════

   Drawn to match the SweetAlert2 dialog LeadXForms opens before ending a
   session, because a visitor moving between the two sites should meet the same
   control. Its warning palette (#facea8 ring, #f8bb86 mark) and Bootstrap's
   btn-danger / btn-dark are copied literally rather than tokenised: they are
   another project's component, and a value changed here would only put the two
   dialogs out of step.

   A native <dialog>, so the parts that are easy to get wrong are not written at
   all - focus is trapped, Escape closes, the page behind goes inert, and the top
   layer puts it above everything without a z-index.
   ═══════════════════════════════════════════════════════════════════════════ */

.lx-confirm {
  width: min(92vw, 420px);
  padding: 40px 28px 32px;
  border: 0;
  border-radius: 12px;
  background: #fff;
  color: var(--text);
  font-family: var(--font);
  text-align: center;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .18);
}

/* Not inherited: ::backdrop is a separate box, not a child. */
.lx-confirm::backdrop {
  background: rgba(0, 0, 0, .45);
}

/*
 * The warning mark. A bordered circle with a character in it rather than an
 * SVG - the glyph IS the shape, so there is nothing to draw.
 *
 * aria-hidden in the markup: it repeats what the title and the buttons already
 * say, and "exclamation mark" read aloud adds nothing.
 */
.lx-confirm-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 80px;
  height: 80px;
  margin: 0 auto 26px;
  border: 4px solid #facea8;
  border-radius: 50%;
  color: #f8bb86;
  font-size: 44px;
  font-weight: var(--fw-bold);
  line-height: 1;
}

.lx-confirm-title {
  margin: 0 0 16px;
  font-size: 28px;
  font-weight: var(--fw-bold);
  line-height: 1.2;
  color: var(--text-heading);
}

.lx-confirm-text {
  margin: 0;
  font-size: 18px;
  font-weight: var(--fw-regular);
  line-height: 1.5;
  color: var(--text);
}

.lx-confirm-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-top: 40px;
}

/*
 * Not .button. That component is a 50px pill with an arrow disc and a hover
 * lift, built for the page's calls to action - a destructive confirm should not
 * look like one of those, and LeadXForms' own dialog uses plain Bootstrap
 * buttons here for the same reason.
 */
.lx-confirm-btn {
  padding: 8px 24px;
  border: 0;
  border-radius: 8px;
  font-family: var(--font);
  font-size: 16px;
  font-weight: var(--fw-regular);
  line-height: 1.5;
  color: #fff;
  cursor: pointer;
  transition: background-color .15s ease;
}

.lx-confirm-btn:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.lx-confirm-btn--danger {
  background: #dc3545;
}

.lx-confirm-btn--danger:hover {
  background: #bb2d3b;
}

.lx-confirm-btn--dark {
  background: #212529;
}

.lx-confirm-btn--dark:hover {
  background: #424649;
}

@media (max-width: 575.98px) {
  .lx-confirm {
    padding: 32px 20px 24px;
  }

  .lx-confirm-title {
    font-size: 24px;
  }

  .lx-confirm-text {
    font-size: 16px;
  }

  /* Stacked, and the destructive one is no longer the first thing a thumb
     reaches - "No, return" sits under it, nearest the bottom of the screen. */
  .lx-confirm-actions {
    flex-direction: column-reverse;
    margin-top: 32px;
  }

  .lx-confirm-btn {
    width: 100%;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   FEATURE DEEP-DIVES — RESPONSIVE
   The desktop implementation above is unchanged. Everything here is scoped to
   max-width, so nothing in this block can reach 992 and over.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 991.98px) {

  /*
   * The copy's side inset opens up on a tablet.
   *
   * 20 was chosen for the whole stacked range as "the smallest inset that still
   * reads as an inset" — true on a phone, where the page gutter outside the grid
   * is doing the heavy work. At 768 the grid is 736 across and 20 a side stops
   * reading as an inset at all; it reads as text that nearly touches the border.
   *
   * The picture cell is untouched by this: .deep-shot sets padding-inline: 0 of
   * its own, because every feature SVG already carries 5.2% of transparent
   * margin on each side.
   */
  :root {
    --deep-cell-pad: 28px 32px;
  }

  /* The head's step, so the gap above the first row comes in with the type.
     It had a phone rung and nothing between that and the desktop 56. */
  .deep-head {
    margin-bottom: 48px;
  }
}

@media (max-width: 575.98px) {

  /* Back to 20 on a phone, where the grid is ~328 across and 32 a side would
     take a fifth of the measure. The vertical comes in with it. */
  :root {
    --deep-cell-pad: 24px 20px;
  }

  /* 56 above the button is most of a thumb's reach on a 360px screen, and the
     section already closes with a full-width rule right above it. */
  .deep-cta {
    margin-top: 40px;
  }
}