/* ============================================================
   Sevenbox public site -- design tokens
   Same palette and type scale as the Operations Center itself
   (sevenbox_demo/static/style.css), but this pass deliberately gives the
   marketing site its own visual personality on top of that shared
   base: Poppins for section eyebrows, a pill-shaped primary CTA
   modeled on a reference button treatment, soft-rounded cards for two
   specific containers (problem cells, "what follows" theorem cards)
   modeled on a reference panel treatment, and two illustrated scene
   backgrounds (header/footer). Everything else -- square corners
   everywhere else, hairline borders, color only where it carries
   meaning -- still runs on the same --radius-sm/md/lg: 0 /
   --radius-full: 999px scale as the real product. --radius-soft is a
   new, local-only token for the two card types above; it does not
   exist in the product's own scale and is never used outside them.
   ============================================================ */
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/inter-400.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("fonts/inter-600.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("fonts/inter-700.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/plexmono-400.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500; font-display: swap; src: url("fonts/plexmono-500.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-style: normal; font-weight: 600; font-display: swap; src: url("fonts/plexmono-600.woff2") format("woff2"); }
@font-face { font-family: "Poppins"; font-style: normal; font-weight: 400; font-display: swap; src: url("fonts/poppins-400.woff2") format("woff2"); }
@font-face { font-family: "Poppins"; font-style: normal; font-weight: 500; font-display: swap; src: url("fonts/poppins-500.woff2") format("woff2"); }
@font-face { font-family: "Poppins"; font-style: normal; font-weight: 600; font-display: swap; src: url("fonts/poppins-600.woff2") format("woff2"); }
@font-face { font-family: "Poppins"; font-style: normal; font-weight: 700; font-display: swap; src: url("fonts/poppins-700.woff2") format("woff2"); }

:root {
  --bg-0: #060708;
  --bg-1: #0b0d10;
  --bg-2: #101216;
  --bg-3: #16181c;
  --bg-well: #050608;

  --border: rgba(255, 255, 255, 0.08);
  --border-strong: rgba(255, 255, 255, 0.16);

  --text-primary: rgba(255, 255, 255, 0.92);
  --text-secondary: rgba(255, 255, 255, 0.62);
  --text-tertiary: rgba(255, 255, 255, 0.40);

  --accent: #5fb3ff;
  --accent-soft: rgba(95, 179, 255, 0.1);
  --accent-border-soft: rgba(95, 179, 255, 0.24);
  --accent-rgb: 95, 179, 255;
  --success: #7ee787;
  --success-soft: rgba(126, 231, 135, 0.1);
  --warning: #f2cc60;
  --warning-soft: rgba(242, 204, 96, 0.1);
  --warning-border-soft: rgba(242, 204, 96, 0.24);
  --danger: #ff7b72;
  --danger-soft: rgba(255, 123, 114, 0.1);
  --danger-border-soft: rgba(255, 123, 114, 0.24);

  --kind-axiom: #8b98a5;
  --kind-oracle: #b083f0;
  --kind-tool: #7c93c9;
  --kind-derived: #8fb87f;

  --on-accent: #04121f;

  /* Radius scale, copied from sevenbox_demo/static/style.css verbatim: square
     everywhere a box is a box, kept at 0 rather than removed so every rule
     below still reads its corner from the scale instead of a stray literal.
     --radius-full stays real for pills/badges/dots -- a distinct shape
     language from a card, not "a box that happens to be round." */
  --radius-sm: 0;
  --radius-md: 0;
  --radius-lg: 0;
  --radius-full: 999px;
  /* Local-only exception, see the file-header comment: a moderate,
     "semi-rounded" corner for the two card types deliberately modeled
     on a softer reference panel, distinct from the square scale above. */
  --radius-soft: 14px;

  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  --font-display: "Poppins", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 72px;
  --space-9: 112px;

  --glow-box: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 36px rgba(255, 255, 255, 0.05);
  --measure: 680px;
}

* { box-sizing: border-box; }

html { scrollbar-color: var(--border-strong) var(--bg-0); scrollbar-width: thin; scroll-behavior: smooth; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--bg-0); }
::-webkit-scrollbar-thumb { background-color: var(--border-strong); border-radius: 999px; border: 3px solid var(--bg-0); }

body {
  margin: 0;
  background: var(--bg-0);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }
code, .mono { font-family: var(--font-mono); }

.wrap { max-width: 1120px; margin: 0 auto; padding: 0 var(--space-6); }
@media (max-width: 640px) { .wrap { padding: 0 var(--space-4); } }

section { padding: var(--space-9) 0; }

.eyebrow {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 13.5px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 var(--space-4);
}

h1, h2, h3 { font-family: var(--font-sans); font-weight: 700; line-height: 1.15; margin: 0; letter-spacing: -0.01em; }
h2 { font-size: clamp(28px, 3.6vw, 40px); margin-bottom: var(--space-5); }
h3 { font-size: 20px; margin-bottom: var(--space-3); }
/* Justified body copy, site-wide (Ctrl+J equivalent) -- hyphens: auto
   keeps justify from opening up ugly rivers of whitespace on the
   narrower paragraph widths used throughout the page. */
p {
  margin: 0 0 var(--space-4); color: var(--text-secondary);
  text-align: justify; text-justify: inter-word;
  hyphens: auto; -webkit-hyphens: auto;
}
.lede { font-size: 20px; color: var(--text-secondary); max-width: var(--measure); }

.section-head { max-width: var(--measure); margin-bottom: var(--space-7); }
.section-head p { font-size: 18px; }
/* Every other section-to-section transition already lands on the same
   224px gap for free, just from the shared section{padding} rule -- the
   hero doesn't, because above 900px .hero-sub is absolutely positioned
   and its real bottom edge has nothing to do with .hero's own declared
   padding. Scoped to #frameworks (not the hero) so none of the hero's
   own carefully-tuned internal spacing has to move: this just gives
   "Runs with your stack" the same 224px of air above it that every
   other eyebrow already gets. Extra 137.61px verified with
   getBoundingClientRect(), not calculated by hand. Scoped to the same
   901px breakpoint .hero-sub itself switches on: below it .hero-sub is
   position:static and already produces the standard 224px gap on its
   own, so this same padding would just overshoot it there too. */
@media (min-width: 901px) {
  #frameworks { padding-top: 230.14px; }
}
/* Same normalization, mobile's own number: .hero-sub going static here
   already closes most of the gap on its own, but still landed 26px
   short of the standard 224px every other transition uses. */
@media (max-width: 900px) {
  #frameworks { padding-top: 138px; }
}
/* This one sentence is meant to read as a single beat, not wrap onto a
   second line -- widened past the usual measure just for this section. */
#frameworks .section-head { max-width: none; }
#frameworks .section-head p { white-space: nowrap; }
@media (max-width: 900px) { #frameworks .section-head p { white-space: normal; } }

/* ---------- Scroll reveal ----------
   Gated behind html.js (set by a synchronous inline script in <head>)
   so that a visitor with JavaScript disabled, or a script load failure,
   never ends up with permanently invisible content -- the un-gated,
   no-JS page is simply static and fully visible, exactly as if this
   block didn't exist. Transform/opacity only: never affects layout, so
   nothing shifts while elements wait to be revealed. */
html.js [data-reveal] {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: var(--reveal-delay, 0s);
  will-change: opacity, transform;
}
html.js [data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  html.js [data-reveal] { opacity: 1; transform: none; transition: none; }
}

/* ---------- Section dividers (shooting star) ----------
   One between every pair of sections, marking each category change.
   Per direct request (revised from an earlier full-viewport, endlessly
   looping version): triggered by scroll like every other [data-reveal]
   element, travels left-to-right ONCE across the normal text width
   (not edge-to-edge of the viewport), then settles and stays as a
   static divider line -- a one-shot "draw-in", not a loop. Back on the
   shared [data-reveal]/IntersectionObserver system for exactly that
   reason: it's a single settle-into-place, the same contract every
   other reveal on the page already has.
   Width/padding match .wrap's own (1120px, var(--space-6)/var(--space-4)
   below 640px) directly, rather than nesting a real .wrap in here, so
   this stays a zero-height element and never adds to the gap it sits
   in, while its left/right edges still land exactly under the text
   above and below it. */
.section-divider {
  position: relative;
  height: 0;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--space-6);
  overflow: visible;
  pointer-events: none;
}
@media (max-width: 640px) {
  .section-divider { padding: 0 var(--space-4); }
}
/* Cancels the base [data-reveal] fade+rise for the zero-height
   container itself -- left alone, its own opacity:0/translateY(22px)
   would stack on top of .divider-line/.divider-head's own animation
   below (transform on a parent still moves absolutely-positioned
   children), doubling up into a stray extra shift+fade that has
   nothing to do with the intended left-to-right draw-in. */
html.js .section-divider[data-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
}
/* The hero is the one boundary where "centered on the section edge"
   doesn't land in the middle of the real gap: above 900px, .hero-sub is
   absolutely positioned and its actual content (the button) sits well
   below .hero's own box edge, so a star centered there would sit on
   top of the note/button text instead of in the empty space below
   them. Shifted down to the real midpoint between the button and "Runs
   with your stack", verified with getBoundingClientRect(). Scoped to
   the same 900px breakpoint .hero-sub itself uses: below it, .hero-sub
   goes back to position:static and .hero's own box already ends in the
   right place, so this same offset would just add an extra, unwanted
   211px on top of an already-correct mobile gap. */
@media (min-width: 901px) {
  .section-divider--hero { top: 132.93px; }
}
/* Two independent pieces so the traveling head stays a round dot
   instead of getting squashed by the line's own scaleX growth:
   .divider-line grows from 0 to full width (transform-origin: left),
   .divider-head travels along its leading edge via a plain % "left",
   arriving at the right edge exactly as the line finishes growing.
   Both animate ONCE on reveal and hold their end state -- no infinite
   loop, no fade-out; the settled line + dot together are the divider. */
.divider-line {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  transform: translateY(-50%) scaleX(0);
  transform-origin: left center;
  /* Tail tapers to transparent over the first 14% instead of starting
     at a flat, blunt 0.55 alpha -- gives the left end a pointed, "sharp"
     read instead of a cut-off edge, per direct request. First version
     of the grain (two repeating layers, 4px/7px period, up to 0.3
     alpha) read as visibly dashed/dotted per direct feedback -- tightened
     to a single, much finer, much fainter repeat (2px period, 0.08
     alpha) so it's a subtle shimmer on top of a continuous line, not a
     dotted one. */
  background-image:
    repeating-linear-gradient(90deg,
      rgba(255, 255, 255, 0.08) 0px, rgba(255, 255, 255, 0.08) 1px,
      transparent 1px, transparent 2px),
    linear-gradient(90deg,
      transparent 0%,
      rgba(210, 220, 255, 0.55) 14%,
      rgba(210, 220, 255, 0.55) 90%,
      #fff 100%);
  border-radius: var(--radius-full);
}
/* left starts at 3.5px (half its own 7px width), not 0%: combined with
   translate(-50%) below, a 0% start would render 3.5px into negative x
   -- invisible on its own, but a real bug: content laid out left of x=0
   still counts toward document.documentElement.scrollWidth in Chromium,
   which was tripping a genuine horizontal-overflow check at every
   divider still in its pre-reveal state. Verified by removing this fix
   and reproducing the scrollWidth mismatch, then restoring it. */
.divider-head {
  position: absolute;
  top: 0;
  left: 3.5px;
  width: 7px;
  height: 7px;
  transform: translate(-50%, -50%);
  background: #fff;
  border-radius: 50%;
  opacity: 0;
  /* Dimmed again, more decisively this time (0.72/0.4356/0.17424 ->
     0.47/0.283/0.113, roughly -35%) -- the previous smaller rounds of
     dimming still read as too strong. */
  box-shadow:
    0 0 8px 2px rgba(255, 255, 255, 0.47),
    0 0 22px 8px rgba(160, 195, 255, 0.283),
    0 0 42px 18px rgba(110, 155, 255, 0.113);
}
html.js .section-divider[data-reveal] .divider-line {
  transition: transform 1.05s cubic-bezier(0.65, 0, 0.35, 1);
  transition-delay: var(--reveal-delay, 0s);
}
html.js .section-divider[data-reveal].is-visible .divider-line {
  transform: translateY(-50%) scaleX(1);
}
html.js .section-divider[data-reveal] .divider-head {
  left: 3.5px;
  opacity: 0;
  transition: left 1.05s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.25s ease;
  transition-delay: var(--reveal-delay, 0s);
}
html.js .section-divider[data-reveal].is-visible .divider-head {
  /* Mirrors the 3.5px left-edge fix above: translate(-50%) would
     otherwise push the settled dot's right edge 3.5px past 100%, which
     was tripping the exact same real scrollWidth overflow on the right
     side, for any divider whose box reaches close to the viewport's
     own right edge. Reproduced and confirmed at 1024px before this fix,
     not assumed by symmetry alone. */
  left: calc(100% - 3.5px);
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  html.js .section-divider[data-reveal] .divider-line,
  html.js .section-divider[data-reveal] .divider-head {
    transition: none;
  }
  html.js .section-divider .divider-line { transform: translateY(-50%) scaleX(1); }
  html.js .section-divider .divider-head { left: calc(100% - 3.5px); opacity: 1; }
}
/* Alternating direction per direct request: right/left/right/left across
   the 7 dividers. .section-divider--reversed (applied to the 2nd, 4th,
   6th) mirrors both pieces -- the line grows from its right edge instead
   of its left (transform-origin flip + the gradient authored at 270deg
   instead of 90deg so the bright/white end still lands on whichever
   side the head actually settles on), and the head's start/end left
   values are swapped. Same specificity as the base rules above (one
   extra class each), so it wins the tie by appearing later in the file. */
.section-divider--reversed .divider-line {
  transform-origin: right center;
  background-image:
    repeating-linear-gradient(90deg,
      rgba(255, 255, 255, 0.08) 0px, rgba(255, 255, 255, 0.08) 1px,
      transparent 1px, transparent 2px),
    linear-gradient(270deg,
      transparent 0%,
      rgba(210, 220, 255, 0.55) 14%,
      rgba(210, 220, 255, 0.55) 90%,
      #fff 100%);
}
html.js .section-divider--reversed[data-reveal] .divider-head {
  left: calc(100% - 3.5px);
}
html.js .section-divider--reversed[data-reveal].is-visible .divider-head {
  left: 3.5px;
}
@media (prefers-reduced-motion: reduce) {
  html.js .section-divider--reversed .divider-head { left: 3.5px; }
}

/* ---------- Header ---------- */
header {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-4) var(--space-6);
  /* Fades to fully transparent at its own bottom edge instead of a hard
     border -- the glass blur tapers off with it, so there's nothing left
     to read as a seam. Eased over several stops (not a straight 2-stop
     ramp) so the edge itself reads as genuinely diffused/hazy rather
     than a visible line that just happens to fade -- still ending by
     ~92%, close to the narrow zone that avoided the under-scroll
     ghosting bug earlier. */
  background: linear-gradient(to bottom,
    rgba(6, 7, 8, 0.46) 0%,
    rgba(6, 7, 8, 0.46) 70%,
    rgba(6, 7, 8, 0.34) 82%,
    rgba(6, 7, 8, 0.16) 90%,
    rgba(6, 7, 8, 0) 100%);
  backdrop-filter: blur(10px);
  -webkit-mask-image: linear-gradient(to bottom, black 0%, black 70%, rgba(0,0,0,0.7) 82%, rgba(0,0,0,0.3) 90%, transparent 100%);
  mask-image: linear-gradient(to bottom, black 0%, black 70%, rgba(0,0,0,0.7) 82%, rgba(0,0,0,0.3) 90%, transparent 100%);
}
.brand { display: flex; align-items: center; gap: var(--space-3); text-decoration: none; color: var(--text-primary); }
.brand-mark { width: 25px; height: 25px; flex: 0 0 auto; object-fit: contain; }
.brand-word { font-weight: 700; font-size: 19.5px; letter-spacing: -0.01em; }
nav.primary-nav { display: flex; align-items: center; gap: var(--space-6); }
.nav-links { display: flex; gap: var(--space-5); }
.nav-links a { font-size: 14px; color: var(--text-secondary); text-decoration: none; transition: color 0.15s ease; }
.nav-links a:hover { color: var(--text-primary); }
@media (max-width: 760px) { .nav-links { display: none; } }
/* position: relative + top (not touching the flex row itself) so this
   nudges up on its own, 3% of the header's own height (76.4px -> 2.3px),
   without shifting the logo or nav links beside it. */
nav.primary-nav .btn-primary { position: relative; top: -2.3px; }

/* ---------- Left content rail ----------
   A fixed vertical strip of section icons, one per section, each a
   small pixel-art glyph hinting at what that section holds. Hovering
   (or focusing) an icon slides out a label + one-line description;
   the current section's icon stays lit via scrollspy (rail.js below).
   Hidden below ~1100px so it never competes for space with real
   content -- same progressive-disclosure treatment as .nav-links. */
.content-rail {
  position: fixed;
  left: 28px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 15;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.content-rail::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -20px;
  bottom: -20px;
  width: 1px;
  background: var(--border);
  z-index: -1;
}
@media (max-width: 1100px) { .content-rail { display: none; } }
/* No circle/background anymore -- just the icon art itself, per direct
   request. The box stays (as an invisible hit target + layout slot for
   the label below) but carries no visible border/fill of its own now;
   hover/active read entirely through the icon's own opacity/glow. */
.rail-stop {
  position: relative;
  width: 58px;
  height: 58px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  /* Instant (transition:none) read as too abrupt on direct feedback --
     settled on fast-but-subtle instead: quick enough not to feel
     delayed, short enough that ease-out's curve barely registers as
     motion. */
  transition: transform 0.12s ease-out;
}
/* Plain .rail-stop wasn't enough on its own: html.js [data-reveal]
   (picked up earlier this session for reveal-on-load) sets its own
   transition: transform 0.7s cubic-bezier(...) at equal specificity to
   a plain .rail-stop rule, and was winning that tie by appearing
   earlier in the file -- the hover scale was visibly easing in over
   the reveal system's 0.7s curve instead of this rule's intended
   duration. Reproduced by sampling the element's width every 20ms after
   triggering hover and watching it ramp on the wrong timing, before
   adding this override. html.js prefix matches that rule's specificity
   and, appearing later in the file, wins the tie -- same fix as the
   hover-scale-not-applying-at-all bug above, same root cause. */
html.js .rail-stop { transition: transform 0.12s ease-out; }
/* 46px -> 51px -> 58.65px, another 15% up. */
.rail-stop img { width: 58.65px; height: 58.65px; object-fit: contain; opacity: 0.6; transition: opacity 0.2s ease, filter 0.2s ease, transform 0.2s ease; }
/* 30% growth on hover, per direct correction. Prefixed with html.js to
   match (and, by appearing later in the file, beat) the specificity of
   html.js [data-reveal].is-visible -- .rail-stop picked up data-reveal
   earlier in this session for the reveal-on-load feature, and that
   rule's own transform:translateY(0) was silently winning over this
   one's scale() at equal-or-lower specificity, so hovering never
   visibly changed anything. Reproduced directly (measured
   getComputedStyle(...).transform on hover: no scale component at all,
   just the reveal system's translateY) before applying this fix. */
html.js .rail-stop:hover, html.js .rail-stop:focus-visible { transform: scale(1.3); }
.rail-stop:hover img, .rail-stop:focus-visible img, .rail-stop.is-active img { opacity: 1; }
/* Active section's own icon grows 20% (independent of hover, which
   scales the whole .rail-stop container instead) -- a persistent size
   cue for "you are here", not just a glow. */
.rail-stop.is-active img { filter: drop-shadow(0 0 6px var(--accent)); transform: scale(1.2); }
.rail-label {
  position: absolute;
  left: calc(100% + 16px);
  top: 50%;
  transform: translateY(-50%) translateX(-6px);
  white-space: nowrap;
  background: var(--bg-1);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  padding: 9px 14px;
  box-shadow: var(--glow-box);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
}
.rail-label .rail-label-title { display: block; font-family: var(--font-display); font-weight: 600; font-size: 14px; color: var(--text-primary); }
.rail-label .rail-label-desc { display: block; font-size: 12.5px; color: var(--text-tertiary); margin-top: 2px; }
.rail-stop:hover .rail-label, .rail-stop:focus-visible .rail-label { opacity: 1; transform: translateY(-50%) translateX(0); }
@media (prefers-reduced-motion: reduce) {
  .rail-stop, .rail-stop img, .rail-label { transition: none; }
}

/* ---------- Constellation ----------
   A fixed, decorative field of slowly drifting star-glows anchored to
   the right edge of the viewport -- purely cosmetic, echoing the night
   sky in the header/footer scenes. position: fixed so it never enters
   document flow or triggers layout/scroll reflow; only opacity/transform
   are animated (compositor-only); pointer-events: none so it can never
   intercept a click; low opacity and a left-edge fade so it never
   competes with text. */
.constellation {
  position: fixed;
  top: 0; right: 0;
  width: min(38vw, 520px);
  height: 100vh;
  pointer-events: none;
  z-index: 1;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to left, black 0%, black 40%, transparent 92%);
  mask-image: linear-gradient(to left, black 0%, black 40%, transparent 92%);
}
.constellation .star {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.9) 0%, rgba(95,179,255,0.5) 45%, rgba(95,179,255,0) 72%);
  filter: blur(0.4px);
  animation: star-drift 20s linear infinite, star-twinkle 4s ease-in-out infinite;
}
@keyframes star-drift {
  0% { transform: translate(0, 0); }
  50% { transform: translate(-18px, 26px); }
  100% { transform: translate(0, 0); }
}
@keyframes star-twinkle {
  0%, 100% { opacity: 0.25; }
  50% { opacity: 0.85; }
}
/* UFO easter egg: an infrequent, fast diagonal streak through the
   constellation, spawned and scheduled from JS (see the constellation
   script) -- rare and quick enough to read as a wink, not a distraction.
   Riding the same fixed/pointer-events:none/overflow:hidden/mask-fade
   container as the stars, so it inherits the same "never touches
   reading" guarantees for free. */
