/* Responsive layer for the design export — handoff.md gap #1.
 *
 * The export was authored at desktop width and carries ZERO @media rules
 * across 68 pages; every layout value is an inline style, which outranks any
 * stylesheet rule. Hence !important throughout, scoped to narrow viewports so
 * desktop rendering is untouched. Selectors match inline-style substrings
 * because the export has almost no classes to hang rules on.
 *
 * Breakpoints are Webflow's 991 / 767 / 479, the ones the live vijil.ai is
 * designed against, so this layer stages the same way the original does
 * rather than inventing its own.
 *
 * Reference measurements taken from the live site at a 390px viewport:
 * nav 75px tall (hamburger, one row), h1 52.76px, headline top 126px.
 */

/* ========================================================================
   Product diagram swap — desktop artwork vs. hand-laid-out mobile variant
   ========================================================================
   Six diagrams draw into a 1096px viewBox. Scaled into 350px their labels
   land near 4px, so they are redrawn rather than shrunk (one module per
   diagram in scripts/mobile-diagrams/). The converter wraps each desktop svg
   together with its variant in .vj-dia-swap; these two rules pick one.

   display:none rather than visibility or opacity, so the hidden one leaves
   the accessibility tree — otherwise a screen reader reads both aria-labels
   for the same figure. */
/* A SWAP FILLS ITS PARENT. Without this the container is shrink-to-fit, and an
   svg inside it asking for width:100% has no definite width to resolve against
   — so it falls back to the CSS default for a replaced element, 300px. That is
   what happened inside figure.vj-fig, which is a grid: a 1096-unit chart drew
   itself at 300px with unreadable labels while its caption ran full width.
   Third instance of this family, after a grid item with margin:0 auto and an
   svg with width="100%" inside one. */
.vj-dia-swap { width: 100%; }

.vj-dia-swap > .vj-dia-mobile { display: none; }

/* lifecycle-graph is a custom element, so it needs its own half of the swap —
   `svg:not(.vj-dia-mobile)` below cannot match a non-svg sibling. */
.vj-dia-swap > lifecycle-graph { display: block; }

/* ========================================================================
   Hamburger nav — mirrors the live site's Webflow navbar (data-collapse=all)
   ======================================================================== */
.vj-burger { display: none; }

@media (max-width: 991px) {
  .vj-burger {
    display: flex !important;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    width: 44px;               /* 44px is the minimum comfortable tap target */
    height: 44px;
    padding: 0;
    margin-left: auto;
    border: 0;
    background: none;
    cursor: pointer;
    flex: none;
  }
  .vj-burger span {
    display: block;
    height: 2px;
    width: 24px;
    background: currentColor;  /* inherits the nav's ink, dark or light */
    transition: transform .18s ease, opacity .18s ease;
  }
  .vj-burger.is-open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  .vj-burger.is-open span:nth-child(2) { opacity: 0; }
  .vj-burger.is-open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

  /* The panel holds the real link row, moved rather than cloned. */
  .vj-mobile-panel {
    padding: 8px 20px 20px;
    border-top: 1px solid currentColor;
    border-color: color-mix(in srgb, currentColor 14%, transparent);
  }
  .vj-mobile-links {
    display: flex !important;
    flex-direction: column !important;
    align-items: stretch !important;
    gap: 2px !important;
  }
  .vj-mobile-links > * { width: 100%; }
  .vj-mobile-links a { padding: 10px 0; display: block; }

  /* Desktop dropdowns open on :hover, which never fires on touch. Flatten
     them so every destination is visible in one scrollable list. */
  .vj-mobile-links .vj-dd { display: block !important; }
  .vj-mobile-links .vj-dd-m {
    display: block !important;
    position: static !important;
    padding-top: 0 !important;
  }
  .vj-mobile-links .vj-dd-m > div {
    background: none !important;
    border: 0 !important;
    box-shadow: none !important;
    padding: 0 0 0 14px !important;
    min-width: 0 !important;
  }
  /* The pill CTAs stay pills, sized to the panel. The export sets them
     content-box with 18-20px of horizontal padding, so width:100% became
     390 + 36 = 426px and they ran past the frame. border-box folds the
     padding back inside the width. */
.vj-mobile-links a.vj-btn,
  .vj-mobile-links a.vj-btn-ghost,
  .vj-mobile-links a.vj-btn-ghost-brand,
  .vj-mobile-links a.vj-btn-sm,
  .vj-mobile-links a.vj-btn-ghost-sm,
  .vj-mobile-links a.vj-chip {
    box-sizing: border-box !important;
    width: 100% !important;
    text-align: center;
    margin-top: 8px;
  }
  /* A button keeps its own padding inside the panel. `.vj-mobile-links a` above
   * sets 11px 0 and is (0,1,1); a bare `.vj-btn-sm` is (0,1,0) and loses to it.
   * That never came up while the pill was an inline style, because an inline
   * style outranks every selector there is — so moving it into a class handed a
   * fight to a rule that had never had to win one. */
  .vj-mobile-links a.vj-btn { padding: 12px 28px; }
  .vj-mobile-links a.vj-btn-ghost,
  .vj-mobile-links a.vj-btn-ghost-brand { padding: 12px 26px; }
  .vj-mobile-links a.vj-btn-sm { padding: 10px 20px; }
  .vj-mobile-links a.vj-btn-ghost-sm { padding: 8px 18px; }
  /* Same trap anywhere else a padded element is stretched to full width. */
  .vj-mobile-panel *, .vj-mobile-links * { box-sizing: border-box; }
}

/* ========================================================================
   Layout — everything below 991px
   ======================================================================== */
