/* ============================================================================
   mv-aisi.css — AI Search Intelligence, the page as one moving system.

   The old page was nine flat sections stacked vertically: a paragraph, then a
   grid of cards, then another paragraph. Everything appeared the same way, at
   the same speed, with the same weight, so nothing on it read as a product
   that does anything.

   This is built around three ideas, and every rule below serves one of them.

   1. ONE CONTINUOUS SURFACE. A single fixed aurora field and grid sit behind
      the whole page rather than each section carrying its own background, so
      scrolling moves THROUGH something instead of past a series of panels.
      Section boundaries are dissolves, not edges.

   2. SCROLL IS THE TIMELINE. js/mv-aisi.js writes one number, --p (0 to 1),
      onto each scene as it crosses the viewport. Nothing here listens to
      scroll itself; every transform, draw, fade and reveal is a function of
      that one custom property. That keeps the work in the compositor, keeps
      the choreography readable in one place, and means a scene can be frozen
      at any point by setting --p by hand.

   3. THE DIAGRAM IS THE EXPLANATION. Where the old page put a paragraph beside
      a static icon, the visual does the explaining: ten blue links beside the
      one answer that replaced them, a trend line drawing itself as the section
      is read, a rail that fills as you move down the steps.

   ACCESSIBILITY IS NOT A LAYER ON TOP. prefers-reduced-motion pins --p to its
   finished value and disables every transition, so a reader who asks for less
   motion gets the COMPLETED diagram rather than an empty frame that never
   animates in. That is the failure mode of scroll-driven pages and it is
   designed out here rather than apologised for.

   Light and dark are both first-class: the page ships with a theme boot and
   tokens.css makes light the default. Every colour is a token or a
   color-mix() of one, so the whole system re-tunes with the theme and there is
   no hardcoded hex to drift.
   ========================================================================== */

/* ---------------------------------------------------------------- scaffolding */

/* The page's ground. This has to be stated: the old page carried it on a
   .plt-main class in an inline <style> that this file replaces, and dropping
   it left the body transparent. In light mode that is invisible, because the
   browser default is white and --bg is nearly white anyway. In DARK mode it
   was the whole page: the translucent aurora painted over a default white
   ground, so the field came out pale and near-white body text sat on it
   unreadable, while the panels and the site chrome stayed correctly dark.
   A theme bug that only appears in one theme is exactly the kind that ships. */
.aisi-body {
  /* margin:0 belongs here and was missing: the browser default 8px was showing
     as a gutter around the whole page, backdrops included, on this and on the
     reference page. The class this replaces on /platform/ (.platform-page in
     css/solutions.css) did set it, so converting a page to this system without
     it was a regression. Caught by rendering, invisible in the source. */
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', system-ui, sans-serif;
}

.aisi {
  --aisi-glow: color-mix(in oklab, var(--brand) 55%, transparent);
  --aisi-glow-2: color-mix(in oklab, var(--brand-2) 50%, transparent);
  --aisi-line: color-mix(in oklab, var(--text) 12%, transparent);
  --aisi-line-soft: color-mix(in oklab, var(--text) 7%, transparent);
  --aisi-panel: color-mix(in oklab, var(--panel) 82%, transparent);
  --aisi-ink: var(--text);
  --aisi-ink-2: var(--text-2);
  --aisi-ink-3: var(--muted);
  position: relative;
  isolation: isolate;
  overflow-x: clip;
}

/* The one background for the whole page. Fixed, so it does not scroll with the
   content: the reader moves through the field rather than dragging it along,
   which is what makes the page feel like a space instead of a stack. */
.aisi-field {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    radial-gradient(60vw 50vh at 15% 0%, color-mix(in oklab, var(--brand) 18%, transparent), transparent 70%),
    radial-gradient(50vw 45vh at 85% 15%, color-mix(in oklab, var(--brand-2) 14%, transparent), transparent 70%),
    radial-gradient(70vw 60vh at 50% 100%, color-mix(in oklab, var(--cyan) 10%, transparent), transparent 70%);
  animation: aisi-drift 34s ease-in-out infinite alternate;
}
@keyframes aisi-drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(0, -3%, 0) scale(1.06); }
}

/* A measurement grid, because that is what the product does. It is nearly
   invisible on purpose: present enough to read as instrumentation, faint
   enough that body copy never fights it. */
.aisi-grid-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, var(--aisi-line-soft) 1px, transparent 1px),
    linear-gradient(to bottom, var(--aisi-line-soft) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(120vw 100vh at 50% 30%, black 0%, transparent 78%);
  opacity: .55;
}

[data-theme="dark"] .aisi-field {
  background:
    radial-gradient(60vw 50vh at 15% 0%, color-mix(in oklab, var(--brand) 34%, transparent), transparent 70%),
    radial-gradient(50vw 45vh at 85% 12%, color-mix(in oklab, var(--brand-2) 26%, transparent), transparent 70%),
    radial-gradient(70vw 60vh at 50% 100%, color-mix(in oklab, var(--cyan) 20%, transparent), transparent 72%);
}
[data-theme="dark"] .aisi-grid-bg { opacity: .8; }
[data-theme="dark"] .aisi { --aisi-line: color-mix(in oklab, var(--text) 18%, transparent); }

.aisi-wrap { width: min(1180px, 92vw); margin-inline: auto; }
.aisi-wrap--narrow { width: min(880px, 92vw); }

/* Sections are dissolves. No borders, no background changes: the only thing
   marking a boundary is rhythm, so nothing chops the field into panels. */
.aisi-scene { position: relative; padding: clamp(72px, 11vh, 132px) 0; }
.aisi-scene--tight { padding: clamp(48px, 7vh, 84px) 0; }
.aisi-scene--pull { padding-top: clamp(40px, 6vh, 72px); }

/* ---------------------------------------------------------------- type */

.aisi-eyebrow {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--aisi-ink-3);
  padding: 6px 14px 6px 10px;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-pill);
  background: var(--aisi-panel);
  backdrop-filter: blur(10px);
}
.aisi-eyebrow i {
  width: 6px; height: 6px; border-radius: var(--radius-pill);
  background: var(--brand);
  box-shadow: 0 0 0 0 var(--aisi-glow);
  animation: aisi-ping 2.6s ease-out infinite;
  flex: none;
}
@keyframes aisi-ping {
  0%   { box-shadow: 0 0 0 0 var(--aisi-glow); }
  70%  { box-shadow: 0 0 0 8px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

.aisi-h1 {
  font-family: 'Inter Tight', Inter, system-ui, sans-serif;
  font-size: clamp(34px, 5.4vw, 66px);
  line-height: 1.04; letter-spacing: -.03em; font-weight: 800;
  color: var(--aisi-ink);
  margin: 22px 0 0;
  text-wrap: balance;
}
.aisi-h2 {
  font-family: 'Inter Tight', Inter, system-ui, sans-serif;
  font-size: clamp(26px, 3.4vw, 42px);
  line-height: 1.12; letter-spacing: -.024em; font-weight: 800;
  color: var(--aisi-ink);
  margin: 16px 0 0;
  text-wrap: balance;
}
.aisi-lead {
  font-size: clamp(15.5px, 1.35vw, 18.5px);
  line-height: 1.7; color: var(--aisi-ink-2);
  margin: 18px 0 0; max-width: 62ch;
}
.aisi-grad {
  background: linear-gradient(100deg, var(--brand), var(--brand-2) 55%, var(--cyan));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

/* ---------------------------------------------------------------- reveal */

/* The base reveal. Deliberately small: a 24px rise and a fade, no scale, no
   blur, no rotation. Big entrances on every element are what makes a page feel
   like a template. The scroll-driven scenes below carry the drama; everything
   else just needs to arrive without a jolt. */
.aisi-rise { opacity: 0; transform: translate3d(0, 22px, 0); transition: opacity .7s ease, transform .7s cubic-bezier(.22, 1, .36, 1); }
.aisi-rise.in { opacity: 1; transform: none; }
.aisi-rise[data-d="1"] { transition-delay: .07s; }
.aisi-rise[data-d="2"] { transition-delay: .14s; }
.aisi-rise[data-d="3"] { transition-delay: .21s; }
.aisi-rise[data-d="4"] { transition-delay: .28s; }
.aisi-rise[data-d="5"] { transition-delay: .35s; }

/* ---------------------------------------------------------------- surfaces */

.aisi-panel {
  position: relative;
  border-radius: var(--radius-lg);
  border: 1px solid var(--aisi-line);
  background: var(--aisi-panel);
  backdrop-filter: blur(14px);
  box-shadow: 0 1px 0 color-mix(in oklab, var(--panel) 90%, transparent) inset,
              0 18px 50px -30px color-mix(in oklab, var(--text) 40%, transparent);
}
/* A hairline of brand light along the top edge. One pixel, and it is most of
   what separates a glass panel from a grey box. */
.aisi-panel::before {
  content: ''; position: absolute; inset: 0 0 auto; height: 1px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: linear-gradient(90deg, transparent, color-mix(in oklab, var(--brand) 60%, transparent), transparent);
  opacity: .8;
}

/* ---------------------------------------------------------------- hero */

.aisi-hero { padding-top: clamp(56px, 9vh, 104px); padding-bottom: clamp(40px, 6vh, 72px); }
.aisi-hero-grid {
  display: grid; gap: clamp(28px, 4vw, 56px);
  grid-template-columns: minmax(0, 1.02fr) minmax(0, 1fr);
  align-items: center;
}
@media (max-width: 940px) { .aisi-hero-grid { grid-template-columns: 1fr; } }

.aisi-cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
.aisi-btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 13px 22px; border-radius: var(--radius-pill);
  font-size: 14.5px; font-weight: 700; text-decoration: none;
  transition: transform .22s cubic-bezier(.22, 1, .36, 1), box-shadow .22s ease, border-color .22s ease;
}
.aisi-btn--primary {
  /* --on-brand is a real token, so no literal fallback: a hex here would be a
     second source of truth for the one colour that has to stay legible on the
     brand gradient in both themes. */
  color: var(--on-brand);
  background: linear-gradient(100deg, var(--brand), var(--brand-2));
  
}
.aisi-btn--primary:hover { transform: translateY(-2px);  }
.aisi-btn--ghost {
  color: var(--aisi-ink); border: 1px solid var(--aisi-line);
  background: var(--aisi-panel); backdrop-filter: blur(10px);
}
.aisi-btn--ghost:hover { transform: translateY(-2px); border-color: color-mix(in oklab, var(--brand) 45%, transparent); }
.aisi-btn svg { flex: none; }

.aisi-engines { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 26px; }
.aisi-engine {
  font-size: 12.5px; font-weight: 600; color: var(--aisi-ink-3);
  padding: 7px 13px; border-radius: var(--radius-pill);
  border: 1px solid var(--aisi-line); background: var(--aisi-panel);
  display: inline-flex; align-items: center; gap: 7px;
  transition: color .25s ease, border-color .25s ease, transform .25s ease;
}
.aisi-engine i {
  width: 5px; height: 5px; border-radius: var(--radius-pill);
  background: var(--brand); flex: none; opacity: .8;
}
.aisi-engine:hover { color: var(--aisi-ink); border-color: color-mix(in oklab, var(--brand) 40%, transparent); transform: translateY(-1px); }

/* ---------------------------------------------------------------- SVG shared */

.aisi-svg { display: block; width: 100%; height: auto; overflow: visible; }
/* Colour comes from CSS so both themes and every hover state are handled in
   one place, and so the markup carries no hex. */
.aisi-stroke { stroke: var(--aisi-line); fill: none; }
.aisi-stroke-b { stroke: var(--brand); fill: none; }
.aisi-fill-panel { fill: color-mix(in oklab, var(--panel) 88%, transparent); }
.aisi-fill-ink { fill: var(--aisi-ink); }
.aisi-fill-ink3 { fill: var(--aisi-ink-3); }
.aisi-fill-b { fill: var(--brand); }
.aisi-fill-b2 { fill: var(--brand-2); }
.aisi-fill-soft { fill: color-mix(in oklab, var(--text) 8%, transparent); }
.aisi-txt { font: 600 11px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-2); }
.aisi-txt--sm { font-size: 9.5px; fill: var(--aisi-ink-3); }
.aisi-txt--b { fill: var(--brand); }

/* A drawn line: dash the whole length, then let --p pull the offset to zero.
   pathLength="1" on the element makes the maths independent of real geometry,
   so a path can be re-drawn by a designer without retuning the animation. */
.aisi-draw { stroke-dasharray: 1; stroke-dashoffset: calc(1 - var(--p, 0)); }

/* ---------------------------------------------------------------- hero diagram */

.aisi-stage { position: relative; }
.aisi-stage::after {
  content: ''; position: absolute; inset: -8% -6%;
  background: radial-gradient(closest-side, var(--aisi-glow), transparent 72%);
  opacity: .35; z-index: -1; filter: blur(20px);
}

/* The hero loop runs on its own clock rather than on scroll: it is above the
   fold, where there is no scroll to drive it yet, and a still diagram at the
   top of an AI product page is a wasted first impression. */
.aisi-pulse { animation: aisi-pulse 3.4s ease-in-out infinite; transform-origin: center; }
@keyframes aisi-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

.aisi-flow { stroke-dasharray: 4 7; animation: aisi-flow 1.6s linear infinite; }
@keyframes aisi-flow { to { stroke-dashoffset: -22; } }

.aisi-orbit { animation: aisi-orbit 14s linear infinite; transform-origin: 50% 50%; }
@keyframes aisi-orbit { to { transform: rotate(360deg); } }

.aisi-node { transition: transform .3s cubic-bezier(.22, 1, .36, 1); transform-origin: center; }
.aisi-hit { cursor: default; }
.aisi-hit:hover .aisi-node { transform: scale(1.08); }

/* Each engine answer fades up in turn, so the loop reads as four engines being
   polled one after another rather than four boxes blinking together. */
.aisi-ans { opacity: 0; animation: aisi-ans 7.2s ease-in-out infinite; }
.aisi-ans--1 { animation-delay: .2s; }
.aisi-ans--2 { animation-delay: .9s; }
.aisi-ans--3 { animation-delay: 1.6s; }
.aisi-ans--4 { animation-delay: 2.3s; }
@keyframes aisi-ans {
  0%, 6%    { opacity: 0; transform: translateY(5px); }
  14%, 62%  { opacity: 1; transform: none; }
  74%, 100% { opacity: 0; transform: translateY(-4px); }
}

/* ---------------------------------------------------------------- scene 2: the collapse */

/* The pinned version of this scene is gone. It held the viewport for the best
   part of three screens while showing placeholder bars, so the reader could not
   reach the content and the half-finished state looked like a broken loading
   skeleton. Doing it with position:sticky does not make it not scroll-jacking.
   What is left is a side-by-side comparison that reveals like everything else:
   the contrast was always the argument, and it reads in a glance. */
.aisi-shift {
  display: grid; align-items: center; gap: clamp(16px, 2.6vw, 34px);
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
@media (max-width: 860px) { .aisi-shift { grid-template-columns: 1fr; } }
.aisi-shift-side { display: grid; gap: 12px; justify-items: start; }
.aisi-shift-panel { padding: 22px 22px 20px; width: 100%; min-height: 208px; }
.aisi-shift-panel--on {
  border-color: color-mix(in oklab, var(--brand) 34%, transparent);
  box-shadow: 0 22px 60px -34px var(--aisi-glow);
}
.aisi-shift-panel--on .aisi-answer-body { margin-top: 14px; }
.aisi-shift-note { font-size: 13.5px; line-height: 1.6; color: var(--aisi-ink-3); margin: 0; }
.aisi-shift-arrow { width: 60px; color: var(--brand); }
@media (max-width: 860px) { .aisi-shift-arrow { width: 40px; justify-self: center; transform: rotate(90deg); } }
.aisi-vs-tag--on { color: var(--brand); border-color: color-mix(in oklab, var(--brand) 34%, transparent); }

/* Two more section shapes, both ordinary flow. */
.aisi-why { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 760px) { .aisi-why { grid-template-columns: 1fr; } }

/* Two across, not four. Four columns filled the full 1180px wrap edge to edge,
   which left the section with no margin of its own and made each column a
   narrow ragged block. Two columns inside a capped width give the set real
   white space on both sides and a comfortable measure to read. */
.aisi-act {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(28px, 4vw, 60px) clamp(32px, 5vw, 72px);
  max-width: 900px; margin-inline: auto;
}
@media (max-width: 640px) { .aisi-act { grid-template-columns: 1fr; } }

/* One rule above each column instead of a border around every row. At four
   across it reads as a set of headings rather than a table, and it is the
   lighter of the two so the section stops feeling boxed in. */
.aisi-act-row {
  display: grid; justify-items: center; align-content: start; gap: 0;
  text-align: center; padding: 22px 4px 0;
  border-top: 1px solid var(--aisi-line);
}
.aisi-act-n {
  font-family: 'Inter Tight', Inter, system-ui, sans-serif;
  font-size: 24px; font-weight: 800; letter-spacing: -.02em;
  color: color-mix(in oklab, var(--brand) 55%, transparent);
  margin-bottom: 10px;
}
.aisi-act-row h3 { font-size: 16.5px; font-weight: 700; color: var(--aisi-ink); margin: 0 0 9px; letter-spacing: -.01em; }
.aisi-act-row p { font-size: 14px; line-height: 1.68; color: var(--aisi-ink-2); margin: 0; }

/* The ten results: each row is pushed away and faded on its own delay via
   --i, so the list peels apart from the bottom up instead of dissolving as a
   block. --p is shared; --i is what makes it a sequence. */
/* Muted on purpose: this side of the comparison is the surface you can
   already measure, and it should read as the quieter half. */
.aisi-serp-row { opacity: .75; }

/* ---------------------------------------------------------------- signals dial */

.aisi-dial-wrap {
  display: grid; gap: clamp(24px, 3.4vw, 46px);
  grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  align-items: center;
}
@media (max-width: 900px) { .aisi-dial-wrap { grid-template-columns: 1fr; } }

.aisi-spoke { transition: opacity .45s ease, transform .45s cubic-bezier(.22, 1, .36, 1); transform-origin: 200px 200px; }
.aisi-spoke[data-on="0"] { opacity: .22; }
.aisi-spoke[data-on="1"] { opacity: 1; }

.aisi-sig-list { display: grid; gap: 8px; }
.aisi-sig {
  display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 14px; align-items: start;
  padding: 14px 16px; border-radius: var(--radius);
  border: 1px solid transparent; background: transparent;
  cursor: default; text-align: left; width: 100%; font: inherit; color: inherit;
  transition: background .28s ease, border-color .28s ease, transform .28s ease;
}
.aisi-sig:hover, .aisi-sig:focus-visible, .aisi-sig[data-on="1"] {
  background: var(--aisi-panel);
  border-color: color-mix(in oklab, var(--brand) 30%, transparent);
  transform: translateX(3px);
  outline: none;
}
.aisi-sig-n {
  width: 34px; height: 34px; border-radius: var(--radius-sm);
  display: grid; place-items: center; flex: none;
  border: 1px solid var(--aisi-line);
  color: var(--brand);
  background: color-mix(in oklab, var(--brand) 10%, transparent);
}
.aisi-sig h3 { font-size: 15px; font-weight: 700; color: var(--aisi-ink); margin: 0 0 4px; }
.aisi-sig p { font-size: 13.5px; line-height: 1.6; color: var(--aisi-ink-3); margin: 0; }

/* ---------------------------------------------------------------- tool map */

.aisi-map { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; position: relative; }
@media (max-width: 900px) { .aisi-map { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .aisi-map { grid-template-columns: 1fr; } }

.aisi-tool {
  position: relative; padding: 22px 20px 20px; border-radius: var(--radius-lg);
  border: 1px solid var(--aisi-line);
  background: var(--aisi-panel);
  backdrop-filter: blur(12px);
  overflow: hidden;
  transition: transform .3s cubic-bezier(.22, 1, .36, 1), border-color .3s ease, box-shadow .3s ease;
}
/* A pointer-following highlight. The coordinates come from JS as --mx/--my,
   which is the one thing CSS cannot know, and everything else is CSS. */
.aisi-tool::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(220px circle at var(--mx, 50%) var(--my, 0%),
              color-mix(in oklab, var(--brand) 16%, transparent), transparent 70%);
  opacity: 0; transition: opacity .3s ease;
}
.aisi-tool:hover { transform: translateY(-3px); border-color: color-mix(in oklab, var(--brand) 38%, transparent); box-shadow: 0 22px 50px -34px var(--aisi-glow); }
.aisi-tool:hover::after { opacity: 1; }
.aisi-tool-ic {
  width: 40px; height: 40px; border-radius: var(--radius-sm);
  display: grid; place-items: center; margin-bottom: 14px;
  color: var(--brand);
  border: 1px solid color-mix(in oklab, var(--brand) 22%, transparent);
  background: color-mix(in oklab, var(--brand) 9%, transparent);
}
.aisi-tool h3 { font-size: 15.5px; font-weight: 700; color: var(--aisi-ink); margin: 0 0 7px; letter-spacing: -.01em; }
.aisi-tool p { font-size: 13.5px; line-height: 1.62; color: var(--aisi-ink-3); margin: 0; }
/* A card is normally one paragraph, so p has no margin and a second one would
   butt straight up against the first. Needed by
   solutions/seo-platform-for-agencies, whose Client workspaces card carries an
   elaboration paragraph that belongs to it. */
.aisi-tool p + p { margin-top: 10px; }

/* platform/index.html needed two things the reference page did not. Its
   capability cards are LINKS with a call-to-action label, where .aisi-tool is a
   static <article>; and its second grid holds two cards rather than three, so
   the three-column track left a hole. Both are additive: an .aisi-tool without
   these classes is unchanged. */
.aisi-tool--link { display: block; text-decoration: none; color: inherit; }
.aisi-tool--link:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 3px;
}
.aisi-tool-go {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 12px;
  font-size: 12.5px; font-weight: 700; letter-spacing: .01em; color: var(--brand);
}
.aisi-tool-go::after {
  content: ''; width: 12px; height: 1.4px; background: currentColor;
  border-radius: var(--radius-pill);
  transition: width .3s cubic-bezier(.22, 1, .36, 1);
}
.aisi-tool--link:hover .aisi-tool-go::after { width: 20px; }
.aisi-map--two { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 860px; margin-inline: auto; }
@media (max-width: 560px) { .aisi-map--two { grid-template-columns: 1fr; } }

/* platform/llm-seo-tools needed two shapes the reference page has none of.

   A DATA TABLE. Its "What is LLM SEO?" section compares LLM SEO, GEO and AEO
   across three columns. That is a table, and turning it into cards would throw
   away the column semantics that make it readable - a screen reader would lose
   the header association, and a sighted reader would lose the comparison. It
   scrolls inside its own wrapper rather than forcing the page sideways.

   BODY PROSE. .aisi-lead is one line under a heading and .aisi-close is a
   centred trailing line; neither is a paragraph of argument. This is that. */
.aisi-prose { max-width: 72ch; margin-inline: auto; }
.aisi-prose p { font-size: 15.5px; line-height: 1.75; color: var(--aisi-ink-2); margin: 0 0 16px; }
.aisi-prose p:last-child { margin-bottom: 0; }
.aisi-prose a { color: var(--brand); text-decoration: underline; text-underline-offset: 2px; }
/* Lists inside prose had NO styling at all, so they fell back to the browser
   default: disc bullets at a 40px indent with no spacing between items. That
   shipped on solutions/seo-tools-for-small-business, whose own stylesheet had
   set a 20px indent and 7px between items. Restores that, for every page. */
.aisi-prose ul, .aisi-prose ol { margin: 0 0 16px; padding-left: 20px; }
.aisi-prose li { font-size: 15.5px; line-height: 1.7; color: var(--aisi-ink-2); margin: 0 0 7px; }
.aisi-prose li:last-child { margin-bottom: 0; }
.aisi-prose li strong, .aisi-prose li b { color: var(--aisi-ink); font-weight: 700; }

/* A checklist: each item a bordered row with its own icon, rather than a
   bullet. Needed by solutions/ahrefs-alternative, whose migration steps are
   authored as <li> carrying an inline check SVG. */
.aisi-checks { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 10px; }
.aisi-checks li {
  position: relative; margin: 0; padding: 13px 18px 13px 44px;
  background: var(--aisi-panel); border: 1px solid var(--aisi-line);
  border-radius: var(--radius-lg); font-size: 15px; line-height: 1.6; color: var(--aisi-ink-2);
}
.aisi-checks li svg { position: absolute; left: 16px; top: 15px; width: 17px; height: 17px; color: var(--brand); }
.aisi-checks li b, .aisi-checks li strong { color: var(--aisi-ink); font-weight: 700; }

/* The winner marker in a comparison row. Needed by the two alternative pages,
   where one side of a row is called out as the one that wins it. */
.aisi-table .win { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; color: var(--green); }
.aisi-table .win svg { width: 16px; height: 16px; flex: 0 0 auto; }

.aisi-tablewrap {
  max-width: 900px; margin: 26px auto 0; overflow-x: auto;
  border: 1px solid var(--aisi-line); border-radius: var(--radius-lg);
  background: var(--aisi-panel); backdrop-filter: blur(12px);
}
.aisi-table { width: 100%; border-collapse: collapse; min-width: 560px; }
.aisi-table th, .aisi-table td { text-align: left; padding: 14px 18px; vertical-align: top; }
.aisi-table thead th {
  font-family: "JetBrains Mono", monospace; font-size: 10.5px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--brand);
  border-bottom: 1px solid var(--aisi-line);
}
.aisi-table tbody tr + tr td { border-top: 1px solid var(--aisi-line-soft); }
.aisi-table td { font-size: 14px; line-height: 1.62; color: var(--aisi-ink-3); }
.aisi-table td:first-child { color: var(--aisi-ink); font-weight: 700; white-space: nowrap; }