.ufo {
  position: absolute;
  right: -60px;
  opacity: 0;
  animation: ufo-fly linear forwards;
  will-change: transform, opacity;
}
.ufo-trail {
  position: absolute;
  right: 44px;
  top: 50%;
  transform: translateY(-50%);
  width: 110px;
  height: 3px;
  background: linear-gradient(to right, transparent, rgba(95, 179, 255, 0.55));
  filter: blur(2px);
}
@keyframes ufo-fly {
  0% { transform: translate(0, 0); opacity: 0; }
  10% { opacity: 1; }
  88% { opacity: 1; }
  /* Vertical drift trimmed way down (was 70px) -- combined with the
     capped 1-11% starting top in the spawner, this keeps the entire pass
     inside the top 20% of the screen instead of drifting toward the
     middle. */
  100% { transform: translate(-640px, 18px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .constellation { display: none; }
}
@media (max-width: 900px) {
  .constellation { display: none; }
}

.btn {
  display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-primary);
  padding: 10px 18px;
  /* Deliberate, small exception to the square-corner scale (--radius-sm is
     still 0), local to .btn only -- same exception the real product carries,
     same value. .btn-primary below overrides this further to a full pill;
     see its own comment for why. */
  border-radius: 6px;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
}
/* background-color, not the background shorthand: the shorthand also
   resets background-image, which silently wiped out .btn-primary's own
   gradient on hover (same specificity, later in source order) and made
   the pill flash to near-black instead of a brighter blue -- a real bug,
   not a styling choice. Longhand here so a button's own background-image
   (if it has one) survives this generic hover. */
.btn:hover { border-color: var(--text-tertiary); background-color: rgba(255, 255, 255, 0.03); }
.btn-primary {
  /* Pill shape modeled on a reference "Schedule demo" button: a local
     override of .btn's own 6px exception, not a change to --radius-sm/md/lg
     and not applied to .btn generally -- square-corner cards elsewhere are
     untouched. Same blue the product already used here; only the shape and
     the permanent, subtle breathing animation are new. */
  border-radius: var(--radius-full);
  background-color: var(--accent);
  background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0) 55%);
  color: var(--on-accent);
  border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 4px 16px rgba(var(--accent-rgb), 0.28);
  animation: btn-breathe 3.4s ease-in-out infinite;
  transition: background-color 0.25s ease, background-image 0.25s ease, filter 0.25s ease, transform 0.15s ease, border-color 0.15s ease;
}
/* Explicit background here (not just filter) so this always wins over
   .btn:hover's generic rule for the same properties, regardless of
   stylesheet order -- a brighter blue plus a diagonal sheen sweep, the
   "metallic" highlight the plain brightness() filter didn't read as. */