@media (max-width: 991px) {

  /* --- Page scrolling ----------------------------------------------------
   * The homepages put every panel inside ONE nested scroll container:
   *   <div data-snap="true" style="height:100vh;overflow-y:auto;
   *        scroll-snap-type:y mandatory">
   * On a phone that breaks scrolling outright: the document never scrolls so
   * the address bar never collapses, 100vh exceeds the visible area while
   * browser chrome shows so panel bottoms are unreachable, and mandatory
   * snapping fights touch momentum. Give the page back to the browser. */
  [data-snap="true"] {
    height: auto !important;
    max-height: none !important;
    overflow-y: visible !important;
    scroll-snap-type: none !important;
  }
  .vj-snap { scroll-snap-align: none !important; }

  /* Desktop snaps one full-viewport panel at a time. Mobile scrolls
   * continuously (the snap container is released above), so a panel no longer
   * needs to fill the screen — and pinning it to one leaves the short panels
   * padded out with blank space. Measured on the risk-owner homepage: the
   * Platform panel held 568px of content in an 844px box, and the page
   * carried 796px of slack in total.
   *
   * Panels size to their content instead. svh, not vh, wherever a viewport
   * height is still wanted: vh predates mobile browser chrome and ignores it. */
  .vj-vh { height: auto !important; min-height: 0 !important; }
  .vj-minvh { min-height: 0 !important; }

  /* The hero is the exception — it should still fill the first screen. */
  /* `:first-child` on BOTH halves, since 2026-09-23. The second selector was
     * written for a container whose first child wraps ONE panel. /product/dome
     * wraps all fourteen in a <main style="display:contents"> — the arc gates
     * slice the last beat at </main>, so that tag has to exist — and every
     * panel is then a first-child's .vj-snap. Every section became a hero:
     * fourteen 844px boxes holding four lines each, which is the exact slack
     * the note above says this block exists to remove. */
    [data-snap] > *:first-child,
  [data-snap] > *:first-child > .vj-snap:first-child {
    min-height: 100svh !important;
  }

  /* 120px/96px vertical padding is a desktop rhythm; on a phone it reads as a
   * gap rather than as breathing room. */
  .vj-band-xl { padding-top: 56px !important; padding-bottom: 48px !important; }
  .vj-band  { padding-top: 48px !important; }

  /* --- Nav must not overlap the hero -------------------------------------
   * The homepages pin the nav absolute and compensate with padding-top:96px
   * sized for a one-row desktop nav. In flow, that padding double-counts. */
nav.vj-abs {
    position: static !important;
  }

  /* The panels are `display:grid; align-content:center` inside min-height:100vh.
   * When the content is TALLER than the panel — which happens in portrait,
   * where a narrow column makes everything taller, but not in landscape — a
   * centred grid overflows equally in BOTH directions, so the top of one
   * panel rides up over the bottom of the previous one. That is the terminal
   * image appearing to occlude the subheading above it. Packing to the start
   * lets the panel grow downward instead. */
.vj-ac-center {
    align-content: start !important;
  }
  .vj-snap.vj-band { padding-top: 28px !important; }

  /* --- Grids -------------------------------------------------------------
   * Track is minmax(0,1fr), NOT 1fr: bare 1fr means minmax(auto,1fr), whose
   * floor is the content's min-content width, so a long code line still
   * forces the page wider than the screen. The 0 floor is what lets it
   * shrink. The second rule catches IMPLICIT grids that declare no columns at
   * all — one of those handed a 326px container a 964px track. */
.vj-cols,
  .vj-grid {
    grid-template-columns: minmax(0, 1fr) !important;
  }
.vj-grid > *,
  .vj-flex > * { min-width: 0 !important; }

  /* --- Flow --------------------------------------------------------------- */
.vj-flex { flex-wrap: wrap !important; }

  /* Terminal comments. The wrap rule above breaks a `$ command    # note` row
     onto two lines, and the orphaned note then reads as a comment on the NEXT
     command — "# in-agent guards" appearing to annotate the line below it. The
     command is the content; the note is decoration. Drop the decoration rather
     than let it lie. Matches only the dimmed, flex:none note spans inside the
     dark terminal blocks; every element this selects starts with "#". */
  .vj-cli-note.vj-fixed { display: none !important; }

  /* Terminal commands. With the note hidden (above) a long command still
     ellipsises, so the reader loses the back half of the one line they came
     for -- "vijil protect my-agent --guards prompt_inj...". A command is not
     decoration; let the row scroll instead of truncating it. Scoped to the
     dark terminal block, not the small inline code chips that share the
     ground token. */
  .vj-panel.vj-terminal .vj-nowrap {
    overflow-x: auto !important;
  }
  .vj-panel.vj-terminal .vj-ellipsis {
    overflow: visible !important;
    text-overflow: clip !important;
  }

  /* Logo marquees are a width:max-content flex track translated by a keyframe
     animation inside an overflow:hidden window. Wrapping it, or capping it at
     max-width:100%, turns the scrolling row into a tall stacked column with
     72px gaps. Animated tracks and their immediate rows keep their own flow. */
  /* div[...] rather than [...] on purpose. The generic
     [style*="width:"] { max-width: 100% } rule below has the same specificity
     and comes later, so it was winning and capping this track at the column
     width — which squashed the two loop copies onto each other and made the
     logos appear to repeat and overlap. The element qualifier outranks it. */
div.vj-marquee,
  div.vj-marquee > * {
    flex-wrap: nowrap !important;
    max-width: none !important;
    width: max-content !important;
    flex: none !important;
  }
  /* Keep each mark's own inline width. width:auto collapses a background-image
     div to zero (no content to size from), and the flex-children min-width:0
     rule above lets it shrink to nothing; flex:none pins it. */
  div.vj-marquee [role="img"] {
    flex: none !important;
    max-width: none !important;   /* keep the mark's own 130px, not 100% of the row */
  }

  /* Step sequences (Scan -> Fingerprint -> Attest) stack rather than wrap:
     a wrapped connector arrow points nowhere. */
.vj-flex.vj-stretch {
    flex-direction: column !important;
  }

  .vj-has-w { max-width: 100% !important; }

  /* The export bakes a measured width AND height onto some elements — this
   * paragraph carries "width:964px; height:29px", the height of ONE line at
   * desktop width. Narrowing it reflows the text to three lines, but the
   * pinned height stays at one, so the text spills over whatever follows.
   * That is the portrait-only occlusion in "Plugs in to": in landscape the
   * column is wide enough that the text still fits its pinned height.
   *
   * Released on text blocks only. Of 56 elements with a pinned width+height,
   * exactly one overflows, and spans are left alone because several use a
   * pinned height as a shape (colour dots, badges) that auto would collapse. */
  p.vj-has-h, h1.vj-has-h, h2.vj-has-h,
  h3.vj-has-h, h4.vj-has-h, li.vj-has-h,
  blockquote.vj-has-h, figcaption.vj-has-h {
    height: auto !important;
    min-height: 0 !important;
  }
  .vj-has-minw { min-width: 0 !important; }
  img, svg, video, canvas, iframe { max-width: 100% !important; height: auto; }

  /* clip, not hidden: hidden makes the element a scroll container and would
     resurrect the nested-scrolling bug that broke the homepage. */
  html, body { overflow-x: clip; }

  /* THE TWO RULES BELOW PAY FOR THE ONE ABOVE. Clipping means anything wider
     than the screen is unreachable, not scrollable — no bar appears and nothing
     throws. check-rendered now fails on any element past 390px, and these are
     the two shapes that were already over it.

     A prose table is content a reader needs. display:block is what makes
     overflow apply to a table box at all; without it the rule is inert. */
  .vj-prose-main table { display: block; overflow-x: auto; max-width: 100%; }

  /* An inline identifier has nowhere to break: a SPIFFE ID is one token with no
     spaces, so it runs straight off the screen. Break it anywhere instead.
     Scoped away from <pre>, where a code block scrolls and must keep its own
     line breaks — breaking there would corrupt what the reader copies. */
  :not(pre) > .vj-code { overflow-wrap: anywhere; }

  /* Same shape, different element: four blog posts print a bare URL as their
     own link text, and an 88-character URL is one token with nowhere to break.
     CI caught this one and macOS did not — Linux lays the same self-hosted type
     out about 46px wider, which is the platform difference check-nav-clearance
     already carries an 8px clearance for. Scoped to prose so the short nav and
     footer links are untouched; `anywhere` only breaks a word that will not
     otherwise fit, so ordinary link text is unaffected either way. */
  .vj-prose-main a { overflow-wrap: anywhere; }
}