/* platform/chatgpt-seo-tools mounts the live AI Visibility Checker inline via
   <div data-mv-aivis>, and js/mv-aivis.js builds the form into it. The FORM is
   the module's, but its STYLING was not: .toolform-inner and its input and
   button lived in that page's inline <style>, so deleting the block would have
   left a working tool rendering as an unstyled input and a bare button. These
   are those rules, carried over unchanged in substance. */
.aisi-toolbox { max-width: 720px; margin: 26px auto 0; }
.aisi-toolbox .toolform-inner {
  display: flex; gap: 10px;
  background: var(--aisi-panel);
  border: 1px solid color-mix(in oklab, var(--brand) 26%, transparent);
  border-radius: var(--radius-lg); padding: 8px;
  backdrop-filter: blur(12px);
  box-shadow: 0 22px 50px -34px var(--aisi-glow);
}
.aisi-toolbox .toolform-inner input {
  flex: 1; background: transparent; border: 0; outline: 0; color: var(--aisi-ink);
  font-size: 16px; padding: 14px 16px; font-family: inherit; min-width: 0;
}
.aisi-toolbox .toolform-inner input::placeholder { color: var(--aisi-ink-3); }
.aisi-toolbox .toolform-inner button {
  background: linear-gradient(135deg, var(--brand), var(--brand-2)); color: var(--on-brand);
  border: 0; border-radius: var(--radius); font-weight: 700; font-size: 15px;
  padding: 0 24px; cursor: pointer; font-family: inherit; transition: transform .15s ease;
}
.aisi-toolbox .toolform-inner button:hover { transform: translateY(-1px); }
.aisi-toolbox .toolform-inner button:disabled { opacity: .6; cursor: default; transform: none; }
.aisi-toolbox .meta { text-align: center; color: var(--aisi-ink-3); font-size: 13px; margin-top: 14px; }
.aisi-toolbox .mv-aivis-result { display: block; }
@media (max-width: 560px) {
  .aisi-toolbox .toolform-inner { flex-direction: column; }
  .aisi-toolbox .toolform-inner button { padding: 15px 24px; }
}
@media (prefers-reduced-motion: reduce) { .aisi-toolbox .toolform-inner button { transition: none; } }

/* Same page: its comparison table carries a <caption> and a row header on
   every row, which the llm-seo-tools one did not. */
.aisi-table caption { text-align: left; padding: 16px 18px 0; color: var(--aisi-ink-3); font-size: 13px; }
.aisi-table tbody th {
  text-align: left; padding: 14px 18px; vertical-align: top;
  font-size: 14px; font-weight: 700; color: var(--aisi-ink); white-space: nowrap;
}
.aisi-table tbody tr + tr th { border-top: 1px solid var(--aisi-line-soft); }

/* platform/ai-rank-tracker compares OURS against THEIRS: td.cap labels the
   row, td.mv is our column and td.them is the other one, with our header
   picked out. The base .aisi-table has no notion of a highlighted column, so
   this is a variant rather than a change to it - llm-seo-tools uses the plain
   table and must not move.

   It also relaxes the nowrap on the first column. That is right for a short
   term like "LLM SEO" and wrong for a row label like "What a result means",
   which would force the column far wider than the content needs. */
.aisi-table--vs td:first-child { white-space: normal; width: 22%; }
.aisi-table--vs thead th.mv,
.aisi-table--vs td.mv { background: color-mix(in oklab, var(--brand) 7%, transparent); }
.aisi-table--vs td.mv { color: var(--aisi-ink-2); }
.aisi-table--vs td.them { color: var(--aisi-ink-3); }

/* platform/answer-engine-optimization-tool runs its own AEO check: a hand-
   rolled <form> plus an inline <script> at the foot of that page, not the
   shared mv-aivis module. That script WRITES RESULT MARKUP using .aeo-check,
   .aeo-score, .aeo-tier and .aeo-good/.aeo-warn/.aeo-bad, all of which were
   styled in the inline <style> the port deletes. Dropping them would not break
   the tool - it would return an unstyled list with the pass/fail colour, which
   is semantic, gone.

   So the class NAMES stay as the script writes them and the rules move here
   instead. Surfaces are re-expressed in the aisi tokens so the panel sits on
   the aurora correctly; the status colours stay on --green/--amber/--red,
   which is what they mean. */
.aeo-tool {
  max-width: 68ch; margin: 0 auto;
  background: var(--aisi-panel); border: 1px solid var(--aisi-line);
  border-radius: var(--radius-xl); padding: clamp(20px, 3vw, 30px);
  backdrop-filter: blur(12px);
  box-shadow: 0 22px 50px -34px var(--aisi-glow);
}
.aeo-tool h2 { margin-top: 0; font-size: clamp(22px, 3vw, 30px); font-weight: 800; letter-spacing: -.02em; color: var(--aisi-ink); }
.aeo-form { display: flex; gap: 10px; flex-wrap: wrap; margin: 18px 0 0; }
.aeo-form input {
  flex: 1 1 260px; min-width: 0; padding: 14px 16px; font-size: 15px; font-family: inherit;
  color: var(--aisi-ink); background: var(--bg);
  border: 1px solid color-mix(in oklab, var(--brand) 26%, transparent);
  border-radius: var(--radius-sm);
}
.aeo-form input:focus { outline: 2px solid var(--brand); outline-offset: 1px; }
.aeo-form button {
  padding: 14px 24px; font-size: 15px; font-weight: 700; font-family: inherit; cursor: pointer;
  color: var(--on-brand); background: linear-gradient(135deg, var(--brand), var(--brand-2));
  border: 0; border-radius: var(--radius-sm);
}
.aeo-form button:disabled { opacity: .6; cursor: default; }
.aeo-note { color: var(--aisi-ink-3); font-size: 13.5px; margin: 12px 0 0; }
.aeo-out { margin: 20px 0 0; }
.aeo-msg { color: var(--aisi-ink-3); font-size: 15px; margin: 0; }
.aeo-msg a { color: var(--brand); text-decoration: underline; text-underline-offset: 2px; }
.aeo-scorerow { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 0 0 6px; }
.aeo-score { font-size: 40px; font-weight: 800; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.aeo-tier { font-size: 15px; font-weight: 700; }
.aeo-good { color: var(--green); }
.aeo-warn { color: var(--amber); }
.aeo-bad { color: var(--red); }
.aeo-sub { color: var(--aisi-ink-3); font-size: 14.5px; margin: 0 0 16px; }
.aeo-check { display: flex; gap: 10px; align-items: flex-start; padding: 10px 0; border-top: 1px solid var(--aisi-line-soft); }
.aeo-check .st { font-size: 12px; font-weight: 800; letter-spacing: .04em; flex: 0 0 46px; padding-top: 2px; }
.aeo-check .bd { flex: 1 1 auto; min-width: 0; }
.aeo-check b { font-size: 14.5px; display: block; color: var(--aisi-ink); }
.aeo-check span { color: var(--aisi-ink-3); font-size: 13.5px; line-height: 1.5; }
.aeo-check .wt { flex: 0 0 auto; color: var(--aisi-ink-3); font-size: 12.5px; font-variant-numeric: tabular-nums; padding-top: 3px; }

/* Same page: the scoring table has a numeric weight column that reads better
   right-aligned and tabular. */
/* A price cell in a plan table. td styling is muted by default and the bold
   first-column rule only reaches the row label, so a price in column two would
   read as body text. Needed by solutions/seo-tools-for-small-business. */
.aisi-table td.aisi-price { color: var(--aisi-ink); font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }

.aisi-table--weights td.aeo-w { text-align: right; font-variant-numeric: tabular-nums; color: var(--brand); font-weight: 700; white-space: nowrap; }

/* ---------------------------------------------------------------- timeline */

.aisi-steps { display: grid; gap: 0; position: relative; margin-top: 34px; }
/* The rail fills as the scene scrolls, so the three steps read as one process
   with a position in it rather than three numbered boxes. */
.aisi-rail { position: absolute; left: 19px; top: 12px; bottom: 12px; width: 2px; background: var(--aisi-line); border-radius: var(--radius-pill); }
.aisi-rail::after {
  content: ''; position: absolute; inset: 0 0 auto; width: 100%;
  height: calc(var(--p, 0) * 100%);
  background: linear-gradient(180deg, var(--brand), var(--brand-2));
  border-radius: var(--radius-pill);
  box-shadow: 0 0 12px var(--aisi-glow);
}
@media (max-width: 620px) { .aisi-rail { display: none; } }

.aisi-step { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 20px; padding: 0 0 30px; position: relative; }
.aisi-step:last-child { padding-bottom: 0; }
.aisi-step-n {
  width: 40px; height: 40px; border-radius: var(--radius-pill);
  display: grid; place-items: center; font-weight: 800; font-size: 14px;
  color: var(--brand); background: var(--panel);
  border: 1px solid color-mix(in oklab, var(--brand) 32%, transparent);
  position: relative; z-index: 1;
}
.aisi-step h3 { font-size: 17px; font-weight: 700; color: var(--aisi-ink); margin: 8px 0 8px; }
.aisi-step p { font-size: 14.5px; line-height: 1.7; color: var(--aisi-ink-2); margin: 0; }
@media (max-width: 620px) { .aisi-step { grid-template-columns: 1fr; gap: 10px; } }

/* ---------------------------------------------------------------- versus */

.aisi-vs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
@media (max-width: 820px) { .aisi-vs { grid-template-columns: 1fr; } }
.aisi-vs-card { padding: 26px 24px; border-radius: var(--radius-lg); border: 1px solid var(--aisi-line); background: var(--aisi-panel); backdrop-filter: blur(12px); position: relative; overflow: hidden; }
.aisi-vs-card h3 { font-size: 17px; font-weight: 700; margin: 16px 0 10px; color: var(--aisi-ink); }
.aisi-vs-card p { font-size: 14.5px; line-height: 1.7; color: var(--aisi-ink-2); margin: 0; }
.aisi-vs-tag {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--aisi-ink-3); padding: 5px 11px; border-radius: var(--radius-pill);
  border: 1px solid var(--aisi-line);
}
.aisi-vs-card--ai .aisi-vs-tag { color: var(--brand); border-color: color-mix(in oklab, var(--brand) 34%, transparent); }