.btn-primary:hover {
  animation-play-state: paused;
  background-color: #85c8ff;
  background-image: linear-gradient(135deg, rgba(255,255,255,0.45) 0%, rgba(255,255,255,0.08) 28%, rgba(255,255,255,0) 52%, rgba(255,255,255,0.22) 78%, rgba(255,255,255,0.4) 100%);
  border-color: rgba(255, 255, 255, 0.4);
  transform: translateY(-1px) scale(1.035);
}
@keyframes btn-breathe {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.035); }
}
@media (prefers-reduced-motion: reduce) {
  .btn-primary { animation: none; }
}
.btn-lg { padding: 14px 26px; font-size: 15px; }
.btn-lg .btn-icon { width: 17px; height: 17px; }

/* ---------- Hero / manifesto ----------
   EXPERIMENT (per direct request): swapped with the CTA/footer scene
   below -- this section now uses footer-scene.webp/footer-glow.webp (the
   UFO-less crop) and the footer's centered, non-directional-gradient
   presentation; the CTA/footer section below now uses header-scene.webp
   (the UFO crop) and the hero's old left-aligned, big-text, directional
   scrim presentation. No copy changed on either side, only which image
   and which layout treatment sits where. Easy to revert: swap the two
   image url()s and the two "presentation" blocks back. */