/* ========================================================================
   Type and rhythm — tablet down
   ======================================================================== */
@media (max-width: 767px) {
  /* Scaled to land near the live site's 52.76px h1 at a 390px viewport
     (13.5vw ~= 52.7px there) rather than to an invented ladder. */
  .vj-display, .vj-display, .vj-display {
    font-size: clamp(36px, 13.5vw, 60px) !important; line-height: 1.06 !important;
  }
  .vj-hero, .vj-hero { font-size: clamp(34px, 12vw, 54px) !important; line-height: 1.08 !important; }
  .vj-title, .vj-title { font-size: clamp(32px, 11vw, 48px) !important; line-height: 1.1 !important; }
  .vj-head, .vj-head { font-size: clamp(28px, 8.5vw, 38px) !important; line-height: 1.15 !important; }
  .vj-subhead { font-size: clamp(26px, 7.5vw, 34px) !important; line-height: 1.18 !important; }

  /* THE LADDER ABOVE MISSED THREE HEADINGS, and a phone is where that shows.
     Measured at a 320px viewport before this: the product h1 rendered at 56px,
     a section h2 at 32px and a blog title at 44px — none of them scaled at all,
     because .vj-h-56 and .vj-h-32 were added for the DESKTOP end of the site
     and .vj-post-title predates the ladder. Vin: "the page headings on mobile
     look very large."

     Same mechanism as the rungs above, and continuous with them: each vw term
     exceeds its ceiling by 767px, so every heading holds its full size at the
     breakpoint and only shrinks below it. The bare h1 is a floor for the
     eighteen that carry no ladder class at all; the doubled class selectors
     above outrank it, so it catches only what nothing else claims. */
  .vj-h-56 { font-size: clamp(30px, 9vw, 56px) !important; line-height: 1.1 !important; }
  .vj-h-32, .vj-h-32-plain { font-size: clamp(24px, 6.2vw, 32px) !important; line-height: 1.2 !important; }
  .vj-post-title { font-size: clamp(28px, 8vw, 44px) !important; line-height: 1.15 !important; }
  h1 { font-size: clamp(30px, 8.5vw, 44px) !important; line-height: 1.12 !important; }

  /* Hard line breaks are set for a desktop measure. On a phone they leave
     orphans mid-sentence ("systems that" alone on a line), so let text
     reflow. display:inline rather than display:none: hiding the <br> also
     removes the whitespace it stood for, which ran words together
     ("systems thatremain reliable"). The ::after restores the space.
     Verified in the browser against all three candidate techniques. */
  .vj-softbr::before { content: none !important; }

  /* nowrap belongs on nav items and pills, not on headings and body, where it
     forces horizontal overflow. */
  h1, h2, h3, h4, p, li, td, th, figcaption, blockquote { white-space: normal !important; }
nav .vj-nowrap,
  a.vj-btn,
  a.vj-btn-ghost,
  a.vj-btn-ghost-brand,
  a.vj-btn-sm,
  a.vj-btn-ghost-sm,
  a.vj-chip { white-space: nowrap !important; }

  .vj-gutter { padding-left: 20px !important; padding-right: 20px !important; }
  .vj-band-gutter { padding-left: 20px !important; padding-right: 20px !important; }
}