/* ---------------------------------------------------------------- faq */

.aisi-faq { display: grid; gap: 10px; margin-top: 30px; }
.aisi-q { border: 1px solid var(--aisi-line); border-radius: var(--radius); background: var(--aisi-panel); overflow: hidden; transition: border-color .28s ease; }
.aisi-q[open] { border-color: color-mix(in oklab, var(--brand) 32%, transparent); }
.aisi-q summary {
  cursor: pointer; list-style: none; padding: 18px 20px;
  font-size: 15.5px; font-weight: 650; color: var(--aisi-ink);
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.aisi-q summary::-webkit-details-marker { display: none; }
.aisi-q summary svg { flex: none; color: var(--aisi-ink-3); transition: transform .3s cubic-bezier(.22, 1, .36, 1), color .3s ease; }
.aisi-q[open] summary svg { transform: rotate(45deg); color: var(--brand); }
.aisi-q-body { padding: 0 20px 20px; font-size: 14.5px; line-height: 1.75; color: var(--aisi-ink-2); }

/* ---------------------------------------------------------------- callout + band */

.aisi-callout {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 20px;
  padding: 28px 30px; border-radius: var(--radius-lg);
  border: 1px solid color-mix(in oklab, var(--brand) 26%, transparent);
  background:
    radial-gradient(90% 160% at 8% 0%, color-mix(in oklab, var(--brand) 12%, transparent), transparent 60%),
    var(--aisi-panel);
  backdrop-filter: blur(12px);
}
.aisi-callout h3 { font-size: 19px; font-weight: 700; margin: 0 0 7px; color: var(--aisi-ink); }
.aisi-callout p { font-size: 14.5px; line-height: 1.65; color: var(--aisi-ink-3); margin: 0; max-width: 56ch; }

.aisi-band { text-align: center; padding: clamp(64px, 9vh, 108px) 0 clamp(72px, 10vh, 118px); }
.aisi-band .aisi-cta-row { justify-content: center; }
.aisi-note { font-size: 13.5px; color: var(--aisi-ink-3); margin: 16px auto 0; max-width: 58ch; }

/* ---------------------------------------------------------------- section heads */

/* Every section opens the same way: eyebrow, heading, one line of lead, all
   centred. They were left-aligned and the page read as eight separate
   decisions, because a centred head above a centred grid and a left head
   above the next one is the kind of inconsistency you feel before you can
   name it. The hero is deliberately not one of these - it sits beside its
   diagram in a two-column grid, where centring the text would break the
   pairing. */
.aisi-sec-head {
  max-width: 68ch;
  margin: 0 auto clamp(30px, 4vh, 48px);
  text-align: center;
}

.aisi-close {
  margin: clamp(42px, 6vh, 76px) auto 0;
  max-width: 78ch;
  text-align: center;
}
.aisi-sec-head .aisi-lead { margin: 14px auto 0; }

/* .aisi-close and .aisi-note are trailing lines and both can carry a link.
   Neither styled one, so an <a> inside them fell through to the browser
   default: measured at rgb(0,0,238) on chatgpt-seo-tools, which is unreadable
   on the dark ground and wrong on the light one. .aisi-prose already did this
   correctly; these now match it. */
.aisi-close a,
.aisi-note a {
  color: var(--brand);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ---------------------------------------------------------------- the collapse panel */

.aisi-collapse-serp { display: grid; gap: 13px; }
.aisi-serp-row { display: grid; gap: 7px; }
/* --i is what turns one shared --p into a sequence: the last row leaves first,
   so the list peels apart from the bottom rather than dissolving as a block.
   Set here rather than as a style attribute, which the design ratchet bans and
   which would put presentation in the markup anyway. */
.aisi-serp-row:nth-child(1) { --i: 0; }
.aisi-serp-row:nth-child(2) { --i: 1; }
.aisi-serp-row:nth-child(3) { --i: 2; }
.aisi-serp-row:nth-child(4) { --i: 3; }
.aisi-serp-row:nth-child(5) { --i: 4; }
.aisi-serp-row:nth-child(6) { --i: 5; }
.aisi-serp-row:nth-child(7) { --i: 6; }
.aisi-serp-row:nth-child(8) { --i: 7; }
.aisi-serp-t { display: block; height: 8px; border-radius: var(--radius-pill); background: color-mix(in oklab, var(--brand) 34%, transparent); width: 56%; }
.aisi-serp-u { display: block; height: 6px; border-radius: var(--radius-pill); background: var(--aisi-line); width: 38%; }
.aisi-serp-row:nth-child(even) .aisi-serp-t { width: 48%; }
.aisi-serp-row:nth-child(3n) .aisi-serp-t { width: 64%; }
.aisi-serp-row:nth-child(3n) .aisi-serp-u { width: 30%; }

.aisi-answer { position: relative; width: 100%; max-width: 560px; }
.aisi-answer-head { margin-bottom: 14px; }
.aisi-answer-body { font-size: clamp(16px, 1.6vw, 21px); line-height: 1.6; color: var(--aisi-ink); margin: 0 0 18px; font-weight: 500; letter-spacing: -.01em; }
.aisi-answer-cites { display: flex; flex-wrap: wrap; gap: 8px; }
.aisi-chip {
  font-size: 12.5px; font-weight: 650; padding: 7px 13px;
  border-radius: var(--radius-pill); border: 1px solid var(--aisi-line);
  color: var(--aisi-ink-3); background: color-mix(in oklab, var(--text) 4%, transparent);
}
.aisi-chip--on {
  color: var(--brand);
  border-color: color-mix(in oklab, var(--brand) 45%, transparent);
  background: color-mix(in oklab, var(--brand) 12%, transparent);
  box-shadow: 0 0 18px -6px var(--aisi-glow);
}

/* ---------------------------------------------------------------- trend + steps */

.aisi-time-grid { display: grid; gap: clamp(20px, 3vw, 38px); grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); align-items: start; }
@media (max-width: 940px) { .aisi-time-grid { grid-template-columns: 1fr; } }

.aisi-chart { padding: 22px 22px 16px; position: sticky; top: 96px; }
@media (max-width: 940px) { .aisi-chart { position: static; } }
.aisi-chart-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 18px; flex-wrap: wrap; }
.aisi-chart-val { font-size: 12px; font-weight: 600; color: var(--aisi-ink-3); letter-spacing: .04em; }

/* Both arrive after the line has drawn, in the order the product surfaces
   them: the threshold you set, then the run that crossed it. */
.aisi-alert { opacity: clamp(0, calc((var(--p, 0) - .45) * 3), 1); }
.aisi-drop {
  opacity: clamp(0, calc((var(--p, 0) - .7) * 4), 1);
  transform-origin: 416px 96px;
  transform: scale(calc(.6 + (clamp(0, calc((var(--p, 0) - .7) * 4), 1) * .4)));
}

/* ---------------------------------------------------------------- versus svg */

.aisi-vs-svg { margin: 4px 0 6px; max-width: 320px; }

/* ---------------------------------------------------------------- svg gradient stops */

/* stop-color cannot be set from a class attribute in every engine, so the
   stops carry classes and the colour is applied here. Keeps hex out of the
   markup, which the design ratchet checks, and keeps both themes correct. */
.aisi-gs1 { stop-color: var(--brand); }
.aisi-gs2 { stop-color: var(--brand-2); }

/* ================================================================ /platform/
   The platform overview diagram. Its own idea, deliberately not the AI Search
   Intelligence fan-out and not the Solutions split: ONE domain feeds TWO live
   surfaces at once - a ten-blue-links column and a written assistant answer -
   and both stream down into a SINGLE scorecard. Breadth converging on one
   source of truth is the only thing this page has to say that its sub-pages do
   not, so it is the only thing the diagram says.

   Everything animates transform, opacity or stroke-dashoffset. Nothing here
   touches a property that costs layout. */

/* The shared query bar fills left to right. scaleX rather than width, so it is
   a compositor transform and not a reflow per frame. */
.pf-fill { transform-origin: left center; animation: pf-fill 6.4s ease-in-out infinite; }
@keyframes pf-fill {
  0%        { transform: scaleX(.06); }
  22%, 100% { transform: scaleX(1); }
}

/* The caret sits at the end of the typed query. It rests SOLID rather than
   mid-blink, so a stilled frame reads as a finished field, not a broken one. */
.pf-caret { animation: pf-caret 1.05s steps(1, end) infinite; }
@keyframes pf-caret { 0%, 100% { opacity: 1; } 45%, 92% { opacity: .15; } }

/* Both lanes populate in parallel: SERP rows on the left, answer lines on the
   right. Same cycle length as the query bar so the three read as one loop. */
.pf-row, .pf-line { animation: pf-in 6.4s ease-in-out infinite; }
@keyframes pf-in {
  0%, 24%   { opacity: .12; transform: translateY(3px); }
  38%, 100% { opacity: 1; transform: none; }
}
.pf-d1 { animation-delay: .08s; }
.pf-d2 { animation-delay: .20s; }
.pf-d3 { animation-delay: .32s; }
.pf-d4 { animation-delay: .44s; }
.pf-d5 { animation-delay: .56s; }
.pf-d6 { animation-delay: .68s; }
.pf-d7 { animation-delay: .80s; }

/* Your brand inside the answer. It arrives last of everything in the lane,
   because being named is the outcome the rest of the picture leads to. */
.pf-named { animation: pf-named 6.4s ease-in-out infinite; transform-origin: center; }
@keyframes pf-named {
  0%, 46%   { opacity: .18; transform: scale(.94); }
  60%, 100% { opacity: 1; transform: none; }
}

/* The scorecard values stamp after both lanes have reported. */
.pf-val { animation: pf-val 6.4s ease-in-out infinite; transform-origin: center; }
@keyframes pf-val {
  0%, 62%   { opacity: 0; transform: translateY(4px); }
  74%, 100% { opacity: 1; transform: none; }
}

/* The ambient sweep. A soft band crossing the scorecard so the panel reads as
   live instrumentation rather than a printed result. Slow on purpose: fast
   enough to notice once, slow enough to ignore while reading. */
/* The sweep is the one thing here that is PURE MOTION: frozen, a band of
   light is just a block sitting on the panel. So it rests at opacity 0 and
   only exists mid-cycle. Caught by rendering with reduced motion, where the
   animation ends and the element falls back to its base style - which is
   exactly how a decorative sweep becomes a permanent grey rectangle. */
.pf-sweep { opacity: 0; animation: pf-sweep 9s cubic-bezier(.45, 0, .55, 1) infinite; }
@keyframes pf-sweep {
  0%, 100% { transform: translateX(-120px); opacity: 0; }
  16%      { opacity: .5; }
  50%      { transform: translateX(300px); opacity: .5; }
  72%      { transform: translateX(420px); opacity: 0; }
}

/* Hovering one surface dims the other, so the reader feels the platform holding
   both rather than choosing between them. Written as container-hover plus
   self-hover rather than :has(), so it needs no selector support to work. */
.pf-surface { transition: opacity .34s ease; }
@media (hover: hover) {
  .pf-surfaces:hover .pf-surface { opacity: .32; }
  .pf-surfaces .pf-surface:hover { opacity: 1; }
}

.pf-ss0 { stop-color: var(--brand); stop-opacity: 0; }
.pf-ss1 { stop-color: var(--brand); stop-opacity: .55; }

.pf-mono { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink-3); letter-spacing: .08em; }
.pf-num  { font: 800 15px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink); }
.pf-num--b { fill: var(--brand); }
.pf-good { fill: var(--green); }

/* ---- the five-tool constellation, above the capability grid ---------------- */

.pf-const { display: block; width: min(100%, 560px); margin: 0 auto clamp(20px, 3vw, 34px); }
.pf-spoke { transition: opacity .4s ease; opacity: .9; }
.pf-hub { animation: pf-hub 5.2s ease-in-out infinite; transform-origin: center; }
@keyframes pf-hub { 0%, 100% { opacity: 1; } 50% { opacity: .62; } }

/* Hovering a tool card brightens its spoke. This is an ENHANCEMENT: without
   :has() every spoke simply stays at its resting opacity, which is a legible
   constellation rather than a broken one. */
@supports selector(:has(a)) {
  @media (hover: hover) {
    .pf-wrap:has(.pf-t1:hover) .pf-spoke:not(.pf-s1),
    .pf-wrap:has(.pf-t2:hover) .pf-spoke:not(.pf-s2),
    .pf-wrap:has(.pf-t3:hover) .pf-spoke:not(.pf-s3),
    .pf-wrap:has(.pf-t4:hover) .pf-spoke:not(.pf-s4),
    .pf-wrap:has(.pf-t5:hover) .pf-spoke:not(.pf-s5) { opacity: .2; }
  }
}
/* Keyboard reaches it too: a card is an <a>, so focus does what hover does. */
@supports selector(:has(a)) {
  .pf-wrap:has(.pf-t1:focus-visible) .pf-spoke:not(.pf-s1),
  .pf-wrap:has(.pf-t2:focus-visible) .pf-spoke:not(.pf-s2),
  .pf-wrap:has(.pf-t3:focus-visible) .pf-spoke:not(.pf-s3),
  .pf-wrap:has(.pf-t4:focus-visible) .pf-spoke:not(.pf-s4),
  .pf-wrap:has(.pf-t5:focus-visible) .pf-spoke:not(.pf-s5) { opacity: .2; }
}

/* Mobile is calm. The looping lanes and the sweep stop, everything sits in its
   finished state, and the constellation goes away rather than shrinking into an
   unreadable tangle - the five cards under it say the same thing in text. */
@media (max-width: 700px) {
  .pf-fill, .pf-caret, .pf-row, .pf-line, .pf-named, .pf-val, .pf-sweep, .pf-hub { animation: none; }
  .pf-fill { transform: scaleX(1); }
  .pf-row, .pf-line, .pf-named, .pf-val { opacity: 1; transform: none; }
  .pf-sweep { opacity: 0; }
  .pf-const { display: none; }
}

/* =================================================== /platform/ai-rank-tracker
   Presence in AI answers, watched over time. The signature object is a live
   appearance chart: runs accrue left to right across a 90-day window, a filled
   point means the answer named you and a hollow one means it did not, and a
   threshold line the trend can fall under is what turns a reading into an
   email.

   The old diagram here was a prompt, three cards and a static line. It showed
   the plumbing and none of the movement, which is the only thing this page is
   about. */

/* The line replays history left to right on its own clock - the hero is above
   the fold, so there is no scroll to drive it - and RESTS fully drawn. */
.rt-play { stroke-dasharray: 1; stroke-dashoffset: 0; animation: rt-play 8s ease-in-out infinite; }
@keyframes rt-play {
  0%        { stroke-dashoffset: 1; }
  46%, 100% { stroke-dashoffset: 0; }
}

/* Runs land in order as the line reaches them. Each rests visible. */
.rt-pt { animation: rt-pt 8s ease-in-out infinite; transform-origin: center; }
@keyframes rt-pt {
  0%, 8%    { opacity: 0; transform: scale(.5); }
  20%, 100% { opacity: 1; transform: none; }
}
.rt-p1 { animation-delay: .10s; } .rt-p2 { animation-delay: .38s; }
.rt-p3 { animation-delay: .66s; } .rt-p4 { animation-delay: .94s; }
.rt-p5 { animation-delay: 1.22s; } .rt-p6 { animation-delay: 1.50s; }
.rt-p7 { animation-delay: 1.78s; } .rt-p8 { animation-delay: 2.06s; }
.rt-p9 { animation-delay: 2.34s; } .rt-p10 { animation-delay: 2.62s; }

/* The most recent run arrives last and keeps a ring, because "latest" is the
   number the reader is actually here for. */
.rt-latest { animation: rt-latest 8s ease-in-out infinite; transform-origin: center; }
@keyframes rt-latest {
  0%, 40%   { opacity: 0; transform: scale(.4); }
  54%, 100% { opacity: 1; transform: none; }
}

/* Deltas fire on the moves, after the point they belong to has landed. */
.rt-delta { animation: rt-delta 8s ease-in-out infinite; }
@keyframes rt-delta {
  0%, 30%   { opacity: 0; transform: translateY(4px); }
  44%, 100% { opacity: 1; transform: none; }
}

/* The alert. It marks a run that crossed the threshold, so it is a STATE and
   rests visible; the expanding ring is a single ping per cycle rather than a
   spinner, because what actually happens is one email, not a process. */
.rt-alert { animation: rt-alert 8s ease-out infinite; transform-origin: center; }
@keyframes rt-alert {
  0%, 52%   { opacity: 0; }
  60%, 100% { opacity: 1; }
}
/* opacity:0 in the BASE rule, not just at 100%. Without animation-fill-mode a
   finished animation reverts to the base style rather than holding its last
   keyframe, so a ring that is only transparent in @keyframes comes back as a
   frozen halo the moment motion is switched off. Second time this exact trap
   appeared; the platform sweep was the first. */