.hero {
  position: relative;
  /* Exactly the source photo's own ratio (1842:854) -- at this ratio
     cover and contain are identical, so the full image renders, full
     width, with no crop and no zoom/compression in either direction. */
  aspect-ratio: 1842 / 854;
  /* Pulled up by the header's own height, so the section (image
     included) starts at the true top of the page instead of right below
     the header -- the header (sticky, higher z-index) simply renders
     over that top slice, hiding it, the same way it would hide anything
     else scrolled underneath it. Simpler than reshaping the image, and
     it's what actually frees up room to bring the text up too. */
  margin-top: -76px;
  /* Background-image position isn't affected by padding-top (it still
     covers the full box from the section's real, pulled-up top edge) --
     only the text's position is. Moved down from 82px to 320px per direct
     request (marked-up screenshot pointing at the gap above the payoff)
     -- the manifesto now sits over the lake/city instead of right under
     the header. */
  padding-top: 220px;
  padding-bottom: 64px;
  background-image:
    linear-gradient(to bottom, rgba(6,7,8,0.88) 0%, rgba(6,7,8,0.5) 40%, rgba(6,7,8,0.55) 62%, #060708 100%),
    url("img/footer-scene.webp");
  background-size: cover;
  background-position: center;
}
/* No padding-left override here on purpose: the base .wrap padding is
   what lines this section's left edge up with every section below it
   (Frameworks' "Runs with your stack" included). */
.hero .wrap { position: relative; z-index: 1; }
@media (max-width: 860px) {
  .hero {
    /* The fixed ratio above makes for a very short section at phone
       widths (not enough room for the manifesto text) -- height goes
       back to being content-driven here, same as before this change. */
    aspect-ratio: auto;
    background-image:
      linear-gradient(to top, #060708 0%, rgba(6,7,8,0.86) 22%, rgba(6,7,8,0.93) 100%),
      url("img/footer-scene.webp");
    background-position: center 30%;
  }
  .hero::before { background-position: center 30%; }
}
/* The cabin windows, animated -- see the matching comment on
   .scene-outro::before below for how this stays pixel-aligned with the
   base photo at any viewport width. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: url("img/footer-glow.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
  animation: window-glow-flicker 4.6s ease-in-out infinite;
}
@keyframes window-glow-flicker {
  0%, 100% { opacity: 0.72; }
  22% { opacity: 0.95; }
  38% { opacity: 0.62; }
  55% { opacity: 1; }
  72% { opacity: 0.78; }
  88% { opacity: 0.9; }
}
@media (prefers-reduced-motion: reduce) {
  .hero::before { animation: none; opacity: 0.85; }
}
/* ---------- Power-on sequence (cabin/city lights) ----------
   Per direct request: dark on load, a "short circuit" flicker at the
   2s mark, then settles into the same steady window-glow-flicker loop
   that always ran before -- opt-in via classes JS adds (only when
   motion is allowed), never the default, so a no-JS or reduced-motion
   visitor gets exactly the prior always-lit-and-flickering behavior
   with zero risk of the scene getting stuck dark. window-power-on's own
   last stop (0.72) matches window-glow-flicker's 0%/100% value on
   purpose, so the handoff between the two animations has no visible
   jump. */
.hero.pre-glow::before, .scene-outro.pre-glow::before {
  opacity: 0;
  animation: none;
}
.hero.powering-on::before, .scene-outro.powering-on::before {
  animation: window-power-on 0.56s linear forwards;
}
@keyframes window-power-on {
  0%   { opacity: 0; }
  9%   { opacity: 0.85; }
  15%  { opacity: 0; }
  23%  { opacity: 0.55; }
  29%  { opacity: 0; }
  38%  { opacity: 1; }
  45%  { opacity: 0.1; }
  54%  { opacity: 0.9; }
  66%  { opacity: 0.5; }
  80%  { opacity: 0.85; }
  100% { opacity: 0.72; }
}
/* The ::before glow alone wasn't enough to read as genuinely "off" --
   the base scene photos have warmth baked into the windows permanently
   (from the earlier warm-glow image pass), so fading just the animated
   highlight over them barely changed how lit they looked (verified with
   a real screenshot at t=0 vs settled: near-identical). This ::after
   layer is a second, generated image -- window pixels only, everything
   else fully transparent -- painted as dark, near-black versions of
   themselves. At opacity:1 it genuinely hides the baked-in warmth
   underneath; fading it out over the same timeline the glow fades in
   is what actually makes the lights read as switching on, not just a
   highlight brightening on top of lights that were already lit. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: url("img/footer-blackout.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
  opacity: 0;
}
.scene-outro::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: url("img/header-blackout.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
  opacity: 0;
}
.hero.pre-glow::after, .scene-outro.pre-glow::after {
  opacity: 1;
}
.hero.powering-on::after, .scene-outro.powering-on::after {
  animation: window-blackout-fade 0.56s linear forwards;
}
/* Inverse of window-power-on's own opacity at each stop (1 - glow's
   value), so the window genuinely flickers on/off during the short
   circuit instead of the highlight flickering over lights that never
   actually went dark. Last stop is an exact 0, not the mathematical
   inverse of 0.72 (0.28) -- the base rule above already rests at 0, so
   ending exactly there means no visible jump when .powering-on is
   removed and this animation hands off. */
@keyframes window-blackout-fade {
  0%   { opacity: 1; }
  9%   { opacity: 0.15; }
  15%  { opacity: 1; }
  23%  { opacity: 0.45; }
  29%  { opacity: 1; }
  38%  { opacity: 0; }
  45%  { opacity: 0.9; }
  54%  { opacity: 0.1; }
  66%  { opacity: 0.5; }
  80%  { opacity: 0.15; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .hero::after, .scene-outro::after { display: none; }
}
/* Back to left-aligned, matching "Runs with your stack" below it (the
   centered version was a one-off preview, not kept) -- no positions or
   spacing touched, only text-align/margin reverted. */
.manifesto { max-width: 980px; }
.manifesto .line {
  font-size: clamp(18px, 2vw, 21px);
  font-weight: 500;
  color: var(--text-secondary);
  margin: 0 0 var(--space-3);
  line-height: 1.45;
  white-space: nowrap;
}
.manifesto .line strong { color: var(--text-primary); font-weight: 600; }
@media (max-width: 900px) { .manifesto .line { white-space: normal; } }

/* The payoff + note + CTA sit as their own block, positioned and centered
   independently of the three lines above. 60% now, after a run of direct
   position requests (67% original bench height -> 47% -> 57% -> 60%),
   photo untouched throughout. Absolute + centered horizontally; falls
   back to normal in-flow stacking on narrow viewports. */
/* Left-aligned now, matching the manifesto lines and "Runs with your
   stack" -- same box as .wrap (max-width 1120px, centered, same side
   padding) instead of its own centered/narrower box, so its left edge
   lands on that exact same vertical line rather than just approximating
   it. */
.hero-sub {
  position: absolute;
  top: 60%;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 1120px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  gap: var(--space-4);
  padding: 0 var(--space-6);
}
/* Widened + sized so "...why an autonomous system believed something,"
   sets on one line (checked against the real rendered text) -- pulls the
   note/CTA below up with it. */
/* Pushed down ~20% of the hero's own height (107px), with the note's own
   margin-top reduced by that exact amount (64px -> -43px) so it and the
   CTA stay exactly where they already were -- only the payoff moves. */
.hero-sub .payoff {
  font-size: clamp(18px, 1.85vw, 22px);
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.4;
  margin: 120.35px 0 0;
}
.hero-sub .payoff .accent-word { color: var(--accent); }
/* The original 80px gap here (16px flex gap + 64px margin) was smaller
   than the 107px the payoff just moved down by, so holding this at its
   old margin would have put the payoff's own bottom edge past the
   note's top -- real overlap, not a style choice. margin-top: 0 is the
   minimum shift (43px down) that clears it while keeping a normal 16px
   gap; the note/CTA aren't fully pinned in place because the geometry
   doesn't allow that without the two overlapping. */
/* Both gaps pulled in another 3% each, independently this time (no
   "don't move the CTA" constraint on this round): payoff-note gap
   15.375px -> 14.91px (margin-top -0.64 -> -1.1); note-CTA gap
   22.625px -> 21.95px (margin-bottom 6.64 -> 5.96). */
/* Centered between payoff and button without moving either: margin-top
   and margin-bottom shifted by the same 4.4875px in opposite directions
   (-1.1 -> -5.59, 5.96 -> 10.45) -- moves the note up within the same
   total space it already occupied, so the button's own top position is
   untouched. Gap above/below was 15.43px/6.46px; now ~10.94px each. */
.hero-sub .note { font-size: 14px; color: var(--text-tertiary); margin: -5.59px 0 10.45px; white-space: nowrap; }
@media (max-width: 900px) { .hero-sub .note { white-space: normal; } }
/* position: relative + top (not flex gap/margin) so only the button
   itself rises -- while the note above and everything else stays
   exactly where it was. Raised again, this time 2% of the hero's own
   height (667.6px -> 13.35px), on top of the earlier -1.1px. */
.hero-sub .btn { position: relative; top: -14.45px; }
@media (max-width: 860px) {
  .hero-sub {
    position: static;
    transform: none;
    left: auto;
    align-items: flex-start;
    text-align: left;
    margin-top: var(--space-7);
    padding: 0;
  }
}

/* The no-framework-note above carries its own var(--space-7) bottom
   margin (deliberate breathing room within #frameworks, not part of the
   inter-section rhythm), which otherwise made this one transition 48px
   wider than the standard 224px every other boundary uses. Trimmed back
   out here rather than touching the note's own margin. */
#problem { padding-top: 64px; }

/* ---------- Problem grid ----------
   Individually boxed cards (not the shared hairline-grid trick used
   elsewhere) so each one can carry its own soft corner and thin border,
   modeled on a reference panel treatment -- see --radius-soft above. */
.problem-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3);
}
@media (max-width: 900px) { .problem-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .problem-grid { grid-template-columns: 1fr; } }
.problem-cell {
  background: var(--bg-1);
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-soft);
  box-shadow: var(--glow-box);
}
/* Poppins + accent blue, matching the eyebrows. The blinking-cursor
   treatment that briefly lived here moved to the eyebrows themselves
   (every section label now carries it), so these card labels stay
   static -- one cursor per section reads as a section-level device,
   four more per problem cell read as noise. */