/* ========================================================================
   Phone
   ======================================================================== */
@media (max-width: 767px) {
  /* ---- Logos: one size for every set ------------------------------------
   * Two different mechanisms carry logos. Customer marks are
   * <div role="img" aria-label="..."> with a background image, sized
   * 176x44 (and NEAR at 112x28, already inconsistent). Analyst and award
   * marks are <img> at 36-48px. Both are normalised to the same 34px so the
   * rows read as one system.
   *
   * The selector keys on the inline opacity every decorative logo carries and
   * the nav wordmark does not, so the wordmark keeps its size. */
img.vj-dim,
  img.vj-dim,
  img.vj-dim {
    width: auto !important;
    object-fit: contain;
  }

  /* Equal height is not equal presence. Measured intrinsic ratios:
   *   award badges      0.92 - 0.93  (square)
   *   investor marks    2.89 - 7.72  (wordmarks)
   *   customer marks    3.71 - 9.89  (wordmarks)
   * At a shared 34px the square badges were ~32px wide and the wordmarks
   * 126-336px — roughly four times the visual mass. Heights now move in
   * opposite directions to meet in the middle: wordmarks down to 28, square
   * badges up to 42. Filename is the discriminator because CSS cannot select
   * on aspect ratio, and the award- and inv- prefixes are stable names.
   * (Do not write a star-slash pair inside this comment: it closes it early
   * and silently turns the rules below into garbage.) */
  img[src*="award-"] { height: 48px !important; max-height: 48px !important; }
  img[src*="inv-"] {
    height: 22px !important; max-height: 22px !important;
    align-self: center !important;      /* Mayfield sat high against the others */
    vertical-align: middle !important;
    object-fit: contain;
  }
  /* Gradient's mark is 193x25 — ratio 7.7 against Mayfield's 2.9 — so at an
     equal height it runs 170px wide and dominates the row. Capping width
     brings its visual weight back in line; BrightMind is capped for the same
     reason, being the heaviest of the three by area. */
  img[src*="inv-gradient"]   { max-width: 112px !important; height: auto !important; }
  /* Mayfield's artwork sits high in its own PNG, so at a shared centre
     line its ink reads about 3px above the others. Nudge the box down
     by that much rather than changing its size. */
  img[src*="inv-mayfield"]   { position: relative !important; top: 3px !important; }
  img[src*="inv-brightmind"] { max-width: 100px !important; height: auto !important; }

  /* The row's 22px desktop gap pushed the third mark onto a second line,
     which read as one logo sitting higher than the others rather than as a
     wrap. Tighter gap keeps all three on one row. */
  .vj-flex:has(> img[src*="inv-"]) {
    gap: 12px !important;
    align-items: center !important;
    flex-wrap: nowrap !important;
  }

  /* Customer logos: a 2 x 3 grid rather than a wrapping flex row, which left
     ragged rows of two-then-one. :has() selects the row by what it contains,
     since the container itself has no class to target. */
  /* [style*="flex-wrap:wrap"] is the discriminator, and it matters: the
     framework marquee is ALSO a flex row of [role=img] marks, but sits inside
     an animated width:max-content track and does not wrap. Without this the
     grid rule collapsed the marquee and width:100% blew each mark up to the
     full row, leaving that section blank. */
  .vj-flex.vj-wrap:has(> [role="img"][aria-label]) {
    display: grid !important;
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: 18px 16px !important;
    align-items: center !important;
    justify-items: stretch !important;
  }
  .vj-flex.vj-wrap:has(> [role="img"][aria-label]) > [role="img"][aria-label] {
    width: 100% !important;
  }
  /* Justify the grid: left column's artwork to the left margin, right
     column's to the right edge, with the slack falling between them. The box
     already fills its cell, so the alignment is done by where the background
     image sits inside it rather than by resizing anything. */
  .vj-flex.vj-wrap:has(> [role="img"][aria-label]) > [role="img"][aria-label]:nth-child(odd) {
    background-position: left center !important;
  }
  .vj-flex.vj-wrap:has(> [role="img"][aria-label]) > [role="img"][aria-label]:nth-child(even) {
    background-position: right center !important;
  }
  /* Height applies to every mark, marquee included, so both sets read as one
     system. Width is left alone outside the grid. */
  [role="img"][aria-label] {
    height: 22px !important;
    background-size: contain !important;
  }
  /* The NEAR mark is a PNG cropped tight to its glyphs, where the others are
     SVGs carrying their own internal whitespace. At an equal box height it
     therefore reads noticeably larger. Scaling the artwork inside the same box
     gives it matching optical padding without changing the grid. */
  [role="img"][aria-label="NEAR"] {
    background-size: auto 78% !important;
  }

  /* ---- Diagrams ---------------------------------------------------------
   * lorenz-loop and lifecycle-graph draw into a ~1044px viewBox. Scaled into
   * 350px their labels render at about 4px. The wrapper (added by the
   * converter) scrolls instead, so the diagram stays legible and swipeable. */
  .vj-diagram-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    max-width: 100% !important;   /* the 720px host must not widen the page */
    width: 100% !important;
  }
  /* The component sizes its own diagram from viewport HEIGHT:
   *   .vj-loop figure { max-width: min(1080px, max(420px,
   *                     calc((100vh - 400px) * 1.678))) }
   * On a phone that resolves to ~600px however wide the host is, so the stage
   * labels (spec, evolve, evaluate, ...) render at roughly 58% and vanish.
   * That rule is inside a shadow root, so it is fixed in the component's own
   * stylesheet (design/lorenz-loop.js) rather than from here. This wrapper
   * only provides the horizontal scroll. */
  /* No min-width here any more. Forcing the whole component to 1044px took
     its heading and lede off-screen with it, so the section opened scrolled
     past its own title. The component now scrolls only its <figure>
     internally (see design/lorenz-loop.js), leaving the prose at viewport
     width. This wrapper stays for lifecycle-graph, which has no such
     internal treatment. */
  .vj-diagram-scroll > * {
    display: block;
    max-width: 100% !important;
  }


  /* The leaderboard card's side padding steals width from the mobile SVG inside
     it, and that SVG is drawn in a 350-unit viewBox — 20px each side dropped its
     labels from 8.6px to 8.11px, under the 9px floor check-rendered enforces.
     The card keeps its frame; it just stops taking room from the chart. */
  .vj-chart-card {
    padding-left: 0 !important;
    padding-right: 0 !important;
    border-left: 0 !important;
    border-right: 0 !important;
    box-shadow: none !important;
  }

  /* ---- CTA buttons ------------------------------------------------------
   * Centre any row of pill CTAs. Selected by content, as the row has no class. */