.rt-ring { opacity: 0; transform-origin: center; animation: rt-ring 8s ease-out infinite; }
@keyframes rt-ring {
  0%, 56%  { opacity: 0; transform: scale(.4); }
  64%      { opacity: .75; transform: scale(1); }
  80%, 100% { opacity: 0; transform: scale(1.9); }
}

/* Ambient sweep across the plot, same contract as the platform one: PURE
   motion, so it rests invisible. Frozen, a band of light is just a block. */
.rt-sweep { opacity: 0; animation: rt-sweep 11s cubic-bezier(.45, 0, .55, 1) infinite; }
@keyframes rt-sweep {
  0%, 100% { transform: translateX(-140px); opacity: 0; }
  18%      { opacity: .42; }
  55%      { transform: translateX(330px); opacity: .42; }
  76%      { transform: translateX(470px); opacity: 0; }
}

/* Hovering a run opens that day's prompts-by-engines grid - the CSV matrix,
   one row per prompt per engine. Hidden by default and revealed on hover of
   its own group, so it costs no JS and no layout. */
.rt-grid { opacity: 0; transform: translateY(4px); transition: opacity .22s ease, transform .22s ease; pointer-events: none; }
@media (hover: hover) {
  .rt-run:hover .rt-grid { opacity: 1; transform: none; }
  .rt-run:hover .rt-pt { animation: none; opacity: 1; transform: none; }
}

/* The threshold. Its position is a custom property so the range input below the
   chart can move it, and so it still renders at a sensible default with no JS
   at all. --rt-th is a y coordinate in the SVG's own user space. */
.rt-thline { transform: translateY(calc((var(--rt-th, 40) - 40) * -1.9px)); transition: transform .12s linear; }

/* The 1-to-100 ladder beside the blind-spot paragraph. A Google position is a
   number on a scale; an AI answer is named or not. Showing both at once is the
   whole argument of that section, so it is drawn rather than described. */
.rt-ladder { display: block; width: min(100%, 300px); margin-inline: auto; }
.rt-rung { transition: opacity .3s ease; }

/* Dragging the threshold re-marks every past run, so the reader can see which
   history WOULD have alerted rather than being told. The driver writes
   data-under on input; with no JS every run simply stays unmarked and the
   chart still reads. */
.rt-run[data-under="1"] .rt-pt { stroke: var(--red); stroke-width: 2; }
.rt-run[data-under="1"] .rt-pt.aisi-fill-b { fill: var(--red); }

.rt-mono { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink-3); letter-spacing: .08em; }
.rt-num { font: 800 14px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink); }
.rt-num--b { fill: var(--brand); }
.rt-up { fill: var(--green); }
.rt-down { fill: var(--red); }
.rt-hollow { fill: var(--panel); stroke: var(--brand); stroke-width: 1.6; }

/* The threshold control. A native range input, so it is draggable with a
   pointer AND operable from the keyboard without a line of custom code. */
.rt-ctl { display: flex; align-items: center; gap: 12px; margin: 14px auto 0; max-width: 340px; }
.rt-ctl label { font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .08em; color: var(--aisi-ink-3); white-space: nowrap; }
.rt-ctl input { flex: 1 1 auto; min-width: 0; accent-color: var(--brand); }
.rt-ctl output { font: 800 12px/1 'JetBrains Mono', ui-monospace, monospace; color: var(--brand); min-width: 34px; text-align: right; }

/* Mobile is calm: the replay, the ping and the sweep stop, everything sits in
   its finished state, and the threshold control goes away rather than becoming
   a fiddly target on a chart that is already small. */
@media (max-width: 700px) {
  .rt-play, .rt-pt, .rt-latest, .rt-delta, .rt-alert, .rt-ring, .rt-sweep { animation: none; }
  .rt-play { stroke-dashoffset: 0; }
  .rt-pt, .rt-latest, .rt-delta, .rt-alert { opacity: 1; transform: none; }
  .rt-ring, .rt-sweep { opacity: 0; }
  .rt-ctl { display: none; }
}

/* ====================================================== /platform/llm-seo-tools
   The loop and the gauge. Five tools in a fixed order, each handing to the
   next and the last feeding back to the first, with a standing in the middle
   that climbs a band every time the loop closes. */

/* Each tool lights in turn. The NODE rests visible - it is a real part of the
   method, not an event - and only the halo travels. */
.ls-halo { opacity: 0; transform-origin: center; animation: ls-halo 10s ease-in-out infinite; }
@keyframes ls-halo {
  0%, 4%   { opacity: 0; transform: scale(.7); }
  8%       { opacity: .85; transform: scale(1); }
  18%, 100% { opacity: 0; transform: scale(1.25); }
}
.ls-h1 { animation-delay: 0s; }
.ls-h2 { animation-delay: 2s; }
.ls-h3 { animation-delay: 4s; }
.ls-h4 { animation-delay: 6s; }
.ls-h5 { animation-delay: 8s; }

/* The hand-off between tools, drawn along its real direction. */
.ls-hop { stroke-dasharray: 1; stroke-dashoffset: 0; animation: ls-hop 10s linear infinite; }
@keyframes ls-hop {
  0%, 6%    { stroke-dashoffset: 1; }
  14%, 100% { stroke-dashoffset: 0; }
}
.ls-o1 { animation-delay: 0s; } .ls-o2 { animation-delay: 2s; }
.ls-o3 { animation-delay: 4s; } .ls-o4 { animation-delay: 6s; }
.ls-o5 { animation-delay: 8s; }

/* The gauge climbs a band per pass and RESTS at the top: the method works, and
   the finished state of the picture is the outcome it argues for. */
.ls-arc { stroke-dasharray: 1; stroke-dashoffset: 0; animation: ls-arc 10s cubic-bezier(.5,0,.2,1) infinite; }
@keyframes ls-arc {
  0%       { stroke-dashoffset: .82; }
  22%      { stroke-dashoffset: .60; }
  46%      { stroke-dashoffset: .40; }
  70%      { stroke-dashoffset: .18; }
  88%, 100% { stroke-dashoffset: 0; }
}

/* The band the gauge is currently in. Rests on the last one. */
.ls-band { opacity: .28; animation: ls-band 10s steps(1, end) infinite; }
@keyframes ls-band { 0%, 100% { opacity: .28; } }
.ls-b1 { animation-name: ls-b1; } .ls-b2 { animation-name: ls-b2; }
.ls-b3 { animation-name: ls-b3; }
/* The top band carries opacity:1 in its BASE rule, not only at 100%. With
   motion off the animation reverts to base, so without this every band sits
   dim and the gauge reads as no standing at all - the opposite of what the
   loop argues. Third time this trap has appeared; it is a property of the
   reduced-motion rule having no animation-fill-mode. */
.ls-b4 { opacity: 1; animation-name: ls-b4; }
@keyframes ls-b1 { 0%, 21% { opacity: 1; } 22%, 100% { opacity: .28; } }
@keyframes ls-b2 { 0%, 21% { opacity: .28; } 22%, 45% { opacity: 1; } 46%, 100% { opacity: .28; } }
@keyframes ls-b3 { 0%, 45% { opacity: .28; } 46%, 69% { opacity: 1; } 70%, 100% { opacity: .28; } }
@keyframes ls-b4 { 0%, 69% { opacity: .28; } 70%, 100% { opacity: 1; } }

/* The probe: a category question goes out and comes back with somebody else
   named. Pure motion, so it rests invisible. */
.ls-probe { opacity: 0; animation: ls-probe 10s ease-in-out infinite; }
@keyframes ls-probe {
  0%, 74%  { opacity: 0; transform: translateY(0); }
  80%      { opacity: 1; transform: translateY(0); }
  92%      { opacity: 1; transform: translateY(-6px); }
  100%     { opacity: 0; transform: translateY(-6px); }
}

/* Ambient sweep over the gauge panel. Pure motion; rests invisible. */
.ls-sweep { opacity: 0; animation: ls-sweep 13s cubic-bezier(.45,0,.55,1) infinite; }
@keyframes ls-sweep {
  0%, 100% { transform: translateX(-150px); opacity: 0; }
  18%      { opacity: .38; }
  56%      { transform: translateX(340px); opacity: .38; }
  78%      { transform: translateX(480px); opacity: 0; }
}

/* ---- click a tool to see what it measures and what it feeds --------------- */

/* Radios rather than JS: this works with no script at all, it is keyboard
   operable for free, and the selected tool survives a re-render. The inputs are
   visually hidden but never display:none, which would take them out of the tab
   order entirely. */
.ls-pick { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
/* The label still CARRIES the tool name - that is where the control gets its
   accessible name from - but the text must not paint, or it lands at browser
   default size on top of the diagram. font-size:0 collapses the glyphs while
   leaving the accessible name intact; display:none or visibility:hidden would
   take the label out of the accessibility tree with it. */
.ls-hot { position: absolute; width: 19%; height: 15%; border-radius: var(--radius-pill); cursor: pointer; font-size: 0; color: transparent; }
.ls-hot:hover { background: color-mix(in oklab, var(--brand) 10%, transparent); }
.ls-pick:focus-visible + .ls-hot { outline: 2px solid var(--brand); outline-offset: 2px; }
.ls-hot--1 { left: 40.5%; top: 3%; }
.ls-hot--2 { left: 72%; top: 30%; }
.ls-hot--3 { left: 60%; top: 71%; }
.ls-hot--4 { left: 21%; top: 71%; }
.ls-hot--5 { left: 9%; top: 30%; }

.ls-detail { opacity: 0; transition: opacity .25s ease; }
.ls-p1:checked ~ .aisi-svg .ls-d1,
.ls-p2:checked ~ .aisi-svg .ls-d2,
.ls-p3:checked ~ .aisi-svg .ls-d3,
.ls-p4:checked ~ .aisi-svg .ls-d4,
.ls-p5:checked ~ .aisi-svg .ls-d5 { opacity: 1; }
.ls-p1:checked ~ .aisi-svg .ls-n1 .ls-ring,
.ls-p2:checked ~ .aisi-svg .ls-n2 .ls-ring,
.ls-p3:checked ~ .aisi-svg .ls-n3 .ls-ring,
.ls-p4:checked ~ .aisi-svg .ls-n4 .ls-ring,
.ls-p5:checked ~ .aisi-svg .ls-n5 .ls-ring { opacity: 1; }
.ls-ring { opacity: 0; transition: opacity .25s ease; }

/* ---- the nested scopes ---------------------------------------------------- */

/* Rings expand as the section arrives, driven by --p from the scroll driver
   rather than a clock, because this one IS below the fold. Rests expanded. */
.ls-scope { transform-origin: 150px 150px; transform: scale(calc(.72 + (var(--p, 1) * .28))); opacity: calc(.35 + (var(--p, 1) * .65)); }
.ls-scopes { display: block; width: min(100%, 340px); margin: 0 auto; }

/* ---- the 30-day band ladder ---------------------------------------------- */

/* Fills as the plan is read: the gauge advancing a band per milestone, driven
   by the same --p the steps rail already uses. */
.ls-fill { transform-origin: left center; transform: scaleX(var(--p, 1)); }
.ls-ladder { display: block; width: min(100%, 620px); margin: 0 auto clamp(18px, 2.6vw, 30px); }

.ls-mono { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink-3); letter-spacing: .08em; }
.ls-mono--b { fill: var(--brand); }
.ls-cap { font: 700 10px/1 'Inter Tight', Inter, system-ui, sans-serif; fill: var(--aisi-ink-2); }

/* Mobile: the loop stops turning, the gauge sits at its finished band, the
   hotspots go away rather than becoming five overlapping tap targets on a
   shrunken diagram, and the detail panels all hide. */
@media (max-width: 700px) {
  .ls-halo, .ls-hop, .ls-arc, .ls-band, .ls-probe, .ls-sweep { animation: none; }
  .ls-hop, .ls-arc { stroke-dashoffset: 0; }
  .ls-band { opacity: .28; }
  .ls-b4 { opacity: 1; }
  .ls-halo, .ls-probe, .ls-sweep { opacity: 0; }
  .ls-hot { display: none; }
  .ls-scope { transform: none; opacity: 1; }
}

/* ================================================= /platform/chatgpt-seo-tools
   One engine, one page, one answer. The three OpenAI agents arrive at a gate
   with different jobs, the page is read, a passage is LIFTED out of it, and it
   lands attributed inside a reply that is still typing.

   Access is drawn as the precondition rather than described as one: the agent
   that is blocked stops at the gate, and the citation that depends on it can be
   switched off by the reader. */

/* The three agents cross to the gate. Each rests THROUGH, because being
   admitted is the normal state; the blocked one is held by a separate rule. */
.cg-agent { animation: cg-agent 11s ease-in-out infinite; }
@keyframes cg-agent {
  0%, 4%    { opacity: 0; transform: translateX(-26px); }
  12%, 100% { opacity: 1; transform: none; }
}
.cg-a1 { animation-delay: 0s; }
.cg-a2 { animation-delay: .45s; }
.cg-a3 { animation-delay: .9s; }

/* A blocked agent never reaches the gate. It rests SHORT of it, which is the
   whole point: no amount of quotable content matters behind a closed door. */
.cg-blocked { animation: cg-blocked 11s ease-in-out infinite; }
@keyframes cg-blocked {
  0%, 4%    { opacity: 0; transform: translateX(-26px); }
  12%, 100% { opacity: 1; transform: translateX(-30px); }
}

/* The quotable passage highlights, then lifts out of the page and becomes the
   citation inside the answer. It rests LIFTED - the finished state of this
   picture is the page having been cited. */
.cg-passage { animation: cg-passage 11s cubic-bezier(.34,1.2,.4,1) infinite; }
@keyframes cg-passage {
  0%, 26%   { opacity: 0; transform: none; }
  32%       { opacity: 1; transform: none; }
  46%       { opacity: 1; transform: translate(96px, 118px) scale(.88); }
  54%, 100% { opacity: 0; transform: translate(96px, 118px) scale(.88); }
}

/* The passage's own glow while it is being chosen. Pure motion, rests off. */
.cg-mark { opacity: 0; animation: cg-mark 11s ease-in-out infinite; }
@keyframes cg-mark { 0%, 22% { opacity: 0; } 30%, 44% { opacity: 1; } 52%, 100% { opacity: 0; } }

/* The reply types out a line at a time and RESTS complete. */
.cg-line { transform-origin: left center; animation: cg-line 11s ease-out infinite; }
@keyframes cg-line {
  0%, 40%   { opacity: 0; transform: scaleX(.04); }
  50%, 100% { opacity: 1; transform: none; }
}
.cg-l1 { animation-delay: 0s; } .cg-l2 { animation-delay: .5s; }
.cg-l3 { animation-delay: 1s; } .cg-l4 { animation-delay: 1.5s; }

/* The caret that runs ahead of the text. Rests solid, so a stilled frame reads
   as a finished reply rather than a stalled one. */
.cg-caret { animation: cg-caret 1.05s steps(1, end) infinite; }
@keyframes cg-caret { 0%, 100% { opacity: 1; } 45%, 92% { opacity: .12; } }

/* The citation pill lands last, because attribution is the outcome everything
   above it is for. */
.cg-cite { animation: cg-cite 11s cubic-bezier(.34,1.2,.4,1) infinite; transform-origin: left center; }
@keyframes cg-cite {
  0%, 52%   { opacity: 0; transform: scale(.7); }
  62%, 100% { opacity: 1; transform: none; }
}

/* Ambient sweep over the answer surface. Pure motion; rests invisible. */
.cg-sweep { opacity: 0; animation: cg-sweep 12s cubic-bezier(.45,0,.55,1) infinite; }
@keyframes cg-sweep {
  0%, 100% { transform: translateX(-160px); opacity: 0; }
  20%      { opacity: .36; }
  58%      { transform: translateX(360px); opacity: .36; }
  80%      { transform: translateX(500px); opacity: 0; }
}

/* ---- the reader can close the door ---------------------------------------- */

/* A checkbox, not script: it works with no JS, it is keyboard operable, and it
   is announced as a control rather than as decoration. Checking it blocks
   OAI-SearchBot, and the citation that depends on live retrieval goes with it -
   which is the page's own point about which bot is the expensive one. */
.cg-toggle { display: flex; align-items: center; gap: 10px; margin: 14px auto 0; max-width: 360px; }
/* The input sits BEFORE the svg, because the whole effect is sibling
   selectors reaching forward from :checked - a control placed after the
   diagram cannot reach it and would silently do nothing. It is clipped rather
   than display:none so it keeps its place in the tab order and its accessible
   name; the visible affordance is the label, which works from anywhere. */
.cg-pick { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); }
.cg-box { width: 15px; height: 15px; flex: none; border: 1.5px solid var(--aisi-line); border-radius: var(--radius-xs); background: var(--panel); transition: background .2s ease, border-color .2s ease; }
.cg-block:checked ~ .cg-toggle .cg-box { background: var(--brand); border-color: var(--brand); }
.cg-pick:focus-visible ~ .cg-toggle .cg-box { outline: 2px solid var(--brand); outline-offset: 2px; }
.cg-toggle label { display: flex; align-items: center; gap: 10px; }
.cg-toggle label { font: 700 10px/1.4 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .06em; color: var(--aisi-ink-3); cursor: pointer; }

.cg-gone { opacity: 0; transition: opacity .3s ease; }
.cg-block:checked ~ .aisi-svg .cg-cite,
.cg-block:checked ~ .aisi-svg .cg-passage { opacity: 0; animation: none; }
.cg-block:checked ~ .aisi-svg .cg-gone { opacity: 1; }
.cg-block:checked ~ .aisi-svg .cg-searchbot { transform: translateX(-30px); animation: none; }
.cg-block:checked ~ .aisi-svg .cg-bar-2 { opacity: 1; }
.cg-bar { opacity: 0; transition: opacity .3s ease; }