.problem-cell h3 { font-size: 15px; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; color: var(--accent); font-weight: 600; margin-bottom: var(--space-3); }
.problem-cell p { font-size: 15px; color: var(--text-secondary); margin: 0; }
/* Matches .problem-cell exactly (background, border, radius, shadow) --
   the closing statement reads as one more card in the same family, not a
   separately-styled callout competing with them. */
.problem-answer {
  margin-top: var(--space-3);
  padding: var(--space-5) var(--space-6);
  border: 1px solid var(--border);
  background: var(--bg-1);
  border-radius: var(--radius-soft);
  box-shadow: var(--glow-box);
}
.problem-answer p { color: var(--text-primary); font-size: 17px; margin: 0; }
/* The category-naming follow-up (observability dashboards / governance
   platforms) reads as supporting evidence for the claim above it, not a
   second claim of equal weight -- smaller, secondary color, its own
   gap. */
.problem-answer p + p { margin-top: var(--space-3); color: var(--text-secondary); font-size: 15.5px; }

/* ---------- Diagram ---------- */
.diagram-frame {
  border: 1px solid var(--border);
  background: var(--bg-well);
  border-radius: var(--radius-sm);
  padding: var(--space-6) var(--space-5) var(--space-5);
  box-shadow: var(--glow-box);
  overflow-x: auto;
}
.diagram-caption { display: flex; justify-content: space-between; align-items: baseline; margin-top: var(--space-4); flex-wrap: wrap; gap: var(--space-2); }
.diagram-caption .legend { display: flex; gap: var(--space-4); flex-wrap: wrap; }
/* Direction reversed per direct request (was -7, flowing one way along
   the path; now +7, flowing the other). */