.vj-flex:has(> a.vj-btn, > a.vj-btn-ghost, > a.vj-btn-ghost-brand, > a.vj-btn-sm, > a.vj-btn-ghost-sm, > a.vj-chip) {
    justify-content: center !important;
  }

  /* An image set to width:112% deliberately overhangs its column on desktop;
     on a phone that is 2px of horizontal overflow and a crowded figure. */
  img.vj-bleed, div.vj-bleed {
    width: 100% !important; max-width: 100% !important;
  }
}

@media (max-width: 479px) {
  /* Stack the CTA pills once a side-by-side pair would be cramped. */
.vj-flex:has(> a.vj-btn, > a.vj-btn-ghost, > a.vj-btn-ghost-brand, > a.vj-btn-sm, > a.vj-btn-ghost-sm, > a.vj-chip) {
    flex-direction: column !important;
    align-items: center !important;
  }
a.vj-btn,
  a.vj-btn-ghost,
  a.vj-btn-ghost-brand,
  a.vj-btn-sm,
  a.vj-btn-ghost-sm,
  a.vj-chip {
    box-sizing: border-box !important;
    width: 100% !important;
    max-width: 320px !important;
    text-align: center !important;
  }

  .vj-gutter { padding-left: 16px !important; padding-right: 16px !important; }
  .vj-gap-xl { gap: 48px !important; }
  .vj-gap-lg { gap: 36px !important; }
  .vj-gap-md { gap: 32px !important; }
}


/* Desktop behaviour of the converted <br> replacements: ::before injects the
 * newline the original <br> provided. The span itself holds a space, so the
 * mobile rule above (content:none) leaves words correctly separated. */
.vj-softbr::before { content: "\A"; white-space: pre; }

/* ========================================================================
   Product diagram swap — the breakpoint half
   ========================================================================
   Its own block at the end of the file for two reasons. The generic
   `img, svg, ... { max-width:100% !important }` rule in the 991 block would
   otherwise tie with the variant's sizing, and last-wins settles it. And an
   earlier attempt anchored these rules on a nearby comment that turned out to
   sit inside the 767 block, so the swap silently fired a breakpoint late —
   scripts/check-css-breakpoints.mjs now asserts the enclosure. */
@media (max-width: 991px) {
  /* The desktop svg is any direct svg child that is not the variant, so no
     class has to be injected into the export's own markup. */
  .vj-dia-swap > svg:not(.vj-dia-mobile) { display: none !important; }
  .vj-dia-swap > lifecycle-graph { display: none !important; }
  .vj-dia-swap > .vj-dia-mobile {
    display: block !important;
    width: 100% !important;
    max-width: 350px !important;
    height: auto !important;
    margin: 0 auto !important;
  }
}

/* ========================================================================
   Lifecycle variant — the chase around the loop
   ========================================================================
   Six chips sit on a rectangle. Each lights in turn, and the connector leaving
   it travels to the next, so the motion runs clockwise around the perimeter
   once every 7s. That IS the argument: the lifecycle is a loop, not a line.

   Keyframes live here rather than in a <style> inside the svg because the svg
   is injected into a .astro file, and Astro parses `{` as the start of a JS
   expression. Per-step delay rides in an inline style attribute, which carries
   no braces; the longhand properties below leave that delay alone (the
   `animation` shorthand would reset it to zero).

   Every connector carries pathLength="1", so one keyframe drives all six
   whatever their real lengths — there is no dash length to keep in step with
   the geometry, which the previous draft got wrong twice. */
@media (max-width: 991px) {
  .vj-lifecycle .vj-lc-ring,
  .vj-lifecycle .vj-lc-seg {
    animation-duration: 7s;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
  }
  .vj-lifecycle .vj-lc-ring { animation-name: vjLcRing; }
  .vj-lifecycle .vj-lc-seg  { animation-name: vjLcSeg; }
}

/* A chip lights as the pulse reaches it, then hands off. Nothing ever dims:
   the highlight is carried by the ring appearing, not by fading the rest —
   a white chip is held by one #E2DED4 hairline and reads as missing, rather
   than as waiting, once it drops much below full strength. */
@keyframes vjLcRing {
  0%        { opacity: 0; }
  3%        { opacity: 1; }
  15%       { opacity: 0; }
  100%      { opacity: 0; }
}