/* ---- the seven-step ladder ------------------------------------------------ */

/* Each rung stays inert until the one above it has arrived. The steps are
   driven by the scene's own scroll progress rather than a clock, so "nothing
   below matters until this is done" is literal rather than stated. Rests at
   full, because --p is 1 once the section has been read. */
.cg-rung { opacity: clamp(.24, calc((var(--p, 1) - (var(--cg-i, 0) * .1)) * 9), 1); transition: opacity .2s linear; }
.cg-rung--1 { --cg-i: 0; } .cg-rung--2 { --cg-i: 1; } .cg-rung--3 { --cg-i: 2; }
.cg-rung--4 { --cg-i: 3; } .cg-rung--5 { --cg-i: 4; } .cg-rung--6 { --cg-i: 5; }
.cg-rung--7 { --cg-i: 6; }

.cg-mono { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink-3); letter-spacing: .07em; }
.cg-mono--b { fill: var(--brand); }
.cg-mono--no { fill: var(--red); }
.cg-cap { font: 600 10px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-2); }

/* Mobile: the reply sits finished, the agents sit where they ended, the sweep
   and the passage-in-flight stop, and the toggle goes rather than becoming a
   stray control under a shrunken diagram. */
@media (max-width: 700px) {
  .cg-agent, .cg-blocked, .cg-passage, .cg-mark, .cg-line, .cg-caret, .cg-cite, .cg-sweep { animation: none; }
  .cg-agent, .cg-line, .cg-cite, .cg-caret { opacity: 1; transform: none; }
  .cg-blocked { opacity: 1; transform: translateX(-30px); }
  .cg-passage, .cg-mark, .cg-sweep { opacity: 0; }
  .cg-toggle { display: none; }
  .cg-rung { opacity: 1; }
}

/* ============================================ /platform/answer-engine-optimization-tool
   A page examined and graded. Everything on this page is one mechanism seen
   three times: the hero plays it through once, the AEO/SEO/GEO planes show
   what the grade is FOR, and the scorer lets the reader drive it.

   WHY THE HERO RUNS ON ITS OWN CLOCK AND NOT ON --p
   It sits above the fold, where there is no scroll to drive anything yet, so a
   scroll-linked hero is a still picture at the moment it matters most. It plays
   its narrative once on arrival with fill-mode:both, then keeps two ambient
   loops running (the scan and a slow instrument sweep) so the panel reads as
   live rather than finished. Because it is pure CSS, it also plays with no JS
   at all -- this diagram never needed the driver.

   THE NUMBERS ARE THE REAL ONES. 20/20/18/14/14/14 summing to 100 are the
   weights the ai_overview check actually applies in _worker.js, and the table
   in the body states them in text. A diagram that invented its own weights
   would be the most expensive kind of decoration.
   ---------------------------------------------------------------------- */

/* ---- shared SVG type for this page ---------------------------------------- */

.aeo-mono { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink-3); letter-spacing: .08em; }
.aeo-lbl { font: 600 10.5px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-2); }
.aeo-w-n { font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--brand); }
.aeo-score-n { font: 800 27px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink); }
.aeo-chip-t { font: 800 8px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .06em; }

/* Gradient stops live here, not in the markup, so both themes are handled in
   one place and the SVG carries no colour literal. */
.aeo-gs-0 { stop-color: var(--brand); stop-opacity: 0; }
.aeo-gs-1 { stop-color: var(--brand); stop-opacity: .55; }
.aeo-gs-sweep-0 { stop-color: var(--aisi-glow); stop-opacity: 0; }
.aeo-gs-sweep-1 { stop-color: var(--aisi-glow); stop-opacity: .5; }

/* ---- hero: the document being read ---------------------------------------- */

/* The scan is the only thing in the hero that never stops. It is the product's
   verb -- the page is being read right now -- so it keeps going after the
   narrative has resolved. */
.aeo-scan { animation: aeo-scan 4.2s cubic-bezier(.65, 0, .35, 1) infinite; }
@keyframes aeo-scan {
  0% { transform: translateY(-10px); opacity: 0; }
  8% { opacity: 1; }
  86% { opacity: 1; }
  100% { transform: translateY(150px); opacity: 0; }
}

/* The two files the check pulls alongside the page itself. They arrive after
   the scan has started, because that is the order it happens in. */
.aeo-file { animation: aeo-pop .5s cubic-bezier(.22, 1, .36, 1) both; transform-box: view-box; transform-origin: center; }
.aeo-file--1 { animation-delay: .6s; }
.aeo-file--2 { animation-delay: .82s; }
@keyframes aeo-pop { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---- hero: the six weighted bars ------------------------------------------ */

/* scaleX from the left edge, so a bar's length IS its weight and nothing about
   the layout moves while it grows. fill-box origin keeps that true whatever
   the rect's x happens to be. */
.aeo-bar {
  transform-box: fill-box; transform-origin: 0 50%;
  animation: aeo-bar .62s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes aeo-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.aeo-b1 { animation-delay: 1.10s; }
.aeo-b2 { animation-delay: 1.52s; }
.aeo-b3 { animation-delay: 1.94s; }
.aeo-b4 { animation-delay: 2.36s; }
.aeo-b5 { animation-delay: 2.78s; }
.aeo-b6 { animation-delay: 3.20s; }

/* Each weight number lands with its own bar rather than all six appearing at
   once, which is what makes the total feel accumulated instead of asserted. */
.aeo-wn { animation: aeo-fade .4s ease both; }
.aeo-wn1 { animation-delay: 1.42s; }
.aeo-wn2 { animation-delay: 1.84s; }
.aeo-wn3 { animation-delay: 2.26s; }
.aeo-wn4 { animation-delay: 2.68s; }
.aeo-wn5 { animation-delay: 3.10s; }
.aeo-wn6 { animation-delay: 3.52s; }
@keyframes aeo-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---- hero: the score ring ------------------------------------------------- */

/* pathLength="1" again, so the arc is authored as a fraction: .68 of the ring
   for a score of 68, and a designer can move the circle without retuning it. */
.aeo-ring {
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: aeo-ring 2.6s cubic-bezier(.33, 1, .68, 1) 1.1s both;
}
@keyframes aeo-ring { from { stroke-dashoffset: 1; } to { stroke-dashoffset: .32; } }

/* A real count-up, done the only way CSS can: five stacked numbers, each
   visible for its slice of the climb. The last one is the only one that ends
   visible, so under prefers-reduced-motion -- where every animation snaps to
   its final frame -- what remains on screen is 68 and nothing else. */
.aeo-cnt { opacity: 0; animation: aeo-cnt-mid .1s linear both; }
@keyframes aeo-cnt-mid { from { opacity: 1; } to { opacity: 0; } }
.aeo-cnt1 { animation-delay: 1.30s; animation-duration: .5s; }
.aeo-cnt2 { animation-delay: 1.80s; animation-duration: .55s; }
.aeo-cnt3 { animation-delay: 2.35s; animation-duration: .55s; }
.aeo-cnt4 { animation-delay: 2.90s; animation-duration: .8s; }
.aeo-cnt-final { animation: aeo-fade .35s ease 3.70s both; }

/* ---- hero: the self-sorting fix list -------------------------------------- */

/* Rows are placed by transform alone, never by document order, so the sort is
   a transition rather than a reflow: nothing below them moves, and the browser
   does no layout work at 60fps. */
.aeo-fix { transform-box: view-box; }
.aeo-f1 { animation: aeo-f1 .78s cubic-bezier(.22, 1, .36, 1) 4.35s both; }
.aeo-f2 { animation: aeo-f2 .78s cubic-bezier(.22, 1, .36, 1) 4.45s both; }
.aeo-f3 { animation: aeo-f3 .78s cubic-bezier(.22, 1, .36, 1) 4.40s both; }
@keyframes aeo-f1 { from { transform: translateY(388px); } to { transform: translateY(358px); } }
@keyframes aeo-f2 { from { transform: translateY(418px); } to { transform: translateY(388px); } }
@keyframes aeo-f3 { from { transform: translateY(358px); } to { transform: translateY(418px); } }

.aeo-chip-fix { fill: color-mix(in oklab, var(--red) 18%, transparent); }
.aeo-chip-pass { fill: color-mix(in oklab, var(--green) 18%, transparent); }
.aeo-chip-t--fix { fill: var(--red); }
.aeo-chip-t--pass { fill: var(--green); }

/* ---- hero: the ambient instrument sweep ----------------------------------- */

/* The slow one. Nothing is measured by it; it is the difference between a
   diagram and a panel that is switched on. */
.aeo-sweep { animation: aeo-sweep 7.5s linear infinite; }
@keyframes aeo-sweep {
  0% { transform: translateX(-180px); }
  100% { transform: translateX(460px); }
}

/* ---- AEO / SEO / GEO as three layers, not three rivals --------------------- */

.aeo-planes { max-width: 520px; margin-inline: auto; }
.aeo-plane { transform-box: view-box; }
.aeo-plane-face {
  fill: color-mix(in oklab, var(--panel) 92%, transparent);
  stroke: color-mix(in oklab, var(--text) 22%, transparent);
}
/* AEO is the layer this page is about, so it is the one picked out. The other
   two are context, drawn in the panel colour. */
.aeo-plane--aeo .aeo-plane-face {
  fill: color-mix(in oklab, var(--brand) 10%, var(--panel));
  stroke: color-mix(in oklab, var(--brand) 45%, transparent);
}
.aeo-plane--aeo .aeo-plane-t { fill: var(--brand); }
.aeo-plane-t { font: 700 15px/1 'Inter Tight', Inter, system-ui, sans-serif; fill: var(--aisi-ink); }

/* ---- the scorer the reader drives ----------------------------------------- */

.aeo-scorer {
  display: grid; gap: clamp(20px, 3vw, 34px);
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  align-items: start;
  /* One number drives both the row pitch and the height of the box they sort
     inside. They were two literals, which is a bug waiting for the first
     narrow screen: a label that wraps makes rows taller than the pitch and
     they overlap, whatever the container height says. */
  --aeo-pitch: 47px;
  padding: clamp(18px, 3vw, 28px);
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-lg);
  background: color-mix(in oklab, var(--panel) 70%, transparent);
}
@media (max-width: 720px) { .aeo-scorer { grid-template-columns: 1fr; } }

.aeo-dial { position: relative; }
.aeo-dial svg { display: block; width: 100%; height: auto; max-width: 200px; margin: 0 auto; }
.aeo-dial-ring { stroke-dasharray: 1; stroke-dashoffset: calc(1 - (var(--aeo-total, 68) / 100)); transition: stroke-dashoffset .5s cubic-bezier(.22, 1, .36, 1); }
.aeo-dial-n { font: 800 34px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink); }
.aeo-dial-sub { font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace; fill: var(--aisi-ink-3); letter-spacing: .1em; }
.aeo-dial-cap { margin: 10px 0 0; text-align: center; font-size: 13px; color: var(--aisi-ink-3); }

/* Fixed-height stack: the rows sort by transform inside it, so the panel never
   changes height and nothing below the widget jumps while the reader plays. */
.aeo-rows { position: relative; height: calc(var(--aeo-pitch) * 6); }
/* Narrow enough that a long signal name takes two lines; the pitch grows with
   it so the sort still lands cleanly. */
@media (max-width: 430px) { .aeo-scorer { --aeo-pitch: 60px; } }

.aeo-row {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; align-items: center; gap: 12px;
  min-height: 41px; padding: 0 12px;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  cursor: pointer;
  transform: translateY(calc(var(--aeo-slot, 0) * var(--aeo-pitch)));
  transition: transform .55s cubic-bezier(.22, 1, .36, 1), border-color .2s ease;
}
.aeo-row:hover { border-color: color-mix(in oklab, var(--brand) 42%, transparent); }
.aeo-row:focus-within { outline: 2px solid var(--brand); outline-offset: 2px; }

/* The checkbox is the control, not a decoration on top of one: it is what the
   keyboard focuses, what a screen reader announces, and what flips the chip
   with no script running at all. */
.aeo-row input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }

.aeo-st {
  flex: 0 0 42px; text-align: center;
  font: 800 10px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .06em;
  padding: 5px 0; border-radius: var(--radius-xs);
  color: var(--red); background: color-mix(in oklab, var(--red) 14%, transparent);
}
.aeo-row input:checked ~ .aeo-st { color: var(--green); background: color-mix(in oklab, var(--green) 14%, transparent); }
.aeo-st-pass { display: none; }
.aeo-row input:checked ~ .aeo-st .aeo-st-fix { display: none; }
.aeo-row input:checked ~ .aeo-st .aeo-st-pass { display: inline; }

/* Authored slot per row, so the opening order is correct in the markup and
   needs no script. The driver overwrites --aeo-slot directly after that. */
.aeo-slot-0 { --aeo-slot: 0; }
.aeo-slot-1 { --aeo-slot: 1; }
.aeo-slot-2 { --aeo-slot: 2; }
.aeo-slot-3 { --aeo-slot: 3; }
.aeo-slot-4 { --aeo-slot: 4; }
.aeo-slot-5 { --aeo-slot: 5; }

.aeo-nm { flex: 1 1 auto; min-width: 0; font-size: 14px; font-weight: 600; color: var(--aisi-ink); }
/* The weight never changes with state; only the chip does. */
.aeo-wt { flex: 0 0 auto; font: 700 12px/1 'JetBrains Mono', ui-monospace, monospace; color: var(--aisi-ink-3); font-variant-numeric: tabular-nums; }
/* deliberately no :checked rule here -- see the note above .aeo-wt */

.aeo-hint { margin: 14px 0 0; font-size: 13.5px; color: var(--aisi-ink-3); }

/* Mobile calms down rather than shrinking: the hero narrative still plays, but
   the ambient loops stop, because a sweep running forever behind a scroll is
   battery spent on something nobody is looking at. */
@media (max-width: 620px) {
  .aeo-sweep { animation: none; }
  .aeo-scan { animation-duration: 6s; }
}

/* ================================================================== /solutions
   A switchboard, not a mechanism. Every other page in this family diagrams how
   something WORKS; this page's job is to route people, so the visual routes.

   WHY THE HERO IS MADE OF REAL LINKS
   The five destinations are <a> elements inside the SVG, pointing at the same
   five pages the cards below point at. That is what makes "hover or tap a path"
   possible without a line of JavaScript: hover, tap, focus and Enter all work
   because they are anchors, not because a script is listening. It also means
   the lit-route effect is a :hover rule rather than a state machine, and the
   keyboard gets it for free rather than as an afterthought.

   Each anchor carries a <title>, which is its accessible name. The visible
   labels are deliberately SHORT -- "Agencies", not "SEO platform for agencies"
   -- so that a translated label still fits the 168px card. That is the opposite
   call to the two diagrams on the AEO page, which are marked data-i18n-skip
   because their labels are hand-positioned in a measured column with no room to
   grow. Here there was room, so the map translates.
   -------------------------------------------------------------------------- */

/* ---- type ----------------------------------------------------------------- */

.sw-grp { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .1em; fill: var(--aisi-ink-3); }
.sw-name { font: 700 11.5px/1 'Inter Tight', Inter, system-ui, sans-serif; fill: var(--aisi-ink); }
.sw-hint { font: 500 9px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-3); }
.sw-hub-t { font: 800 12px/1 'Inter Tight', Inter, system-ui, sans-serif; fill: var(--aisi-ink); }
.sw-hub-s { font: 700 8px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .1em; fill: var(--aisi-ink-3); }

/* ---- the hub -------------------------------------------------------------- */

/* The exchange is the one thing here that is always live, so it is the one
   thing that never stops moving. Opacity only, and slow enough to read as a
   standing indicator rather than a blink. */
.sw-hub-halo { opacity: .14; animation: sw-halo 3.6s ease-in-out infinite; transform-box: view-box; transform-origin: center; }
@keyframes sw-halo { 0%, 100% { opacity: .10; } 50% { opacity: .24; } }

/* ---- routes --------------------------------------------------------------- */

.sw-route { cursor: pointer; transition: opacity .28s ease; }
.sw-line { stroke: var(--aisi-line); fill: none; stroke-width: 1.4; }

/* The visible route is a 2px stroke, which is a 2px hover target: on the live
   page the pointer slid between the curve and its card and lit nothing. This is
   the same curve at 18px and fully transparent, purely to catch the pointer, so
   "hover the path" means hovering anywhere near the path. pointer-events:stroke
   keeps the gap between routes from being claimed by either. */
.sw-hit { stroke: transparent; fill: none; stroke-width: 18; pointer-events: stroke; }

/* The lit overlay draws from the hub outwards, so the motion runs in the
   direction the reader travels. pathLength="1" keeps the timing independent of
   how long each curve actually is, which matters when five curves of different
   lengths have to light at the same rate. */
.sw-lit {
  stroke: var(--brand); fill: none; stroke-width: 2; stroke-linecap: round;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: sw-draw .62s cubic-bezier(.33, 1, .68, 1) both;
  transition: stroke-width .2s ease;
}
@keyframes sw-draw { to { stroke-dashoffset: 0; } }

/* A travelling token, done as a very short dash rather than a circle on a
   motion path. offset-path would mean repeating all five curves in the
   stylesheet, where they would rot the first time a designer nudged one; a dash
   follows whatever the path actually is. These keep running after the intro
   resolves, because a switchboard with no traffic on it is a diagram. */