.supersede-line {
  stroke-dasharray: 4 3;
  animation: dash-flow 0.9s linear infinite;
}
@keyframes dash-flow {
  to { stroke-dashoffset: 7; }
}
@media (prefers-reduced-motion: reduce) {
  .supersede-line { animation: none; }
}
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; font-family: var(--font-mono); color: var(--text-tertiary); }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }

/* ---------- The Warrant fields ---------- */
/* margin-bottom:0 kills the browser's default 1em/17px <dl> bottom
   margin -- left alone, it silently made the warrant->proof gap 17px
   wider than every other section-to-section transition. */
/* Premium-card treatment, same recipe as .problem-cell/.theorem-card:
   bg-1, hairline border, --radius-soft, --glow-box shadow, generous
   inset padding -- turns a bare field list into a bounded container
   instead of text floating directly on the page background. */
.fields-list {
  display: grid; grid-template-columns: 1fr 2fr; gap: var(--space-3) var(--space-6);
  max-width: var(--measure); margin-bottom: 0;
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: var(--radius-soft); box-shadow: var(--glow-box);
  padding: var(--space-2) var(--space-6);
}
.fields-list dt { font-family: var(--font-mono); font-size: 14px; color: var(--accent); padding: var(--space-2) 0; border-top: 1px solid var(--border); }
.fields-list dd { margin: 0; padding: var(--space-2) 0; border-top: 1px solid var(--border); color: var(--text-secondary); font-size: 15px; }
.fields-list dt:first-of-type, .fields-list dd:first-of-type { border-top: none; }

/* ---------- Math section ---------- */
.math-layout { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-8); align-items: start; }
@media (max-width: 880px) { .math-layout { grid-template-columns: 1fr; gap: var(--space-6); } }

/* Same premium-card recipe as .fields-list/.problem-cell: bg-1, hairline
   border, --radius-soft, --glow-box, generous inset. Axiom rows keep
   their own top-border dividers inside it. */