/* The connector travels from the chip that just lit to the next one. It clears
   before the pulse comes round again, so only one segment is ever lit. */
@keyframes vjLcSeg {
  0%, 1%    { stroke-dashoffset: 1; opacity: 0; }
  3%        { opacity: 1; }
  15%       { stroke-dashoffset: 0; opacity: 1; }
  22%       { stroke-dashoffset: 0; opacity: 0; }
  23%, 100% { stroke-dashoffset: 1; opacity: 0; }
}

/* Relighting the loop after render. /product/diamond's fits section is a
   four-product tab strip, and .vj-fits-tabs carries no rule hiding it at 991px
   — so below that width the tabs were visible and driving a lifecycle-graph
   that this block's sibling had already set to display:none. The tabs now
   repaint this variant instead (public/fits-tabs.js).

   Outside the 991 block on purpose: the hero variant renders at every width,
   and a lit step is not a breakpoint-sensitive property. The component paints
   the server-rendered `lit` step into fill/stroke ATTRIBUTES, which any rule
   here outranks — that is what lets the script switch with a class and one
   custom property instead of restating the palette in JavaScript. */
.vj-lifecycle [data-lc-node].is-lit > .vj-lc-box {
  fill: color-mix(in srgb, var(--vj-accent-local) 9%, #FFFFFF);
  stroke: var(--vj-accent-local);
  stroke-width: 2;
}
.vj-lifecycle [data-lc-node].is-lit > .vj-lc-name,
.vj-lifecycle [data-lc-node].is-lit > .vj-lc-label { fill: var(--vj-accent-local); }

/* An animation that repeats forever is exactly what this setting is for. The
   diagram must still read as a loop without it, so the connectors rest fully
   drawn in their static grey — which they already are, underneath. */
@media (prefers-reduced-motion: reduce) {
  .vj-lifecycle .vj-lc-ring,
  .vj-lifecycle .vj-lc-seg { animation: none !important; }
  .vj-lifecycle .vj-lc-seg { opacity: 0 !important; }
  .vj-lifecycle .vj-lc-ring { opacity: 0 !important; }
}

/* ========================================================================
   Hero simulations — desktop only
   ========================================================================
   The simulations occupy a right-anchored wedge that clears the hero copy,
   which on desktop is a 640px column inside a 1096px content area: the copy
   ends near 812px and the wedge starts near 900px.

   On a phone that separation does not exist. The copy is full-width to ~370px
   and the wedge still begins near 210px, so the ant network ran straight
   through the headline, the CTAs and the customer logos. This is not a tuning
   problem — a right-side wedge cannot avoid full-width text.

   Hidden rather than narrowed. A strip thin enough to clear the copy is too
   thin to read as anything, and the modules skip mounting at this width, so
   a phone allocates no canvas and runs no loop. */
.vj-sim-band { display: none; }        /* desktop uses the wedge instead */

/* The wedge's left edge dissolves by construction — cells cannot be born past
   it, and reaction-diffusion additionally ramps its kill rate there. Its TOP
   is a hard horizontal cut where the canvas begins under the nav, so it gets a
   short fade.

   The bottom used to get none, on the grounds that all three simulations reach
   zero ink by the fourth viewport. True, and not enough: the taper shrinks the
   live band's WIDTH, not its brightness, so at the SECOND viewport the band is
   still 16-30% of the page at full strength — which put lit cells straight over
   the third card of the problem section. Zero-by-the-fourth is an endpoint; the
   brief was "more in the hero, less with each section below", which is a
   gradient. The stops below leave the hero (0-25%) exactly as it was and thin
   everything under it. */
/* The page-spanning simulation is absolutely positioned with z-index 0, and the
   hero is the only section that is itself positioned — so on every section BELOW
   the hero the canvas paints over in-flow content, and lit cells landed on the
   card text of the problem section. The sections carry no background, so lifting
   them costs nothing visually: cells still show through the gaps, they just stop
   washing text. The hero is excluded deliberately — the wedge belongs in front
   there, and its copy already sits at z-index 2. */
[data-snap] > section:not([data-label="Hero"]) { position: relative; z-index: 1; }

canvas[data-sim]:not(.vj-sim-band) {
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 4%, #000 24%, rgba(0,0,0,.42) 52%, rgba(0,0,0,.16) 78%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 4%, #000 24%, rgba(0,0,0,.42) 52%, rgba(0,0,0,.16) 78%, transparent 100%);
}

@media (max-width: 991px) {
  /* The wedge is hidden; the band takes its place. */
  canvas[data-sim]:not(.vj-sim-band) { display: none !important; }
  /* Larger, full-bleed, and faded at every edge.
     Full-bleed via the 100vw + translateX trick, so the panel escapes the
     section's own side padding and meets the viewport instead of stopping at a
     ruled margin. Taller to match.
     The fade is a MASK rather than per-simulation alpha maths: two linear
     gradients intersected, which feathers all four edges of whatever is drawn.
     One rule covers Life, the ant colony and reaction-diffusion alike — the
     last of which is a solid field and was reading as a hard blue rectangle. */
  .vj-sim-band {
    display: block !important;
    width: 100vw !important;
    max-width: 100vw !important;
    margin-left: 50% !important;
    transform: translateX(-50%) !important;
    height: 260px !important;
    margin-top: 20px !important;
    margin-bottom: 2px !important;
    -webkit-mask-image:
      linear-gradient(to right, transparent 0%, #000 14%, #000 86%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%);
    mask-image:
      linear-gradient(to right, transparent 0%, #000 14%, #000 86%, transparent 100%),
      linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
  }
}

/* Beat containers on the five argument pages (transforms.mjs beatContainers).
 *
 * Vin's teammate asked for containers so the blocks stop "floating on the
 * background"; Vin picked this over full-bleed banding — "cleaner".
 *
 * DESKTOP ONLY, DELIBERATELY. Below 768px the sub-cards inside a beat stack
 * vertically, so this becomes a white box drawn 22px outside other white
 * boxes — a frame hugging a frame. It also eats 44px of a 390px viewport, and
 * <main> is already narrowed here by the rules above, so the bill is bigger
 * than it looks. A phone shows one section at a time, so the vertical gap
 * already says where the argument breaks.
 *
 * Measured on the built page at 390px, not assumed: 278px of text with the
 * container, 358px without. 29% of the measure, on the screen that can least
 * afford it. Breakpoint verified: off at 767px, on at 768px.
 *
 * No shadow, unlike the cta beat, which keeps its own. If every block gets the
 * full treatment the CTA stops being the loudest thing on the page and the page
 * loses its ending.
 */
/* Below the breakpoint the beat keeps whatever vertical padding its export
 * asked for. beatContainers moves an inline `padding` into this property
 * precisely so the container can win above 768px without destroying it: an
 * inline declaration outranks a stylesheet rule, and the proof beat's
 * `padding:56px 0` silently beat the container, painting a card whose content
 * sat flush against both edges. */
.vj-beat { padding: var(--vj-beat-pad, 0); }

@media (min-width: 768px) {
  .vj-beat {
    padding: clamp(22px, 3.2vw, 40px);
    background: var(--vj-surface);
    border: 1px solid rgba(16, 24, 40, .12);
    border-radius: 16px;
  }
}

/* The lifecycle mnemonic beside a product-page heading (transforms.mjs
 * heroLifecycleMini). DESKTOP ONLY, and every part of that is load-bearing:
 *
 *  - Below 992px the same six-step loop ALREADY appears further down the page,
 *    as the mobile variant of the `fits` beat's lifecycle-graph. Showing it in
 *    the hero too would print the same diagram twice on one phone screen.
 *  - The hero's inline grid collapses to one column at this breakpoint (the
 *    rule above, with !important), so a third column has nowhere to go anyway.
 *
 * The loop is LAST in the DOM and placed into column 1 here, so reading and
 * focus order stay breadcrumb -> heading -> copy -> diagram while the visual
 * order puts the mnemonic first. Explicit placement rather than `order`,
 * because the other two children must move deliberately, not by side effect. */
.vj-hero-loop { display: none; }
.vj-hero-loop-cap { margin-top: 10px; text-align: center;
  font: 500 13px ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: .1em; text-transform: uppercase; color: var(--vj-ink-muted); }

@media (min-width: 992px) {
  .vj-hero-loop { display: block; grid-column: 1; grid-row: 1; align-self: center; }
  section.vj-hero-mini > :nth-child(1) { grid-column: 2; grid-row: 1; }
  section.vj-hero-mini > :nth-child(2) { grid-column: 3; grid-row: 1; }
}

/* The same mnemonic beside a product page's h1 — the `claim` beat on
 * /product/*. Same intent as the block above, different shape, and the
 * difference is the whole reason this rule exists: there the loop IS the grid
 * child, here it is WRAPPED so the grid item is the wrapper. `.vj-hero-loop`'s
 * display:none then hides the drawing and leaves a zero-height item behind,
 * which still claims a grid row and still collects the section's 48px row-gap
 * under the copy.
 *
 * Measured at 390px before this rule: /product/diamond's claim beat was 455px
 * tall around 399px of content, /product/discover's 516px around 460px. Both
 * shipped that way. Hide the ITEM, not the drawing.
 *
 * No grid placement here on purpose. The block above sets grid-column:1 for the
 * hero-mini layout, and inheriting that would move the loop to the left column
 * on desktop — where this pattern wants it on the right, by natural flow. */
.vj-hero-aside { display: none; }
@media (min-width: 992px) { .vj-hero-aside { display: block; } }

/* Product tabs on the "where it fits" diagram (transforms.mjs fitsProductTabs,
 * behaviour in public/fits-tabs.js). Replaces the export's row of product
 * links, so it inherits that row's pill shape deliberately — a reader who has
 * seen those pills on another product page should recognise these. */
/* The strip sits ON the diagram, with a hairline between them, so the tabs and
 * the thing they switch read as one component. No enclosing border: the fits
 * beat is already inside a .vj-beat card and a second frame would be a box in a
 * box — the defect that took containers off phones. */
.vj-fits-frame { border-bottom: 1px solid rgba(16, 24, 40, .12);
  margin: 0 0 22px; padding-bottom: 12px; }
.vj-fits-tabs { display: flex; flex-wrap: wrap; gap: 10px;
  justify-content: center; }
.vj-fits-tab { display: inline-flex; align-items: center; gap: 8px;
  font: 400 16px Futura, 'Jost', Arial, sans-serif; padding: 8px 16px;
  border-radius: 999px; background: var(--vj-surface);
  border: 1px solid rgba(16, 24, 40, .12); color: var(--vj-ink-body);
  white-space: nowrap; cursor: pointer; text-decoration: none; }
.vj-fits-tab:hover { background: var(--vj-ground-flat); }
/* The selected tab is marked by weight and a heavier border as well as by the
 * dot's colour. Selection must not rest on hue alone, and the reason has
 * changed without the rule changing: Discover used to be a slate that blended
 * into the rules around it, and is now a teal that does not. It is still wrong
 * to encode selection in hue, because roughly 8% of men would be reading it
 * with a red-green deficiency. Weight and border carry it; the dot agrees. */
.vj-fits-tab[aria-selected="true"] { font-weight: 600; color: var(--vj-ink);
  border-color: rgba(16, 24, 40, .34); position: relative; }
/* The selected pill grows a foot to the hairline below the strip. That, not the
 * dot's hue, is what says WHICH panel this tab opens — and it survives
 * greyscale, which no tint does. */
.vj-fits-tab[aria-selected="true"]::after { content: ''; position: absolute;
  left: 50%; transform: translateX(-50%); top: 100%; width: 2px; height: 12px;
  background: rgba(16, 24, 40, .34); }
.vj-fits-tab:focus-visible { outline: 2px solid var(--vj-ink); outline-offset: 2px; }
.vj-fits-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.vj-fits-go { display: block; text-align: center; margin-top: 14px;
  font-size: 16px; color: var(--vj-brand); }
.vj-fits-go[hidden] { display: none; }

/* An inline SVG that would otherwise shrink its own text illegible
 * (transforms.mjs scrollUnreadableSvgs). Distinct from .vj-diagram-scroll,
 * whose child rule is `max-width:100% !important` — that squeezes the child to
 * fit, which is why CLAUDE.md records that hatch as inert. This one must be
 * allowed to overflow, so the child keeps a computed minimum width and the
 * container scrolls to it. --vj-svg-min is the width at which the diagram's
 * smallest label reaches 9px. */
.vj-svg-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain; max-width: 100%; }
.vj-svg-scroll > svg { min-width: var(--vj-svg-min, 0); display: block; }

/* A labelled pill must wrap, not collapse.
 *
 * Vin, 2026-09-09, with a screenshot: "the partner stack is mangled (on
 * mobile)". Measured on /partners at 390px: five sector pills — Banking,
 * Insurance, Healthcare, Legal, Hiring — were 19px wide each, their 15px
 * nowrap labels spilling straight out of them, while "Government" in the same
 * flex row kept its full 140px.
 *
 * The cause is the `[style*="display:flex"] > * { min-width: 0 !important }`
 * rule above. That exists to stop a long code line forcing the page wider, and
 * it does — but it also removes the min-content floor that makes a flex row
 * WRAP. With `flex:1` and no floor, six items in a 286px row do not wrap onto a
 * second line; five of them shrink to nothing so the sixth can keep its width.
 *
 * So a pill gets its floor back. Selected by the shape the export actually
 * ships — a flex child that is itself a centred flex box with a radius — not by
 * a class, because these carry none. 88px fits the longest label at 15px and
 * lets three sit per line at 390px.
 */
@media (max-width: 991px) {
  .vj-flex > .vj-fill.vj-rounded.vj-jc-center {
    min-width: 88px !important;
    flex: 1 1 88px !important;
  }
}

/* ========================================================================
   Large desktop — the first rules on this site above 1440px
   ========================================================================

   THE BAND NEVER GREW. Measured before this block existed, the content band
   was 1224px at 1280, at 1440, at 1728, at 1920 and at 2560 — it is a fixed
   inline max-width on every section, so what grew with the screen was the
   empty space beside it: 28px each side on a 13-inch laptop, 348px at 1080p,
   and 668px each side on a 27-inch display. Over half of that screen was
   margin. Vin: "the content seems somewhat compressed in the center".

   WIDENING THE BAND DOES NOT WIDEN THE PROSE, which is the only reason this
   is safe. Every paragraph carries its own cap — 520, 560, 600, 640 or 700px
   — so the reading measure is set one level below this and does not move.
   Measured at 62 characters on /product/dome, 62 on the blog and 69 on
   /pricing, all inside the 65-75 band check-measure exists to defend. What
   widens is the LAYOUT: diagrams, tables, the pricing comparison, the
   multi-column rows.

   !important because the 1224 is an inline style, and an inline style
   outranks every selector. That is the same reason the 132 other
   !importants in this file exist, not a shortcut.

   min() rather than a step: at 1600 this is 1248px, a hair over today's
   1224, and it grows smoothly from there to a 1800px ceiling at 2308 and
   above. A hard jump at one breakpoint is what makes a resize feel broken. */
@media (min-width: 1600px) {
  .vj-band { max-width: min(1800px, 78vw) !important; }

  /* DISPLAY TYPE GROWS WITH THE BAND; BODY COPY DOES NOT. The reading measure
     is already where it should be — 62 characters on /product/dome, 62 on the
     blog, 69 on /pricing, inside the 65-75 band check-measure defends — so
     larger body text would push it out of range in one direction and a wider
     column would push it out in the other. Headings carry no measure, which is
     why they are the half with room.

     clamp() rather than a step, and check-type-scale says why in its own
     comment: a fluid size is a range, not a step, so it is counted and
     reported rather than held to the 17-step scale. This is the same mechanism
     .vj-display and .vj-subhead already use at the phone end of the site; this
     block is its other end.

     EVERY RANGE IS CONTINUOUS AT 1600. The vw term is below the minimum there
     — 3.3vw is 52.8px against a 56px floor — so each heading holds its current
     size at the breakpoint and only grows above it. A step change at the exact
     width where a rule engages is what makes a resize look broken.

     The ceilings land near the scale where they can: 40px and 26px are steps,
     72 and 96 sit between them. Caps are reached at 2182px (.vj-h-56) and
     2182px (.vj-display), so a 27-inch display sits at the top of the range
     rather than past it. */
  .vj-h-56                 { font-size: clamp(56px, 3.3vw, 72px) !important; }
  .vj-display              { font-size: clamp(76px, 4.4vw, 96px) !important; }
  .vj-subhead              { font-size: clamp(40px, 2.4vw, 52px) !important; }
  .vj-h-32, .vj-h-32-plain { font-size: clamp(32px, 1.9vw, 40px) !important; }
  .vj-h-22                 { font-size: clamp(22px, 1.3vw, 26px) !important; }
}