.sw-tok {
  stroke: var(--brand); fill: none; stroke-width: 3.4; stroke-linecap: round;
  stroke-dasharray: .05 .95;
  animation: sw-tok 3.4s linear infinite;
}
@keyframes sw-tok { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }

/* ---- destination cards ---------------------------------------------------- */

.sw-card-f {
  fill: color-mix(in oklab, var(--panel) 88%, transparent);
  stroke: var(--aisi-line); stroke-width: 1;
  transition: stroke .22s ease, fill .22s ease;
}
.sw-card { animation: sw-prev .42s cubic-bezier(.22, 1, .36, 1) both; transform-box: view-box; }
@keyframes sw-prev { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: none; } }

/* The hint line is the "preview": it is what the card reveals when its route
   reaches it, so it lands a beat after the card itself. */
.sw-hint { animation: sw-fade .38s ease both; }
@keyframes sw-fade { from { opacity: 0; } to { opacity: 1; } }

.sw-chev { stroke: var(--aisi-ink-3); fill: none; stroke-width: 1.6; stroke-linecap: round; transition: stroke .22s ease, transform .22s ease; transform-box: view-box; }

/* ---- one route at a time -------------------------------------------------- */

/* Dimming the others is what makes the hovered route read end to end. :has is
   the whole mechanism, so a browser without it simply gets no dimming and every
   route stays legible -- the effect is additive, never load-bearing. */
.sw:has(.sw-route:hover) .sw-route:not(:hover),
.sw:has(.sw-route:focus-visible) .sw-route:not(:focus-visible) { opacity: .28; }

.sw-route:hover .sw-lit,
.sw-route:focus-visible .sw-lit { stroke-width: 3.2; }
.sw-route:hover .sw-card-f,
.sw-route:focus-visible .sw-card-f {
  stroke: color-mix(in oklab, var(--brand) 55%, transparent);
  fill: color-mix(in oklab, var(--brand) 7%, var(--panel));
}
.sw-route:hover .sw-chev,
.sw-route:focus-visible .sw-chev { stroke: var(--brand); transform: translateX(3px); }
.sw-route:focus-visible { outline: none; }
.sw-route:focus-visible .sw-card-f { stroke-width: 2; }

/* Per-route timing. The lit line and the card that receives it share a delay so
   the route reads as one movement; the token starts with them and then just
   keeps going. */
.sw-r1 .sw-lit { animation-delay: .45s; }  .sw-r1 .sw-card { animation-delay: .90s; }  .sw-r1 .sw-hint { animation-delay: 1.14s; } .sw-r1 .sw-tok { animation-delay: .45s; }
.sw-r2 .sw-lit { animation-delay: 1.30s; } .sw-r2 .sw-card { animation-delay: 1.75s; } .sw-r2 .sw-hint { animation-delay: 1.99s; } .sw-r2 .sw-tok { animation-delay: 1.30s; }
.sw-r3 .sw-lit { animation-delay: 2.15s; } .sw-r3 .sw-card { animation-delay: 2.60s; } .sw-r3 .sw-hint { animation-delay: 2.84s; } .sw-r3 .sw-tok { animation-delay: 2.15s; }
.sw-r4 .sw-lit { animation-delay: 3.00s; } .sw-r4 .sw-card { animation-delay: 3.45s; } .sw-r4 .sw-hint { animation-delay: 3.69s; } .sw-r4 .sw-tok { animation-delay: 3.00s; }
.sw-r5 .sw-lit { animation-delay: 3.85s; } .sw-r5 .sw-card { animation-delay: 4.30s; } .sw-r5 .sw-hint { animation-delay: 4.54s; } .sw-r5 .sw-tok { animation-delay: 3.85s; }

/* ---- two doors, not two features ------------------------------------------ */

/* The switching section is a pair of doors onto the same room. Each panel
   swings from its hinge on scaleX, which is a transform, so nothing reflows and
   the light behind it is simply uncovered rather than faded in. */
.sw-doors { max-width: 560px; margin-inline: auto; }

/* Double doors, not one sliding panel. The first attempt hinged a single leaf
   on its left edge, and once open it was a thin pale strip against the jamb:
   at rest -- which is how almost everyone sees it, since the animation is over
   before they scroll to it -- the drawing said nothing at all. Two leaves
   parting from the middle read as a doorway opening even frozen, and the
   squash is symmetric so neither side looks like a rendering fault. */
.sw-leaf {
  fill: color-mix(in oklab, var(--panel) 97%, transparent);
  stroke: var(--aisi-line); stroke-width: 1.2;
  transform-box: fill-box;
  transform: scaleX(1);
  transition: transform 1s cubic-bezier(.22, 1, .36, 1);
}
.sw-leaf-l { transform-origin: 0 50%; }
.sw-leaf-r { transform-origin: 100% 50%; }
.aisi-rise.in .sw-leaf { transform: scaleX(.12); }
.sw-d2 .sw-leaf { transition-delay: .16s; }

/* The caption never leaves. Which door was which is the entire point of the
   panel, and hanging it on the leaf meant it scaled away with the door. */
.sw-door-cap { font: 700 8.5px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .09em; fill: var(--aisi-ink-3); }

/* Both doorways lead to the same place, and the line saying so has to be
   visible at rest too -- the walker alone was gone a second after it arrived. */
.sw-conn { stroke: var(--aisi-line); fill: none; stroke-width: 1.4; }
.sw-flow {
  stroke: var(--brand); fill: none; stroke-width: 3.2; stroke-linecap: round;
  stroke-dasharray: .16 .84;
  animation: sw-tok 2.6s linear infinite;
}
.sw-d2 .sw-flow { animation-delay: 1.3s; }

.sw-room { fill: color-mix(in oklab, var(--brand) 9%, var(--panel)); stroke: color-mix(in oklab, var(--brand) 34%, transparent); stroke-width: 1.2; }
.sw-dest { fill: color-mix(in oklab, var(--panel) 92%, transparent); stroke: color-mix(in oklab, var(--brand) 40%, transparent); stroke-width: 1.3; }
.sw-dest-t { font: 800 12px/1 'Inter Tight', Inter, system-ui, sans-serif; fill: var(--aisi-ink); }



/* ---- fallbacks ------------------------------------------------------------ */

/* No JS means no IntersectionObserver, so .in never arrives and the doors would
   sit shut over their own punchline. Pin them open; the reader gets the end of
   the story rather than the setup. */
html.no-js .sw-leaf { transform: scaleX(.12); }

/* A token frozen mid-flight is an artefact, not information: with motion off
   the lit route already says everything the token was saying. */
@media (prefers-reduced-motion: reduce) {
  .sw-tok, .sw-flow { opacity: 0; }
  .sw-leaf { transform: scaleX(.12); }
}

/* Mobile keeps the map and drops the traffic: five tokens looping behind a
   scroll is battery spent on something nobody is looking at. */
@media (max-width: 620px) {
  .sw-tok, .sw-flow { animation: none; opacity: 0; }
  .sw-hub-halo { animation: none; opacity: .16; }
}

/* ====================================== /solutions/seo-tools-for-small-business
   The calm one. Every other page in this family is built for someone who reads
   dashboards on purpose; this one is for the person who runs the business and
   would rather not. So it is deliberately the least dense thing on the site:
   bigger type, fewer objects, more air, and green rather than brand purple for
   anything that means "done".

   WHY THE HERO IS MOSTLY HTML
   The others are one inline SVG because they diagram a mechanism. This one is a
   to-do list, and a to-do list made of <text> at 11px is a picture of a to-do
   list: unselectable, awkward to translate, and invisible to a screen reader
   without a paragraph of description. Real checkboxes and real text are calmer
   AND more capable -- they tick with the keyboard, they translate, and the rank
   line answers them through a sibling selector, so the whole interaction works
   with the script switched off. Only the chart is SVG, because a chart is the
   one part that genuinely is a drawing.
   -------------------------------------------------------------------------- */

.sb-card {
  position: relative;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-xl);
  background: color-mix(in oklab, var(--panel) 92%, transparent);
  padding: clamp(18px, 2.6vw, 26px);
}

/* ---- the triage line ------------------------------------------------------ */

.sb-top { display: flex; align-items: center; gap: 16px; }

/* Five numbers stacked in one place, each visible for its share of the count.
   The last is the only one that ends visible, so with motion off -- where every
   animation lands on its final frame -- what is left on screen is 3. */
.sb-num { position: relative; display: block; min-width: 78px; height: 40px; flex: none; }
.sb-num b {
  position: absolute; inset: 0; display: block;
  font: 800 34px/40px 'JetBrains Mono', ui-monospace, monospace;
  color: var(--aisi-ink); font-variant-numeric: tabular-nums;
  opacity: 0;
}
.sb-n1, .sb-n2, .sb-n3, .sb-n4 { animation: sb-off .1s linear both; }
@keyframes sb-off { from { opacity: 1; } to { opacity: 0; } }
.sb-n1 { animation-delay: .25s; animation-duration: .55s; }
.sb-n2 { animation-delay: .80s; animation-duration: .50s; }
.sb-n3 { animation-delay: 1.30s; animation-duration: .45s; }
.sb-n4 { animation-delay: 1.75s; animation-duration: .45s; }
.sb-n5 { animation: sb-on .5s cubic-bezier(.22, 1, .36, 1) 2.20s both; }
@keyframes sb-on { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }

.sb-lbl { position: relative; flex: 1 1 auto; min-width: 0; height: 34px; }
.sb-lbl span {
  position: absolute; left: 0; top: 0;
  font: 700 10px/34px 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: .1em; color: var(--aisi-ink-3); white-space: nowrap;
}
.sb-lbl-a { animation: sb-off .4s linear 1.85s both; }
.sb-lbl-b { opacity: 0; animation: sb-on .5s ease 2.20s both; }

/* The noise. It does not simply vanish: each tile drifts back toward the number
   as it goes, so the panel reads as many things being condensed into one
   answer rather than as data being thrown away. */
.sb-noise { display: flex; gap: 5px; flex: none; }
.sb-noise i {
  display: block; width: 15px; height: 26px;
  border-radius: var(--radius-xs);
  background: color-mix(in oklab, var(--text) 9%, transparent);
  animation: sb-collapse .55s cubic-bezier(.4, 0, .6, 1) both;
}
@keyframes sb-collapse {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateX(-16px) scaleY(.35); }
}
.sb-noise i:nth-child(1) { animation-delay: .30s; }
.sb-noise i:nth-child(2) { animation-delay: .42s; }
.sb-noise i:nth-child(3) { animation-delay: .54s; }
.sb-noise i:nth-child(4) { animation-delay: .66s; }
.sb-noise i:nth-child(5) { animation-delay: .78s; }
.sb-noise i:nth-child(6) { animation-delay: .90s; }
.sb-noise i:nth-child(7) { animation-delay: 1.02s; }
.sb-noise i:nth-child(8) { animation-delay: 1.14s; }

.sb-rule { height: 1px; background: var(--aisi-line); margin: 20px 0 4px; }

/* ---- the three things ----------------------------------------------------- */

/* The inputs sit before the list and the chart so both can answer them with a
   plain sibling selector. No :has, so this works everywhere, and no script, so
   it works before -- and without -- js/mv-aisi.js. */
.sb-cb { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }

.sb-todos { list-style: none; margin: 0; padding: 0; }
.sb-todos li { animation: sb-in .5s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes sb-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.sb-todos li:nth-child(1) { animation-delay: 2.35s; }
.sb-todos li:nth-child(2) { animation-delay: 2.50s; }
.sb-todos li:nth-child(3) { animation-delay: 2.65s; }

.sb-todos label {
  display: flex; align-items: center; gap: 13px;
  padding: 12px 4px; cursor: pointer;
  border-radius: var(--radius-sm);
  transition: background .2s ease;
}
.sb-todos label:hover { background: color-mix(in oklab, var(--text) 4%, transparent); }

.sb-box {
  flex: none; width: 21px; height: 21px;
  border: 1.8px solid color-mix(in oklab, var(--text) 26%, transparent);
  border-radius: var(--radius-xs);
  display: grid; place-items: center;
  transition: border-color .25s ease, background .25s ease;
}
.sb-box svg { width: 13px; height: 13px; display: block; }
.sb-box path {
  stroke: var(--on-brand); fill: none; stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  transition: stroke-dashoffset .34s cubic-bezier(.22, 1, .36, 1);
}

.sb-txt { flex: 1 1 auto; min-width: 0; font-size: 14.5px; color: var(--aisi-ink); transition: color .25s ease; }
.sb-fix {
  flex: none; font: 700 11px/1 Inter, system-ui, sans-serif;
  color: var(--brand);
  padding: 6px 11px; border-radius: var(--radius-pill);
  background: color-mix(in oklab, var(--brand) 10%, transparent);
  transition: opacity .25s ease;
}

/* Checked: the box fills green, the tick draws itself, the prompt to fix it is
   no longer needed, and the line is quietly struck through. */
.sb-t1:checked ~ .sb-todos li:nth-child(1) .sb-box,
.sb-t2:checked ~ .sb-todos li:nth-child(2) .sb-box,
.sb-t3:checked ~ .sb-todos li:nth-child(3) .sb-box {
  border-color: var(--green); background: var(--green);
}
.sb-t1:checked ~ .sb-todos li:nth-child(1) .sb-box path,
.sb-t2:checked ~ .sb-todos li:nth-child(2) .sb-box path,
.sb-t3:checked ~ .sb-todos li:nth-child(3) .sb-box path { stroke-dashoffset: 0; }
.sb-t1:checked ~ .sb-todos li:nth-child(1) .sb-txt,
.sb-t2:checked ~ .sb-todos li:nth-child(2) .sb-txt,
.sb-t3:checked ~ .sb-todos li:nth-child(3) .sb-txt { color: var(--aisi-ink-3); }
.sb-t1:checked ~ .sb-todos li:nth-child(1) .sb-fix,
.sb-t2:checked ~ .sb-todos li:nth-child(2) .sb-fix,
.sb-t3:checked ~ .sb-todos li:nth-child(3) .sb-fix { opacity: 0; }

.sb-t1:focus-visible ~ .sb-todos li:nth-child(1) .sb-box,
.sb-t2:focus-visible ~ .sb-todos li:nth-child(2) .sb-box,
.sb-t3:focus-visible ~ .sb-todos li:nth-child(3) .sb-box { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---- the one line that goes up -------------------------------------------- */

.sb-chart { margin-top: 6px; }
.sb-chart svg { display: block; width: 100%; height: auto; }
.sb-chart-h { margin: 20px 0 10px; font: 700 10px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .1em; color: var(--aisi-ink-3); }
.sb-base { stroke: var(--aisi-line); fill: none; stroke-width: 1; stroke-dasharray: 3 4; }
.sb-ghost { stroke: color-mix(in oklab, var(--text) 13%, transparent); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 4 5; }
.sb-flat { stroke: color-mix(in oklab, var(--text) 22%, transparent); fill: none; stroke-width: 2.4; stroke-linecap: round; }

/* Each step is drawn, not faded: the line grows in the direction it is going,
   which is the whole point of the picture. */
.sb-step {
  stroke: var(--green); fill: none; stroke-width: 2.6;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  transition: stroke-dashoffset .75s cubic-bezier(.33, 1, .68, 1);
}
.sb-dot0 { fill: color-mix(in oklab, var(--text) 26%, transparent); }
.sb-dot { fill: var(--green); opacity: 0; transition: opacity .3s ease .45s; }
.sb-t1:checked ~ .sb-chart .sb-s1, .sb-t2:checked ~ .sb-chart .sb-s2, .sb-t3:checked ~ .sb-chart .sb-s3 { stroke-dashoffset: 0; }
.sb-t1:checked ~ .sb-chart .sb-d1, .sb-t2:checked ~ .sb-chart .sb-d2, .sb-t3:checked ~ .sb-chart .sb-d3 { opacity: 1; }

.sb-hint { margin: 14px 0 0; font-size: 13px; color: var(--aisi-ink-3); }

/* ---- fallbacks ------------------------------------------------------------ */

/* With no JS the intro still runs, because all of it is CSS. Nothing to pin. */

@media (prefers-reduced-motion: reduce) {
  /* The count has no resting value of its own -- opacity lives in the keyframes
     -- so the intermediates need one, or they all stack on top of each other. */
  .sb-num b { opacity: 0; }
  .sb-n5 { opacity: 1; }
  .sb-lbl-a { opacity: 0; }
  .sb-lbl-b { opacity: 1; }
  .sb-noise i { opacity: 0; }
  .sb-todos li { opacity: 1; }
}

@media (max-width: 620px) {
  .sb-noise { display: none; }
  .sb-txt { font-size: 13.5px; }
}

/* ========================================= /solutions/seo-platform-for-agencies
   Many clients, kept apart, under one control layer with your name on it.

   The other heroes in this family diagram ONE mechanism. This page's subject is
   not a mechanism, it is multiplicity: eight workspaces updating side by side
   and staying visibly un-merged, one switch that restamps every one of them
   with the agency's brand instead of ours, and a report that re-points at
   whichever client you pick.

   All of it runs on real inputs -- a radio group for the active client, one
   checkbox for white-label -- so switching and swapping work from the keyboard
   and with the script switched off. There is no state machine here; there is a
   :checked selector, which is the same thing without the failure modes.

   Colour is deliberately NOT used to tell the clients apart. Twelve accent
   hues would mean twelve literals, the constitution allows none, and separation
   is already carried by the thing that actually separates them: eight discrete
   panels with their own borders and their own data.
   -------------------------------------------------------------------------- */

.ag-card {
  position: relative;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-xl);
  background: color-mix(in oklab, var(--panel) 90%, transparent);
  padding: clamp(16px, 2.4vw, 22px);
  /* The report's bar heights come from the active client. Defaults are
     client 01, so the panel is correct before any input is read. */
  --b1: .45; --b2: .62; --b3: .78; --b4: .95;
}

.ag-in { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }

/* ---- header + the white-label switch -------------------------------------- */

.ag-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.ag-h-t { font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .11em; color: var(--aisi-ink-3); }

.ag-switch {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 12px; font-weight: 600; color: var(--aisi-ink-2);
  padding: 4px 6px; border-radius: var(--radius-pill);
}
.ag-track {
  width: 32px; height: 18px; border-radius: var(--radius-pill);
  background: color-mix(in oklab, var(--text) 16%, transparent);
  display: block; position: relative;
  transition: background .3s ease;
}
.ag-knob {
  position: absolute; top: 3px; left: 3px; width: 12px; height: 12px;
  border-radius: var(--radius-pill); background: var(--panel);
  transition: transform .3s cubic-bezier(.22, 1, .36, 1);
}
.ag-wl:checked ~ .ag-head .ag-track { background: var(--cyan); }
.ag-wl:checked ~ .ag-head .ag-knob { transform: translateX(14px); }
.ag-wl:focus-visible ~ .ag-head .ag-track { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---- the wall ------------------------------------------------------------- */

.ag-wall { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
@media (max-width: 560px) { .ag-wall { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.ag-tile {
  display: block; cursor: pointer;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  padding: 9px 9px 8px;
  transition: border-color .22s ease, transform .22s cubic-bezier(.22, 1, .36, 1);
  animation: ag-land .5s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes ag-land { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
.ag-tile:hover { border-color: color-mix(in oklab, var(--brand) 40%, transparent); }

/* They arrive together but not in step: a wall of accounts refreshing in
   parallel, which is the claim, rather than a queue being processed. */
.ag-tile:nth-child(1) { animation-delay: .30s; }
.ag-tile:nth-child(2) { animation-delay: .38s; }
.ag-tile:nth-child(3) { animation-delay: .34s; }
.ag-tile:nth-child(4) { animation-delay: .44s; }
.ag-tile:nth-child(5) { animation-delay: .40s; }
.ag-tile:nth-child(6) { animation-delay: .50s; }
.ag-tile:nth-child(7) { animation-delay: .46s; }
.ag-tile:nth-child(8) { animation-delay: .56s; }

.ag-t-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 7px; }
.ag-mark {
  width: 11px; height: 11px; border-radius: var(--radius-xs);
  background: var(--brand); display: block;
  transition: background .45s ease, transform .45s cubic-bezier(.22, 1, .36, 1);
}
.ag-dot { width: 5px; height: 5px; border-radius: var(--radius-pill); background: var(--green); display: block; }
.ag-dot--w { background: var(--amber); }
.ag-id { font: 700 8px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .06em; color: var(--aisi-ink-3); display: block; margin-bottom: 7px; }

.ag-spark { display: flex; align-items: flex-end; gap: 3px; height: 20px; }
.ag-spark i {
  flex: 1 1 0; display: block; height: 100%;
  border-radius: var(--radius-xs);
  background: color-mix(in oklab, var(--text) 15%, transparent);
  transform-origin: bottom;
  animation: ag-grow .55s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes ag-grow { from { transform: scaleY(.06); } to { transform: scaleY(var(--h, .6)); } }
.ag-b3 { --h: .30; } .ag-b4 { --h: .42; } .ag-b5 { --h: .54; }
.ag-b6 { --h: .66; } .ag-b7 { --h: .78; } .ag-b8 { --h: .88; } .ag-b9 { --h: .97; }
/* The bars fill just behind their tile, so a workspace lands and then reports. */
.ag-spark i:nth-child(1) { animation-delay: .50s; }
.ag-spark i:nth-child(2) { animation-delay: .56s; }
.ag-spark i:nth-child(3) { animation-delay: .62s; }
.ag-spark i:nth-child(4) { animation-delay: .68s; }

/* Selected: the tile the report is pointed at. */
.ag-c1:checked ~ .ag-wall .ag-tile:nth-child(1),
.ag-c2:checked ~ .ag-wall .ag-tile:nth-child(2),
.ag-c3:checked ~ .ag-wall .ag-tile:nth-child(3),
.ag-c4:checked ~ .ag-wall .ag-tile:nth-child(4),
.ag-c5:checked ~ .ag-wall .ag-tile:nth-child(5),
.ag-c6:checked ~ .ag-wall .ag-tile:nth-child(6),
.ag-c7:checked ~ .ag-wall .ag-tile:nth-child(7),
.ag-c8:checked ~ .ag-wall .ag-tile:nth-child(8) {
  border-color: var(--brand);
  background: color-mix(in oklab, var(--brand) 7%, var(--panel));
  transform: translateY(-2px);
}
.ag-c1:focus-visible ~ .ag-wall .ag-tile:nth-child(1),
.ag-c2:focus-visible ~ .ag-wall .ag-tile:nth-child(2),
.ag-c3:focus-visible ~ .ag-wall .ag-tile:nth-child(3),
.ag-c4:focus-visible ~ .ag-wall .ag-tile:nth-child(4),
.ag-c5:focus-visible ~ .ag-wall .ag-tile:nth-child(5),
.ag-c6:focus-visible ~ .ag-wall .ag-tile:nth-child(6),
.ag-c7:focus-visible ~ .ag-wall .ag-tile:nth-child(7),
.ag-c8:focus-visible ~ .ag-wall .ag-tile:nth-child(8) { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---- the brand swap ------------------------------------------------------- */

/* One switch, every tile at once. That simultaneity IS the feature: white-label
   is worthless if it has to be applied per client. */
.ag-wl:checked ~ .ag-wall .ag-mark,
.ag-wl:checked ~ .ag-report .ag-brand-m { background: var(--cyan); transform: rotate(45deg); }

.ag-brand-n { position: relative; display: inline-block; min-width: 84px; height: 13px; vertical-align: middle; }
.ag-brand-n span {
  position: absolute; left: 0; top: 0; white-space: nowrap;
  font: 700 11px/13px 'Inter Tight', Inter, system-ui, sans-serif;
  color: var(--aisi-ink-2); transition: opacity .35s ease;
}
.ag-brand-b { opacity: 0; }
.ag-wl:checked ~ .ag-report .ag-brand-a { opacity: 0; }
.ag-wl:checked ~ .ag-report .ag-brand-b { opacity: 1; color: var(--cyan); }

/* ---- the report ----------------------------------------------------------- */

.ag-report {
  margin-top: 12px; padding: 13px 14px 14px;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  animation: ag-land .55s cubic-bezier(.22, 1, .36, 1) .78s both;
}
.ag-rhead { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.ag-brand-m { width: 13px; height: 13px; border-radius: var(--radius-xs); background: var(--brand); flex: none; transition: background .45s ease, transform .45s cubic-bezier(.22, 1, .36, 1); }
.ag-rclient { margin-left: auto; font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .09em; color: var(--aisi-ink-3); }

/* Which client the report is aimed at. Eight numbers in one slot, one shown. */
.ag-num { position: relative; display: inline-block; width: 18px; height: 11px; }
.ag-num b { position: absolute; right: 0; top: 0; font: 800 10px/11px 'JetBrains Mono', ui-monospace, monospace; color: var(--brand); opacity: 0; transition: opacity .25s ease; }
.ag-c1:checked ~ .ag-report .ag-num b:nth-child(1),
.ag-c2:checked ~ .ag-report .ag-num b:nth-child(2),
.ag-c3:checked ~ .ag-report .ag-num b:nth-child(3),
.ag-c4:checked ~ .ag-report .ag-num b:nth-child(4),
.ag-c5:checked ~ .ag-report .ag-num b:nth-child(5),
.ag-c6:checked ~ .ag-report .ag-num b:nth-child(6),
.ag-c7:checked ~ .ag-report .ag-num b:nth-child(7),
.ag-c8:checked ~ .ag-report .ag-num b:nth-child(8) { opacity: 1; }

.ag-row { display: flex; align-items: center; gap: 12px; padding: 7px 0; }
.ag-rl { flex: 0 0 92px; font-size: 11.5px; color: var(--aisi-ink-2); }
.ag-bars { flex: 1 1 auto; display: flex; align-items: flex-end; gap: 3px; height: 32px; }
.ag-bars i {
  flex: 1 1 0; display: block; height: 100%;
  border-radius: var(--radius-xs); transform-origin: bottom;
  transition: transform .55s cubic-bezier(.22, 1, .36, 1);
}
.ag-row--g .ag-bars i { background: color-mix(in oklab, var(--brand) 60%, transparent); }
/* The second surface. It is a fraction of the first on purpose: an agency's AI
   numbers are lower than its Google numbers today, and drawing them level would
   be the flattering lie this page exists to avoid. */
.ag-row--a .ag-bars i { background: color-mix(in oklab, var(--cyan) 62%, transparent); }

.ag-bars i:nth-child(1)  { --x: calc(var(--b1) * .85); }
.ag-bars i:nth-child(2)  { --x: var(--b1); }
.ag-bars i:nth-child(3)  { --x: calc(var(--b1) * 1.08); }
.ag-bars i:nth-child(4)  { --x: calc(var(--b2) * .90); }
.ag-bars i:nth-child(5)  { --x: var(--b2); }
.ag-bars i:nth-child(6)  { --x: calc(var(--b2) * 1.06); }
.ag-bars i:nth-child(7)  { --x: calc(var(--b3) * .88); }
.ag-bars i:nth-child(8)  { --x: var(--b3); }
.ag-bars i:nth-child(9)  { --x: calc(var(--b3) * 1.05); }
.ag-bars i:nth-child(10) { --x: calc(var(--b4) * .90); }
.ag-bars i:nth-child(11) { --x: var(--b4); }
.ag-bars i:nth-child(12) { --x: calc(var(--b4) * 1.02); }
.ag-bars i { transform: scaleY(var(--x, .6)); }
/* The AI row sits below the Google row on purpose. An agency's AI numbers are
   lower than its Google numbers today, and drawing them level would be the
   flattering lie this page exists to avoid. */
.ag-row--a .ag-bars i { transform: scaleY(calc(var(--x, .6) * .68)); }

/* The AI row arrives after the Google row, because that is the argument: the
   number you have always reported, and then the one nobody was reporting. */
.ag-row--a { animation: ag-land .5s cubic-bezier(.22, 1, .36, 1) 1.28s both; }

.ag-sched { display: flex; align-items: center; gap: 7px; margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--aisi-line); font: 600 10.5px/1 Inter, system-ui, sans-serif; color: var(--aisi-ink-3); }
.ag-sched-d { width: 5px; height: 5px; border-radius: var(--radius-pill); background: var(--green); flex: none; }

/* Per-client shapes. Eight sets of four, which is what makes switching a client
   change the report rather than just relabel it. */
.ag-c1:checked ~ .ag-report { --b1: .45; --b2: .62; --b3: .78; --b4: .95; }
.ag-c2:checked ~ .ag-report { --b1: .70; --b2: .55; --b3: .62; --b4: .80; }
.ag-c3:checked ~ .ag-report { --b1: .38; --b2: .50; --b3: .44; --b4: .58; }
.ag-c4:checked ~ .ag-report { --b1: .82; --b2: .74; --b3: .88; --b4: .96; }
.ag-c5:checked ~ .ag-report { --b1: .55; --b2: .68; --b3: .60; --b4: .72; }
.ag-c6:checked ~ .ag-report { --b1: .92; --b2: .80; --b3: .66; --b4: .52; }
.ag-c7:checked ~ .ag-report { --b1: .48; --b2: .58; --b3: .72; --b4: .84; }
.ag-c8:checked ~ .ag-report { --b1: .62; --b2: .70; --b3: .66; --b4: .78; }

.ag-hint { margin: 13px 0 0; font-size: 12.5px; color: var(--aisi-ink-3); }

/* ---- fallbacks ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  /* Every animated property that has no resting value of its own needs one, or
     stopping the animation reverts it to the element's base style. Learned the
     hard way twice on this site: a glow that went opaque and a counter that
     stacked five numbers on top of each other. */
  .ag-tile, .ag-report, .ag-row--a { opacity: 1; }
  .ag-spark i { transform: scaleY(var(--h, .6)); }
}

@media (max-width: 620px) {
  .ag-rl { flex-basis: 74px; font-size: 11px; }
}

/* ============================================== /solutions/ahrefs-alternative
   This page's hero was byte-for-byte the same SVG as the Semrush page's -- same
   2,464 bytes, same md5 -- so two different arguments were being made with one
   picture. This one is now the link graph and the migration: many referring
   domains funnelling into one site, that work carried across into a single
   workspace, and the AI lane underneath it.

   WHAT THE TOGGLE ACTUALLY CONTRASTS
   Not "Ahrefs has no AI visibility". It does -- Brand Radar -- and the page's
   own dated comparison says so two screens further down. Claiming otherwise in
   the hero would be the diagram contradicting the table, which is the fastest
   way to lose an argument you were winning. What the toggle contrasts is the
   PRICE OF THE LANE: $129 a month there, $0 here. That is the real difference,
   it is what the comparison row says, and it is a better line anyway.

   The balance at the bottom exists for the same reason. Ahrefs wins on link
   index depth, the page says so in full paragraphs, and a hero that pretended
   otherwise would be selling past the copy.
   -------------------------------------------------------------------------- */

.ah-card { position: relative; }
.ah-in { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }

/* ---- the before / after control ------------------------------------------- */

.ah-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.ah-h-t { font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .11em; color: var(--aisi-ink-3); }

.ah-seg {
  display: inline-flex; align-items: center; cursor: pointer;
  padding: 3px; gap: 2px;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-pill);
  background: color-mix(in oklab, var(--text) 4%, transparent);
}
.ah-seg-o {
  font: 700 11px/1 Inter, system-ui, sans-serif;
  padding: 6px 12px; border-radius: var(--radius-pill);
  color: var(--aisi-ink-3);
  transition: background .28s ease, color .28s ease;
}
.ah-seg-o:nth-of-type(1) { background: var(--panel); color: var(--aisi-ink); }
.ah-in:checked ~ .ah-head .ah-seg-o:nth-of-type(1) { background: transparent; color: var(--aisi-ink-3); }
.ah-in:checked ~ .ah-head .ah-seg-o:nth-of-type(2) { background: var(--brand); color: var(--on-brand); }
.ah-in:focus-visible ~ .ah-head .ah-seg { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---- the link graph ------------------------------------------------------- */

.ah-mono { font: 700 8px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .1em; fill: var(--aisi-ink-3); }
.ah-lbl { font: 600 10.5px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-2); }
.ah-sub { font: 500 9px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-3); }

.ah-node { fill: color-mix(in oklab, var(--text) 42%, transparent); animation: ah-pop .45s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes ah-pop { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: none; } }
.ah-node { transform-box: fill-box; transform-origin: center; }
.ah-n1 { animation-delay: .22s; } .ah-n2 { animation-delay: .29s; } .ah-n3 { animation-delay: .36s; }
.ah-n4 { animation-delay: .43s; } .ah-n5 { animation-delay: .50s; } .ah-n6 { animation-delay: .57s; }

.ah-hub { fill: var(--brand); animation: ah-pop .5s cubic-bezier(.22, 1, .36, 1) .62s both; transform-box: fill-box; transform-origin: center; }
.ah-edge { stroke: color-mix(in oklab, var(--text) 20%, transparent); fill: none; stroke-width: 1.2; stroke-dasharray: 1; stroke-dashoffset: 1; animation: ah-draw .5s ease both; }
@keyframes ah-draw { to { stroke-dashoffset: 0; } }
.ah-e1 { animation-delay: .30s; } .ah-e2 { animation-delay: .37s; } .ah-e3 { animation-delay: .44s; }
.ah-e4 { animation-delay: .51s; } .ah-e5 { animation-delay: .58s; } .ah-e6 { animation-delay: .65s; }

/* Links arriving, then the same work moving across. A short dash travelling the
   path rather than a dot on a motion path, so the curve stays the single source
   of its own geometry. */
.ah-tok {
  stroke: var(--brand); fill: none; stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: .07 .93;
  animation: ah-tok 2.8s linear infinite;
}
@keyframes ah-tok { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
.ah-t1 { animation-delay: .9s; } .ah-t2 { animation-delay: 1.4s; } .ah-t3 { animation-delay: 1.9s; }

/* ---- the workspace -------------------------------------------------------- */

.ah-panel { fill: color-mix(in oklab, var(--panel) 90%, transparent); stroke: var(--aisi-line); stroke-width: 1; }
.ah-row-bg { fill: var(--panel); stroke: var(--aisi-line); stroke-width: 1; }
.ah-row { animation: ah-slide .5s cubic-bezier(.22, 1, .36, 1) both; transform-box: view-box; }
@keyframes ah-slide { from { opacity: 0; transform: translateX(10px); } to { opacity: 1; transform: none; } }
.ah-r1 { animation-delay: 1.00s; } .ah-r2 { animation-delay: 1.16s; }
.ah-r3 { animation-delay: 1.32s; } .ah-r4 { animation-delay: 1.48s; }
.ah-mark { fill: color-mix(in oklab, var(--brand) 55%, transparent); }
.ah-tick { stroke: var(--green); fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ---- the AI lane ---------------------------------------------------------- */

.ah-lane { animation: ah-rise .55s cubic-bezier(.22, 1, .36, 1) 1.75s both; transform-box: view-box; }
@keyframes ah-rise { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }
.ah-lane-bg { fill: color-mix(in oklab, var(--brand) 6%, var(--panel)); stroke: color-mix(in oklab, var(--brand) 32%, transparent); stroke-width: 1.2; transition: fill .35s ease, stroke .35s ease; }
.ah-eng { fill: var(--panel); stroke: var(--aisi-line); stroke-width: 1; transition: opacity .35s ease; }
.ah-eng-t { font: 600 10px/1 Inter, system-ui, sans-serif; fill: var(--aisi-ink-2); transition: opacity .35s ease; }

/* Unchecked is the "Ahrefs" side: the lane exists there too, it just costs
   $129 a month to open. Dimmed, not deleted. */
.ah-lane-g { transition: opacity .35s ease; }
.ah-price-a, .ah-price-b { font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .05em; transition: opacity .3s ease; }
.ah-price-a { fill: var(--aisi-ink-2); }
.ah-price-b { fill: var(--green); opacity: 0; }
.ah-in:checked ~ .ah-svg .ah-price-a { opacity: 0; }
.ah-in:checked ~ .ah-svg .ah-price-b { opacity: 1; }
.ah-svg .ah-lane-g { opacity: .42; }
.ah-in:checked ~ .ah-svg .ah-lane-g { opacity: 1; }

/* ---- the honest balance --------------------------------------------------- */

/* Ahrefs is heavier on link index depth and the beam says so. The page argues
   this in prose; a hero that tipped the other way would be selling past it. */
.ah-beam { transform-box: view-box; transform-origin: 220px 298px; animation: ah-tip .9s cubic-bezier(.34, 1.3, .64, 1) 2.35s both; }
@keyframes ah-tip { from { transform: rotate(0deg); } to { transform: rotate(-4.5deg); } }
.ah-beam-r { fill: color-mix(in oklab, var(--text) 34%, transparent); }
.ah-pan { fill: color-mix(in oklab, var(--text) 34%, transparent); }
.ah-pan--mv { fill: var(--brand); }
.ah-fulcrum { fill: color-mix(in oklab, var(--text) 22%, transparent); }

/* ---- migration checklist: what each step moves ---------------------------- */

/* The list is already ticked prose; what it lacked was any sense of WHICH thing
   each step carries across. The note is present in the markup and revealed on
   hover or keyboard focus, so it costs nothing to a reader who never points at
   it and is reachable by one who cannot. */
.aisi-checks li .ah-moves {
  display: block; margin-top: 4px;
  font: 700 9.5px/1.5 'JetBrains Mono', ui-monospace, monospace;
  letter-spacing: .05em; color: var(--brand);
  opacity: 0; transform: translateY(-2px);
  transition: opacity .25s ease, transform .25s ease;
}
.aisi-checks li:hover .ah-moves,
.aisi-checks li:focus-within .ah-moves { opacity: 1; transform: none; }

/* ---- fallbacks ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  /* Resting values for everything the keyframes were the only source of. */
  .ah-node, .ah-hub, .ah-row, .ah-lane { opacity: 1; }
  .ah-edge { stroke-dashoffset: 0; }
  .ah-tok { opacity: 0; }
  .ah-beam { transform: rotate(-4.5deg); }
  .ah-moves { opacity: 1; transform: none; }
}

@media (max-width: 620px) {
  .ah-tok { animation: none; opacity: 0; }
  .ah-moves { opacity: 1; transform: none; }
}

/* ============================================= /solutions/semrush-alternative
   This page and the Ahrefs page shipped the same hero SVG, and the comment
   above this one still described Ahrefs. Two competitors, one drawing, and the
   wrong label on it.

   Its own argument is shape, not quality: the AI Visibility Toolkit is a
   separate box you bolt on at $99 a month PER DOMAIN, so the cost multiplies
   with every client site, while the same capability here is already inside the
   plan. So the signature is a tower that grows as the reader adds domains
   against a flat block that does not.

   WHAT THE NUMBERS ARE, AND WHAT THEY ARE NOT
   $139.95 Pro and the $99 per-domain add-on are Semrush's own published prices
   as quoted and dated in this page's body (verified 7 September 2026); the
   totals are those two numbers added up, nothing modelled. The Metric Vault
   side reads $0 and its caption scopes it: included on every plan. It is the
   ADD-ON that costs nothing at any domain count, which is exactly what the
   comparison table says. Printing a bare $0 as a total would be claiming the
   whole subscription is free at six client domains, which the page never says
   and which the Agency plan contradicts.

   The control is a radio group, not a range input. It looks like a stepped
   slider and arrow keys move it the same way, but a radio group is readable by
   CSS -- so the tower stacks, the totals change and the track fills with no
   JavaScript at all.
   -------------------------------------------------------------------------- */

.sm-card {
  position: relative;
  border: 1px solid var(--aisi-line);
  border-radius: var(--radius-xl);
  background: color-mix(in oklab, var(--panel) 90%, transparent);
  padding: clamp(16px, 2.4vw, 22px);
}
.sm-in { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }

/* ---- the consolidation strip ---------------------------------------------- */

.sm-mono { font: 700 8px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .1em; fill: var(--aisi-ink-3); }
.sm-tile-t { font: 700 7.5px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .05em; fill: var(--aisi-ink-2); }
.sm-panel { fill: color-mix(in oklab, var(--panel) 94%, transparent); stroke: var(--aisi-line); stroke-width: 1; }
.sm-tile-bg { fill: var(--panel); stroke: var(--aisi-line); stroke-width: 1; }

/* Scattered tools arriving in one place. One keyframe, six offsets: the tile
   carries where it came from, the animation only says "go home". */
.sm-tile {
  transform-box: view-box;
  animation: sm-gather .72s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes sm-gather {
  from { opacity: .25; transform: translate(var(--dx, 0), var(--dy, 0)); }
  to { opacity: 1; transform: none; }
}
.sm-g1 { --dx: -96px; --dy: -18px; animation-delay: .30s; }
.sm-g2 { --dx: -40px; --dy: -34px; animation-delay: .38s; }
.sm-g3 { --dx: 58px;  --dy: -30px; animation-delay: .46s; }
.sm-g4 { --dx: -88px; --dy: 26px;  animation-delay: .54s; }
.sm-g5 { --dx: 14px;  --dy: 38px;  animation-delay: .62s; }
.sm-g6 { --dx: 94px;  --dy: 20px;  animation-delay: .70s; }

/* The AI panel is already docked when the tools land. That is the whole claim:
   not added afterwards, not sold separately. */
.sm-ai { fill: color-mix(in oklab, var(--brand) 12%, var(--panel)); stroke: color-mix(in oklab, var(--brand) 38%, transparent); stroke-width: 1.2; }
.sm-ai-t { font: 700 8px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .09em; fill: var(--brand); }
.sm-ai-g { animation: sm-dock .55s cubic-bezier(.22, 1, .36, 1) .92s both; transform-box: view-box; }
@keyframes sm-dock { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }

.sm-rule { height: 1px; background: var(--aisi-line); margin: 16px 0 14px; }

/* ---- the domain control --------------------------------------------------- */

.sm-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.sm-h-t { font: 700 9px/1 'JetBrains Mono', ui-monospace, monospace; letter-spacing: .11em; color: var(--aisi-ink-3); }
.sm-dom { font-size: 12px; font-weight: 600; color: var(--aisi-ink-2); }
.sm-n { position: relative; display: inline-block; width: 12px; height: 14px; vertical-align: -2px; }
.sm-n b { position: absolute; left: 0; top: 0; font: 800 13px/14px 'JetBrains Mono', ui-monospace, monospace; color: var(--brand); opacity: 0; transition: opacity .2s ease; }

.sm-track { position: relative; display: flex; gap: 0; margin-bottom: 16px; }
.sm-rail { position: absolute; left: 6%; right: 6%; top: 13px; height: 3px; border-radius: var(--radius-pill); background: color-mix(in oklab, var(--text) 12%, transparent); }
.sm-fill {
  position: absolute; left: 6%; right: 6%; top: 13px; height: 3px;
  border-radius: var(--radius-pill); background: var(--brand);
  transform-origin: left center; transform: scaleX(0);
  transition: transform .4s cubic-bezier(.22, 1, .36, 1);
}
.sm-stop {
  position: relative; flex: 1 1 0; text-align: center; cursor: pointer;
  padding-top: 22px;
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
  color: var(--aisi-ink-3);
  transition: color .25s ease;
}
.sm-stop::before {
  content: ''; position: absolute; top: 8px; left: 50%; width: 13px; height: 13px;
  margin-left: -6.5px; border-radius: var(--radius-pill);
  background: var(--panel); border: 2px solid color-mix(in oklab, var(--text) 20%, transparent);
  transition: border-color .25s ease, transform .25s cubic-bezier(.22, 1, .36, 1);
}

/* ---- the tower ------------------------------------------------------------ */

.sm-cols { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(12px, 2.4vw, 22px); align-items: end; }

/* Fixed height, boxes placed from the bottom. The tower grows by revealing
   boxes that are already positioned, so nothing below it ever moves and the
   browser does no layout at 60fps. */
.sm-stack { position: relative; height: 196px; }
.sm-box, .sm-base, .sm-flat {
  position: absolute; left: 0; right: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-sm);
  font: 700 11px/1 'JetBrains Mono', ui-monospace, monospace;
}
.sm-base {
  bottom: 0; height: 28px;
  background: color-mix(in oklab, var(--text) 8%, transparent);
  border: 1px solid var(--aisi-line); color: var(--aisi-ink-3);
}
.sm-box {
  height: 26px;
  background: transparent;
  border: 1px dashed color-mix(in oklab, var(--text) 20%, transparent);
  color: color-mix(in oklab, var(--text) 34%, transparent);
  transform-origin: bottom; transform: scaleY(.82);
  transition: background .3s ease, border-color .3s ease, color .3s ease,
              transform .35s cubic-bezier(.22, 1, .36, 1);
}
.sm-k1 { bottom: 30px; } .sm-k2 { bottom: 58px; } .sm-k3 { bottom: 86px; }
.sm-k4 { bottom: 114px; } .sm-k5 { bottom: 142px; } .sm-k6 { bottom: 170px; }

.sm-flat {
  bottom: 0; height: 28px;
  background: color-mix(in oklab, var(--green) 13%, transparent);
  border: 1px solid color-mix(in oklab, var(--green) 34%, transparent);
  color: var(--green);
}

.sm-total { margin-top: 12px; position: relative; height: 30px; }
.sm-total b {
  position: absolute; left: 0; top: 0;
  font: 800 24px/30px 'JetBrains Mono', ui-monospace, monospace;
  color: var(--aisi-ink); opacity: 0; transition: opacity .25s ease;
  font-variant-numeric: tabular-nums;
}
.sm-total--mv b { color: var(--green); opacity: 1; }
.sm-sub { margin: 4px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--aisi-ink-3); }

/* ---- what each stop selects ----------------------------------------------- */

.sm-d1:checked ~ .sm-track .sm-fill { transform: scaleX(0); }
.sm-d2:checked ~ .sm-track .sm-fill { transform: scaleX(.2); }
.sm-d3:checked ~ .sm-track .sm-fill { transform: scaleX(.4); }
.sm-d4:checked ~ .sm-track .sm-fill { transform: scaleX(.6); }
.sm-d5:checked ~ .sm-track .sm-fill { transform: scaleX(.8); }
.sm-d6:checked ~ .sm-track .sm-fill { transform: scaleX(1); }

.sm-d1:checked ~ .sm-track .sm-stop:nth-of-type(1),
.sm-d2:checked ~ .sm-track .sm-stop:nth-of-type(2),
.sm-d3:checked ~ .sm-track .sm-stop:nth-of-type(3),
.sm-d4:checked ~ .sm-track .sm-stop:nth-of-type(4),
.sm-d5:checked ~ .sm-track .sm-stop:nth-of-type(5),
.sm-d6:checked ~ .sm-track .sm-stop:nth-of-type(6) { color: var(--brand); }
.sm-d1:checked ~ .sm-track .sm-stop:nth-of-type(1)::before,
.sm-d2:checked ~ .sm-track .sm-stop:nth-of-type(2)::before,
.sm-d3:checked ~ .sm-track .sm-stop:nth-of-type(3)::before,
.sm-d4:checked ~ .sm-track .sm-stop:nth-of-type(4)::before,
.sm-d5:checked ~ .sm-track .sm-stop:nth-of-type(5)::before,
.sm-d6:checked ~ .sm-track .sm-stop:nth-of-type(6)::before { border-color: var(--brand); transform: scale(1.18); }

.sm-d1:focus-visible ~ .sm-track .sm-stop:nth-of-type(1)::before,
.sm-d2:focus-visible ~ .sm-track .sm-stop:nth-of-type(2)::before,
.sm-d3:focus-visible ~ .sm-track .sm-stop:nth-of-type(3)::before,
.sm-d4:focus-visible ~ .sm-track .sm-stop:nth-of-type(4)::before,
.sm-d5:focus-visible ~ .sm-track .sm-stop:nth-of-type(5)::before,
.sm-d6:focus-visible ~ .sm-track .sm-stop:nth-of-type(6)::before { outline: 2px solid var(--brand); outline-offset: 2px; }

/* N boxes for N domains. Each rule is "show every box up to mine". */
.sm-d1:checked ~ .sm-cols .sm-k1,
.sm-d2:checked ~ .sm-cols .sm-k1, .sm-d2:checked ~ .sm-cols .sm-k2,
.sm-d3:checked ~ .sm-cols .sm-k1, .sm-d3:checked ~ .sm-cols .sm-k2, .sm-d3:checked ~ .sm-cols .sm-k3,
.sm-d4:checked ~ .sm-cols .sm-k1, .sm-d4:checked ~ .sm-cols .sm-k2, .sm-d4:checked ~ .sm-cols .sm-k3, .sm-d4:checked ~ .sm-cols .sm-k4,
.sm-d5:checked ~ .sm-cols .sm-k1, .sm-d5:checked ~ .sm-cols .sm-k2, .sm-d5:checked ~ .sm-cols .sm-k3, .sm-d5:checked ~ .sm-cols .sm-k4, .sm-d5:checked ~ .sm-cols .sm-k5,
.sm-d6:checked ~ .sm-cols .sm-k1, .sm-d6:checked ~ .sm-cols .sm-k2, .sm-d6:checked ~ .sm-cols .sm-k3, .sm-d6:checked ~ .sm-cols .sm-k4, .sm-d6:checked ~ .sm-cols .sm-k5, .sm-d6:checked ~ .sm-cols .sm-k6 {
  background: color-mix(in oklab, var(--red) 13%, transparent);
  border-style: solid;
  border-color: color-mix(in oklab, var(--red) 34%, transparent);
  color: var(--red);
  transform: none;
}

.sm-d1:checked ~ .sm-cols .sm-t1, .sm-d2:checked ~ .sm-cols .sm-t2,
.sm-d3:checked ~ .sm-cols .sm-t3, .sm-d4:checked ~ .sm-cols .sm-t4,
.sm-d5:checked ~ .sm-cols .sm-t5, .sm-d6:checked ~ .sm-cols .sm-t6,
.sm-d1:checked ~ .sm-head .sm-n b:nth-child(1), .sm-d2:checked ~ .sm-head .sm-n b:nth-child(2),
.sm-d3:checked ~ .sm-head .sm-n b:nth-child(3), .sm-d4:checked ~ .sm-head .sm-n b:nth-child(4),
.sm-d5:checked ~ .sm-head .sm-n b:nth-child(5), .sm-d6:checked ~ .sm-head .sm-n b:nth-child(6) { opacity: 1; }

.sm-hint { margin: 14px 0 0; font-size: 12.5px; color: var(--aisi-ink-3); }

/* ---- fallbacks ------------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  /* Resting values for everything the keyframes were the only source of. */
  .sm-tile, .sm-ai-g { opacity: 1; }
}

@media (max-width: 620px) {
  .sm-stack { height: 168px; }
  .sm-box { height: 22px; }
  .sm-k1 { bottom: 26px; } .sm-k2 { bottom: 50px; } .sm-k3 { bottom: 74px; }
  .sm-k4 { bottom: 98px; } .sm-k5 { bottom: 122px; } .sm-k6 { bottom: 146px; }
  .sm-total b { font-size: 19px; }
  .sm-stop { font-size: 10px; }
}

/* ---------------------------------------------------------------- motion off */

/* The important half of this file. --p is pinned to its finished value, so
   every scroll-driven diagram renders COMPLETE: the links have collapsed, the
   answer is there, the rail is full, the paths are drawn. A reduced-motion
   reader gets the end of the story, not a blank frame waiting for a scroll
   handler that has been switched off. */
@media (prefers-reduced-motion: reduce) {
  .aisi, .aisi * { --p: 1 !important; }
  .aisi *, .aisi *::before, .aisi *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    /* Delay has to go too, and it was missed here for a long time. Killing the
       duration alone only makes each step INSTANT; it does not make it happen
       NOW. A sequence built from delays -- which is how every staggered diagram
       on these pages is built -- still played out over four and a half seconds,
       so a reduced-motion reader watched the whole animation, just in jump
       cuts. Caught on the AEO hero, where the score ring is five stacked
       numbers whose delays had not elapsed yet, so it rendered as overlapping
       digits. With the delay zeroed every animation lands on its final frame in
       the first paint, which is what the note above always claimed. */
    animation-delay: 0s !important;
    transition-delay: 0s !important;
  }
  .aisi-rise { opacity: 1 !important; transform: none !important; }
  .aisi-field { animation: none; }
}

/* Same treatment for anyone who lands without JS: the scroll driver never
   runs, so nothing would ever set --p. Scoped to html.no-js, which the
   inline boot removes immediately, so this costs a class toggle and buys a
   page that is readable when a script fails to load. */
html.no-js .aisi-rise { opacity: 1; transform: none; }
html.no-js .aisi { --p: 1; }