.axiom-list {
  display: flex; flex-direction: column; gap: 0;
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: var(--radius-soft); box-shadow: var(--glow-box);
  padding: var(--space-2) var(--space-6);
}
.axiom-item { padding: var(--space-4) 0; border-top: 1px solid var(--border); }
.axiom-item:first-child { border-top: none; }
.axiom-item:last-child { border-bottom: none; }
.axiom-tag { font-family: var(--font-mono); font-size: 13px; color: var(--accent); font-weight: 600; }
.axiom-item h4 { display: inline; font-size: 15px; font-weight: 600; color: var(--text-primary); margin-left: 8px; }
.axiom-item p { margin: var(--space-2) 0 0; font-size: 14.5px; }
.axiom-item.demoted { opacity: 0.82; }
.axiom-item.demoted .axiom-tag { color: var(--text-tertiary); }
.axiom-item.demoted .demoted-tag {
  font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--warning); border: 1px solid var(--warning-border-soft); background: var(--warning-soft);
  /* Pill badge, not a box -- --radius-full, same shape language the real
     product uses for every badge/tag, not an arbitrary in-between radius. */
  padding: 1px 6px; border-radius: var(--radius-full); margin-left: 8px;
}

.theorem-card {
  border: 1px solid var(--border);
  background: var(--bg-1);
  border-radius: var(--radius-soft);
  padding: var(--space-5);
  margin-bottom: var(--space-4);
  box-shadow: var(--glow-box);
}
.theorem-card:last-child { margin-bottom: 0; }
.theorem-name { font-family: var(--font-mono); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--success); margin-bottom: var(--space-2); }
.theorem-card h4 { font-size: 16px; margin: 0 0 var(--space-2); color: var(--text-primary); font-weight: 600; }
.theorem-card p { font-size: 14.5px; margin: 0; }

/* Neon glow tried and dropped, per direct feedback -- back to a plain
   flat solid-color bar, same amber as the original, just on the bottom
   edge instead of the left (the position change from the neon
   experiment is the one part that stuck). Corners and shadow later
   brought in line with the other premium-card boxes (--radius-soft,
   --glow-box) -- the bottom color stripe itself is untouched. */
.honesty-note {
  margin-top: var(--space-6);
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-bottom: 3px solid var(--warning);
  background: var(--bg-1);
  border-radius: var(--radius-soft);
  box-shadow: var(--glow-box);
}
.honesty-note p { font-size: 14.5px; margin: 0; color: var(--text-secondary); }
.honesty-note strong { color: var(--text-primary); }
/* Same treatment as .honesty-note, reused rather than a new one-off
   style -- this is the same kind of aside (an honest negative result),
   just promoted to lead the axioms instead of trailing them. Needs its
   own margin-bottom that the original never did, since that one was
   always the last element in its column; this one is followed directly
   by .math-layout. */
.rigor-callout { margin-bottom: var(--space-6); }

/* ---------- Validation / rigor ---------- */
/* Corners rounded 10% past --radius-soft (14px -> 15.4px), per direct
   request -- overflow:hidden so the outer radius actually clips the
   three flush stat-cells sitting inside it, instead of their own square
   corners poking out past the rounded edge. */
.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 15.4px; overflow: hidden; margin-bottom: var(--space-7); box-shadow: var(--glow-box); }
@media (max-width: 700px) { .stat-row { grid-template-columns: 1fr; } }
.stat-cell { background: var(--bg-1); padding: var(--space-6); }
.stat-value {
  font-family: var(--font-mono); font-size: clamp(32px, 4vw, 44px); font-weight: 600; color: var(--text-primary); display: block;
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum";
}
/* Breathing starts only once the odometer roll has actually settled (JS
   adds this class after the last digit's own roll transition ends) --
   per direct request, so the roll and the breathe never overlap/compete
   as motion. Opacity alone read as too flat/not enough like an actual
   pulse ("palpitar") -- added a matching scale pulse, same restrained
   amount as .btn-primary's own breathe. */
.stat-value.breathing { animation: value-breathe 2.2s ease-in-out infinite; transform-origin: left center; }
@keyframes value-breathe {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.82; transform: scale(1.035); }
}
@media (prefers-reduced-motion: reduce) {
  .stat-value.breathing { animation: none; }
}
.stat-label { font-size: 14px; color: var(--text-tertiary); margin-top: var(--space-2); }
/* Odometer roll for .stat-value's digits/letters (JS builds this markup
   on reveal, see the matching script below the constellation one) --
   each character gets a 1-line-tall window (overflow:hidden) onto a
   vertical strip running from its pool's first character up to the
   target, and JS animates the strip's own translateY(-Nem) so it scrolls
   upward into place, like a real mechanical counter.
   .roll-visual is marked aria-hidden and unselectable: a plain
   element.textContent read of the rolling markup returns every
   intermediate character each track scrolled through concatenated
   together, not just the one landed on and visible -- real, verified
   directly (e.g. "15" came back as "01012345"). Screen readers and
   copy/paste both read the separate .sr-only span instead, which always
   holds the exact final value regardless of animation state. */
.roll-visual { user-select: none; }
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.roll-digit {
  display: inline-block;
  overflow: hidden;
  height: 1em;
  line-height: 1;
  vertical-align: top;
}
.roll-track {
  display: flex;
  flex-direction: column;
  transform: translateY(0);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.3, 0, 0.2, 1);
}
.roll-line {
  display: block;
  height: 1em;
  line-height: 1;
}

/* Smooth, continuous right-to-left marquee of real logos (see
   index.html's comment above this markup for provenance). The track
   holds the tile list twice back to back; animating exactly -50% of its
   own width loops seamlessly with no visible seam or reset-jump.
   Pauses on hover so a reader can actually stop and look, and drops to
   a static, manually-scrollable row under prefers-reduced-motion. */
.marquee-wrap {
  position: relative;
  overflow: hidden;
  margin-bottom: var(--space-7);
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 64px, black calc(100% - 64px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black 64px, black calc(100% - 64px), transparent 100%);
}
.marquee-track {
  display: flex;
  width: max-content;
  gap: var(--space-7);
  animation: marquee-scroll 52s linear infinite;
}
.marquee-wrap:hover .marquee-track { animation-play-state: paused; }
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; }
  .marquee-wrap { overflow-x: auto; }
  .marquee-wrap { -webkit-mask-image: none; mask-image: none; }
}
/* No card box around each logo on purpose: with a hard border/background
   per tile, 14 adjacent boxes read as a grid of separate chips. Dropping
   the box and just floating each mark on the same dark backdrop, spaced
   generously, makes neighbors blend into one continuous flowing row
   instead of a row of distinct cards -- closer to the "smooth transition
   between them" this was asked for than any per-edge crossfade could be. */
.marquee-tile {
  flex: 0 0 auto;
  width: 238px;
  height: 85px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Every logo sits in the identical box (object-fit: contain fills it
   regardless of native aspect ratio) so the 14 marks -- wordmarks, icon
   marks, tall marks, wide marks -- read as one coherent, same-size row.
   Dim/greyed out by default; hovering (or focusing, for keyboard users)
   reveals the real logo at full color, mirroring a "reveal on hover"
   card treatment from a reference marquee. */
.marquee-tile img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: grayscale(1) brightness(1.6) opacity(0.4);
  transition: filter 0.4s ease, transform 0.4s ease;
}
.marquee-tile:hover img, .marquee-tile:focus-within img {
  filter: grayscale(0) brightness(1) opacity(1);
  transform: scale(1.06);
}
@media (prefers-reduced-motion: reduce) {
  .marquee-tile img { transition: none; }
}
/* Minimal, standard fine-print near the logo marquee -- reduces any
   risk of the marquee reading as an endorsement from those projects. */
.trademark-note {
  font-size: 11px;
  font-style: italic;
  color: var(--text-tertiary);
  margin: 0 0 var(--space-4);
}
.no-framework-note {
  font-family: var(--font-display); font-weight: 500; font-size: 16.5px; color: var(--accent);
  margin: 0 0 var(--space-7);
}
/* Typewriter: JS fills .typewriter-text in one character at a time and
   removes data-typewriter when done. Before JS runs (or with JS/motion
   disabled) the full text already sits in .typewriter-text as real markup,
   so nothing depends on the animation actually playing. */
.typewriter-cursor {
  display: inline-block;
  margin-left: 2px;
  animation: caret-blink 1s step-end infinite;
}
@keyframes caret-blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .typewriter-cursor { animation: none; }
}

/* Same as .honesty-note: neon dropped, plain flat solid-color bar on
   the bottom edge, --danger red, thinner (2px vs 3px). Font colors
   (.eyebrow/.fixed-tag/p below) untouched throughout. Background
   changed to the same var(--bg-1) every other bottom-line box uses,
   per direct request -- was var(--danger-soft), a reddish tint. */
.bug-example {
  border: 1px solid var(--danger-border-soft);
  border-bottom: 2px solid var(--danger);
  background: var(--bg-1);
  border-radius: var(--radius-soft);
  box-shadow: var(--glow-box);
  padding: var(--space-5) var(--space-6);
}
.bug-example .eyebrow { color: var(--danger); margin-bottom: var(--space-2); }
.bug-example p { color: var(--text-primary); margin: 0 0 var(--space-2); font-size: 15.5px; }
.bug-example p:last-child { margin-bottom: 0; }
.bug-example .fixed-tag { font-family: var(--font-mono); font-size: 12.5px; color: var(--success); }

/* ---------- FAQ ---------- */
/* Deliberately NOT the premium-card treatment the other boxes on this
   page got -- per direct feedback, FAQ stays a flat list, no
   background/border/shadow container around it. */
.faq-item { border-top: 1px solid var(--border); padding: var(--space-6) 0; }
.faq-item:last-child { border-bottom: 1px solid var(--border); }
.faq-item h3 { font-size: 18px; margin-bottom: var(--space-3); }
.faq-item .competitors { font-family: var(--font-mono); font-size: 13px; color: var(--text-tertiary); margin-top: var(--space-3); }

/* ---------- CTA + footer scene ----------
   One background scene painted once on the shared wrapper, spanning from
   the top of "Get in" through the bottom of the footer -- not two
   separate section backgrounds. Darkest at the very top (CTA heading) and
   very bottom (footer text), lighter in between so the scene actually
   shows through in the gap where there's no text to protect. */
/* EXPERIMENT (see the matching note on .hero above): now carries
   header-scene.webp (the UFO crop) and the old hero-style directional
   scrim + left-aligned big-text presentation. Content unchanged. */
.scene-outro {
  position: relative;
  background-image:
    linear-gradient(100deg, rgba(6,7,8,0.9) 0%, rgba(6,7,8,0.8) 32%, rgba(6,7,8,0.5) 62%, rgba(6,7,8,0.3) 100%),
    linear-gradient(to top, #060708 0%, rgba(6,7,8,0) 22%, rgba(6,7,8,0.4) 78%, rgba(6,7,8,0.85) 100%),
    url("img/header-scene.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
@media (max-width: 860px) {
  /* Same "cover zooms into the center crop" contrast issue as the hero on
     narrow viewports -- same fix: a much darker, near-uniform scrim. */
  .scene-outro {
    background-image:
      linear-gradient(rgba(6,7,8,0.93), rgba(6,7,8,0.9)),
      url("img/header-scene.webp");
    background-position: center 30%;
  }
  .scene-outro::before { background-position: center 30%; }
}
/* Same animated cabin-window glow as the hero (see its comment above),
   sharing the exact background-size/position rule as .scene-outro's own
   photo so the two stay aligned. */
.scene-outro::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: url("img/header-glow.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  pointer-events: none;
  animation: window-glow-flicker 4.6s ease-in-out infinite;
  animation-delay: -1.8s;
}
@media (prefers-reduced-motion: reduce) {
  .scene-outro::before { animation: none; opacity: 0.85; }
}
.cta-section, footer { position: relative; z-index: 1; }
.cta-section { text-align: left; padding-top: var(--space-9); padding-bottom: var(--space-8); }
/* Same 1120px measure as every other section's .wrap, not the narrower
   780px column this used to carry -- that narrower width centered
   itself independently of the rest of the page, so its left edge never
   matched the manifesto/eyebrows above it, per direct request to fix.
   section-head/.lede already clamp to --measure (680px) for readability,
   so this only changes where the block starts, not how wide the text
   actually reads. */
.cta-section .wrap { max-width: 1120px; margin: 0 auto; }
.cta-section .section-head { margin: 0 0 var(--space-6); text-align: left; }
.cta-section .section-head h2 {
  font-size: clamp(30px, 4.2vw, 46px);
  font-weight: 700;
  line-height: 1.2;
}
.cta-section .lede { margin: 0 0 var(--space-6); font-size: clamp(19px, 2.2vw, 22px); }
.cta-actions { display: flex; justify-content: flex-start; }
.cta-fine { margin-top: var(--space-5); font-size: 13.5px; color: var(--text-secondary); }

footer { padding: 72px 0 var(--space-8); }
.footer-row { display: flex; justify-content: flex-start; align-items: center; flex-wrap: wrap; gap: var(--space-2); }
.footer-row .brand-word { font-size: 14px; color: var(--text-tertiary); }
footer .status-line { font-size: 13px; color: var(--text-tertiary); font-family: var(--font-mono); }
.footer-tagline { margin: var(--space-2) 0 0; font-size: 12.5px; color: var(--text-tertiary); }
