/* ═══════════════════════════════════════════════════════════════════════
   GeXOn AI — Scroll-scrubbed process chapters
   ───────────────────────────────────────────────────────────────────────
   Loaded AFTER main.min.css / components.min.css, on services.html and
   solmirix.html only.

   WHY THIS FILE EXISTS, AND WHY IT IS NOT MINIFIED
   There is no minifier, build script or package.json anywhere in this
   repo. main.css (source) and main.min.css (what pages actually load) are
   kept in sync entirely by hand, which means they can silently drift and
   nobody can review a diff of a single 68KB line. Rather than extend that
   liability, all new CSS lives here as one hand-maintained, readable file.

   Assets are served `immutable` for one year (.htaccess:37-40), so
   BUMP THE ?v= IN BOTH <link> TAGS WHENEVER YOU EDIT THIS FILE.
   Returning visitors will not otherwise revalidate it, even on reload.

   RULES MARKED "SHADOWS" override a rule whose source of truth is
   main.css. main.min.css is deliberately left byte-identical so that no
   other page's year-long cache is invalidated. Apply the same fix to
   main.css when convenient so a future re-minify converges; never apply
   it to main.min.css alone.

   THE ONE ARCHITECTURAL RULE
   Base styles are the FINISHED END STATE. The `[data-stage]` attribute —
   which only JavaScript can add — is the switch that starts hiding the
   future. So "no JS", "reduced motion" and "before the first scroll
   frame" all degrade to a complete, readable visual rather than an empty
   box. Never write a base rule that hides content pending a JS class.
   ═══════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════
   1 · SHADOWED OVERRIDES
   Each of these fixes a rule in main.css that would otherwise break the
   staged chapters or lose content when JS is off.
   ═══════════════════════════════════════════════════════════════════════ */

/* SHADOWS main.css:3824 — `.chapter { overflow: hidden }`.
   overflow:hidden establishes a scroll container, and a descendant
   position:sticky resolves against THAT rather than the viewport. Since
   it never scrolls, sticky silently never engages. `clip` clips exactly
   the same way without creating a scroll container. */
.chapter {
  overflow: clip;
}

/* SHADOWS main.css:2140 — `.doc-demo__field-value { opacity: 0 }`.
   The value was revealed only by `.is-filled`, a class JS adds. With JS
   disabled every extracted value rendered invisible, so the document
   visual showed four empty labelled boxes. Base is now the filled end
   state; the [data-stage] rules in §5 hide the not-yet-reached ones. */
.doc-demo__field-value {
  opacity: 1;
  transform: none;
}

/* SHADOWS main.css:2217 — `.lead-demo__card-badge { opacity: 0 }`.
   Same bug: with JS off the lead card showed no score and no assignee. */
.lead-demo__card-badge {
  opacity: 1;
  transform: none;
}

/* SHADOWS main.css:1968 — `.chat-bubble { opacity: 0 }`.
   The worst instance of the pattern: `.chat-app__body` was EMPTY in the
   HTML and every bubble was JSON.parse'd out of an adjacent <script> and
   injected by JS, so with JS off the chat visuals rendered as blank
   screens. The conversations are now authored as real HTML, which means
   the bubbles must be visible by default. */
.chat-bubble {
  opacity: 1;
  transform: none;
}

/* A conversation inside a stage panel, without the phone frame. The
   320px fixed height (main.css:1947) is sized for a phone mock and is
   far too tall here. */
.pv__panel .chat-app__body {
  height: auto;
  max-height: 260px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  padding: 0.6rem 0.55rem;
}

.pv__panel .chat-bubble { font-size: 0.68rem; max-width: 88%; }

/* Bubbles arrive one after another when their stage opens, restarting on
   each entry because the panel flips display:none -> block. */
[data-stage] .pv__panel .chat-bubble {
  animation: pv-row-in var(--duration-lift) var(--ease-premium) both;
}
[data-stage] .pv__panel .chat-bubble:nth-child(1) { animation-delay: calc(var(--stagger-step) * 1); }
[data-stage] .pv__panel .chat-bubble:nth-child(2) { animation-delay: calc(var(--stagger-step) * 3); }
[data-stage] .pv__panel .chat-bubble:nth-child(3) { animation-delay: calc(var(--stagger-step) * 5); }
[data-stage] .pv__panel .chat-bubble:nth-child(4) { animation-delay: calc(var(--stagger-step) * 7); }
[data-stage] .pv__panel .chat-bubble:nth-child(n+5) { animation-delay: calc(var(--stagger-step) * 9); }

/* The source-of-truth line above a reply: shows the answer came from a
   real system lookup rather than being invented. */
.chat-source {
  align-self: flex-end;
  max-width: 88%;
  margin-bottom: -0.1rem;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  color: rgba(20, 30, 69, 0.5);
}

/* There is no scroll-margin-top anywhere in this codebase, despite a
   72px position:fixed navbar — so every anchor jump (#doctrace, #ask-ai)
   already lands underneath it today. Worse once sections are tall. */
.chapter,
.stage-step,
.module-panel {
  scroll-margin-top: calc(var(--nav-height) + var(--space-4));
}

/* The five service anchors are the ones the sub-nav itself links to, so
   they have to clear the sub-nav as well as the navbar — otherwise
   clicking "UnifyCore" hides the UnifyCore heading behind the very strip
   that was clicked. */
.svc-chapter {
  scroll-margin-top: calc(var(--nav-height) + 46px + var(--space-2));
}


/* ═══════════════════════════════════════════════════════════════════════
   2 · STAGED CHAPTER LAYOUT
   `.is-scrubbed` is added by JS only, and only above 900px with motion
   allowed. Every sticky/tall rule hangs off it, so mobile and no-JS both
   fall back to ordinary document flow for free — no @media duplication,
   and no chance of a no-JS visitor landing on a 300vh section whose
   visual does not stick.
   ═══════════════════════════════════════════════════════════════════════ */

/* 900px matches the existing two-column switch for .svc-chapter__grid
   (main.css:4008) and .chapter__grid (main.css:3834). Below it there is
   only one column, so there is no second column for copy to scrub past.
   A structural fact, not a taste call — do not invent another value. */
@media (min-width: 900px) {

  /* align-self on the item rather than align-items on the grid, so this
     does not have to shadow main.css:4004 / :3830. A grid item needs to
     be shorter than its grid area for sticky to have travel; `start`
     content-sizes it while the area stays as tall as the copy column. */
  .is-scrubbed .svc-chapter__visual,
  .is-scrubbed .chapter__visual {
    position: sticky;
    align-self: start;
  }

  /* Two different offsets on purpose. services.html carries the sticky
     .chapter-nav (§8) directly under the navbar, so its visuals have to
     clear both; solmirix.html has no sub-nav and only clears the navbar.
     The class names are already page-exclusive — .svc-chapter__visual
     exists only on services.html, .chapter__visual only on solmirix.html
     — so no extra hook is needed. */
  .is-scrubbed .svc-chapter__visual {
    top: calc(var(--nav-height) + 46px + var(--space-6));
  }

  .is-scrubbed .chapter__visual {
    top: calc(var(--nav-height) + var(--space-8));
  }

  /* Each stage's copy block owns a screenful. This min-height is the ONLY
     pacing knob in the system — the controller derives everything else
     from measured geometry, so there is no magic number to keep in sync
     between CSS and JS. Note that a copy edit which makes one step much
     taller than its siblings will lengthen that step's dwell time. */
  .is-scrubbed .stage-step {
    min-height: 62vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  /* The last step gets less trailing room so the CTA below it is not
     stranded a full screen further down. */
  .is-scrubbed .stage-step:last-child {
    min-height: 48vh;
  }
}

/* ── The stage list (copy column) ──────────────────────────────────────
   A plain <ol>. With no JS this is simply a numbered list of five
   headed paragraphs — which is already the readable stacked fallback,
   so no <noscript> is needed. Mirrors the .service-demo__steps idiom
   already used on solmirix.html. */
.stage-list {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  counter-reset: stage;
}

.stage-step {
  counter-increment: stage;
  position: relative;
  padding-left: var(--space-10);
  padding-bottom: var(--space-6);
}

/* The connector rail lives on the LIST, not on each step — one element
   spanning the whole process, fading out at both ends so it does not
   overhang the first and last badge.

   It has to be the list rather than per-step because in scrub mode each
   step is 62vh with its content vertically centred, so a per-step rail
   segment has no fixed relationship to where that step's badge sits. */
.stage-list::before {
  content: "";
  position: absolute;
  left: 13.25px;
  top: 0;
  bottom: 0;
  width: 1.5px;
  background: linear-gradient(to bottom,
    transparent 0,
    var(--color-border) 3%,
    var(--color-border) 97%,
    transparent 100%);
}

.chapter .stage-list::before {
  background: linear-gradient(to bottom,
    transparent 0,
    rgba(255, 255, 255, 0.14) 3%,
    rgba(255, 255, 255, 0.14) 97%,
    transparent 100%);
}

/* The number badge hangs off the TITLE, not the step. In scrub mode the
   step is 62vh tall with its content centred, so a badge anchored to the
   step's top edge ends up orphaned in empty space far above the heading
   it belongs to. Anchoring it to the title means it travels with the
   text in every mode. It sits on the rail with an opaque background, so
   the line appears to break around it. */
.stage-step__title::before {
  content: counter(stage);
  position: absolute;
  left: calc(-1 * var(--space-10));
  top: 50%;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--color-border);
  background: var(--stage-badge-bg, var(--color-bg-primary));
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 700;
  z-index: 1;
  transition: background var(--duration-snap) var(--ease-premium),
              border-color var(--duration-snap) var(--ease-premium),
              color var(--duration-snap) var(--ease-premium);
}

/* The badge must match whatever surface its section paints, or the rail
   shows through it. Three cases across the two pages. */
.svc-chapter--alt { --stage-badge-bg: var(--color-bg-page); }
.chapter { --stage-badge-bg: var(--color-bg-dark); }
.chapter .stage-step__title::before { border-color: rgba(255, 255, 255, 0.22); color: rgba(255, 255, 255, 0.6); }

.stage-list { position: relative; }

.stage-step__title {
  position: relative;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  margin: 0 0 var(--space-2);
  color: var(--color-text-primary);
}

.stage-step__body {
  margin: 0;
  color: var(--color-text-secondary);
  max-width: 46ch;
}

/* Active state. Only reachable once a driver has set a stage, so with
   JS off every step renders in the neutral state — correct, since with
   no scrubbing no single step is "current". */
.stage-step.is-active .stage-step__title::before {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--navy-900, #0D1520);
}

.stage-step.is-active .stage-step__title {
  color: var(--teal-700);
}

/* Dim the steps the reader is not on — but only while scrubbing, and
   never below a readable contrast. Text stays legible because dimming
   copy the reader might still want is hostile; this is a soft focus
   cue, not a curtain. */
@media (min-width: 900px) {
  .is-scrubbed .stage-step {
    opacity: 0.45;
    transition: opacity var(--duration-lift) var(--ease-premium);
  }
  .is-scrubbed .stage-step.is-active {
    opacity: 1;
  }
}

/* Dark-section variant (solmirix chapters sit on .section--dark). */
.chapter .stage-step__title { color: var(--white); }
.chapter .stage-step__body { color: rgba(255, 255, 255, 0.72); }
.chapter .stage-step.is-active .stage-step__title { color: var(--teal-300, #85E8C8); }


/* ═══════════════════════════════════════════════════════════════════════
   3 · STAGE CHIPS (below 900px)
   The mobile counterpart to the sticky rail: a horizontal strip naming
   every stage, with the running one highlighted. Labels are visible
   because naming the five steps IS the point — the reader should see
   this is a process at a glance. aria-hidden because the <h3> in each
   stage-step already carries the same text to assistive tech.
   Driven entirely from data-stage on the section root, so it needs no
   JavaScript of its own.
   ═══════════════════════════════════════════════════════════════════════ */

.stage-chips {
  display: flex;
  gap: var(--space-2);
  margin: var(--space-4) 0 var(--space-2);
  padding: 0 0 var(--space-2);
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
}

.stage-chips::-webkit-scrollbar { display: none; }

@media (min-width: 900px) {
  .stage-chips { display: none; }
}

.stage-chip {
  flex: 0 0 auto;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  font-size: 0.7rem;
  font-weight: 600;
  /* --color-text-muted is 3.14:1 on the light .svc-chapter background —
     fails WCAG 4.5:1. .chapter's dark variant below has its own color. */
  color: var(--color-text-secondary);
  white-space: nowrap;
  transition: background var(--duration-snap) var(--ease-premium),
              border-color var(--duration-snap) var(--ease-premium),
              color var(--duration-snap) var(--ease-premium);
}

.chapter .stage-chip { border-color: rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.6); }

[data-stage="0"] .stage-chip:nth-child(1),
[data-stage="1"] .stage-chip:nth-child(2),
[data-stage="2"] .stage-chip:nth-child(3),
[data-stage="3"] .stage-chip:nth-child(4),
[data-stage="4"] .stage-chip:nth-child(5) {
  background: rgba(61, 213, 160, 0.12);
  border-color: var(--color-accent);
  color: var(--teal-700);
}

/* No descendant combinator: data-stage is set on the section root, which
   IS the .chapter element on solmirix.html. `.chapter [data-stage]` would
   look for a data-stage DESCENDANT and never match. */
.chapter[data-stage="0"] .stage-chip:nth-child(1),
.chapter[data-stage="1"] .stage-chip:nth-child(2),
.chapter[data-stage="2"] .stage-chip:nth-child(3),
.chapter[data-stage="3"] .stage-chip:nth-child(4),
.chapter[data-stage="4"] .stage-chip:nth-child(5) {
  color: var(--teal-300, #85E8C8);
}


/* ═══════════════════════════════════════════════════════════════════════
   4 · PROCESS VISUAL (.pv) — the shared component vocabulary
   Seven chapters × five stages is thirty-five distinct screens. Rather
   than thirty-five bespoke components, everything below is built from
   one small set of primitives: a persistent side rail, a swapping main
   panel, rows, badges, chips, bars and a log. New stages are authored in
   HTML, not CSS.
   ═══════════════════════════════════════════════════════════════════════ */

.pv__body {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  padding: 0.9rem;
  min-height: 340px;
  align-content: start;
}

@media (min-width: 560px) {
  .pv__body--split {
    grid-template-columns: 0.92fr 1.08fr;
  }
}

/* Contain layout so the visual's internal reflows — a row appearing, a
   bar growing — do not invalidate layout for the whole tall section.
   Deliberately NOT `contain: paint`: .app-window carries a box-shadow
   that paint containment would clip off. */
.svc-chapter__visual > .app-window,
.chapter__visual > .app-window {
  contain: layout;
}

/* ── Side rail: the thing that persists across all five stages ──────── */
.pv__side {
  min-width: 0;
}

.pv__side-head,
.pv__panel-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.55rem;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* --color-text-muted (grey-400) is 3.14:1 on the .app-window__body white
     card — below WCAG's 4.5:1 minimum for this size. */
  color: var(--color-text-secondary);
}

.pv__count {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  padding: 0.1rem 0.4rem;
  border-radius: var(--radius-full);
  background: rgba(20, 30, 69, 0.07);
  color: var(--color-text-secondary);
}

/* ── Rows: the workhorse. A document, a message, a lead, a system, a
   source file — all the same primitive. ─────────────────────────────── */
.pv__rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.pv__row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: #fff;
  font-size: 0.72rem;
  transition: border-color var(--duration-snap) var(--ease-premium),
              background var(--duration-snap) var(--ease-premium),
              transform var(--duration-snap) var(--ease-premium);
}

.pv__row-icon {
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  background: rgba(20, 30, 69, 0.06);
  font-size: 0.72rem;
  flex-shrink: 0;
}

.pv__row-icon svg { width: 13px; height: 13px; }

/* A connection indicator drawn in CSS rather than typed as a ● glyph.
   Obscure Unicode symbols are font-dependent — U+25CF and friends render
   as empty tofu boxes wherever the glyph is missing, which is exactly
   what happened on Linux. Short mono labels (PDF, SCN, WEB) and this
   drawn dot are the only two icon treatments used, both font-proof. */
.pv__row-icon--dot::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(61, 213, 160, 0.18);
}

.pv__row-main { min-width: 0; }

.pv__row-name {
  font-weight: 700;
  color: var(--color-text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pv__row-meta {
  font-size: 0.62rem;
  /* --color-text-muted is 3.14:1 on white — fails WCAG 4.5:1 at this size. */
  color: var(--color-text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Row states. --active is the one currently being worked on; --ok has
   passed; --warn is the failure branch that every process needs. */
.pv__row--active {
  border-color: var(--color-accent);
  background: rgba(61, 213, 160, 0.07);
}

.pv__row--ok { border-color: rgba(61, 213, 160, 0.55); }

.pv__row--warn {
  border-color: var(--amber-500, #F5A623);
  background: rgba(245, 166, 35, 0.08);
}

.pv__row--muted { opacity: 0.5; }

/* ── Badges ────────────────────────────────────────────────────────── */
.pv__badge {
  display: inline-flex;
  align-items: center;
  gap: 0.22rem;
  padding: 0.12rem 0.42rem;
  border-radius: var(--radius-full);
  font-size: 0.6rem;
  font-weight: 700;
  white-space: nowrap;
  background: rgba(20, 30, 69, 0.07);
  color: var(--color-text-secondary);
}

.pv__badge--ok { background: rgba(61, 213, 160, 0.16); color: var(--teal-700); }
.pv__badge--warn { background: rgba(245, 166, 35, 0.18); color: #8A5A00; }
.pv__badge--hot { background: rgba(154, 40, 70, 0.12); color: var(--garnet-500, #9A2846); }
.pv__badge--mono { font-family: var(--font-mono); font-weight: 500; }

/* ── Key/value list: extracted fields, enrichment, score breakdown ──── */
.pv__kvs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.32rem;
}

.pv__kv {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.38rem 0.55rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-size: 0.71rem;
  transition: border-color var(--duration-snap) var(--ease-premium),
              background var(--duration-snap) var(--ease-premium);
}

/* --color-text-muted is 3.14:1 on white — fails WCAG 4.5:1 at this size. */
.pv__kv-key { color: var(--color-text-secondary); }

.pv__kv-val {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-weight: 600;
  color: var(--color-text-primary);
  text-align: right;
}

.pv__kv--ok { border-color: rgba(61, 213, 160, 0.5); background: rgba(61, 213, 160, 0.06); }
.pv__kv--warn { border-color: var(--amber-500, #F5A623); background: rgba(245, 166, 35, 0.08); }
.pv__kv--total { border-color: var(--color-accent); background: rgba(61, 213, 160, 0.1); font-weight: 700; }
.pv__kv--total .pv__kv-key { color: var(--color-text-primary); font-weight: 700; }

.pv__tick { color: var(--color-accent); font-weight: 700; }
.pv__flag { color: #8A5A00; font-weight: 700; }

/* ── Destination chips: where the data actually lands. The stage that
   makes the whole pipeline feel real. ──────────────────────────────── */
.pv__dests {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.pv__dest {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.55rem;
  border: 1px solid rgba(61, 213, 160, 0.5);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: rgba(61, 213, 160, 0.07);
  font-size: 0.71rem;
}

.pv__dest-sys {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--teal-700);
}

.pv__dest-what {
  color: var(--color-text-secondary);
  font-size: 0.66rem;
}

/* ── Capacity / confidence bars ─────────────────────────────────────── */
.pv__bar {
  height: 4px;
  border-radius: var(--radius-full);
  background: rgba(20, 30, 69, 0.09);
  overflow: hidden;
  margin-top: 0.25rem;
}

.pv__bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--color-accent);
  width: var(--fill, 0%);
}

.pv__bar--full > span { background: var(--amber-500, #F5A623); }

/* ── Event log (UnifyCore sync + guard) ─────────────────────────────── */
.pv__log {
  list-style: none;
  margin: 0;
  padding: 0.5rem 0.55rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: #F7F8FA;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  line-height: 1.75;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.pv__log li { display: flex; gap: 0.45rem; align-items: baseline; }
/* --color-text-muted is 3.14:1 on white — fails WCAG 4.5:1 at this size. */
.pv__log-t { color: var(--color-text-secondary); flex-shrink: 0; }
.pv__log-msg { color: var(--color-text-secondary); }
.pv__log li[data-level="ok"] .pv__log-msg { color: var(--teal-700); }
.pv__log li[data-level="warn"] .pv__log-msg { color: #8A5A00; }

/* ── Mapping rows (UnifyCore) ───────────────────────────────────────── */
.pv__map {
  font-family: var(--font-mono);
  font-size: 0.63rem;
}

.pv__map .pv__row { grid-template-columns: 1fr auto; }

/* ── The closing note under each panel: the plain-language promise ──── */
.pv__note {
  margin: 0.6rem 0 0;
  padding-top: 0.55rem;
  border-top: 1px dashed var(--color-border);
  font-size: 0.66rem;
  line-height: 1.55;
  /* --color-text-muted is 3.14:1 on white — fails WCAG 4.5:1 at this size. */
  color: var(--color-text-secondary);
}

.pv__note strong { color: var(--color-text-secondary); }


/* ═══════════════════════════════════════════════════════════════════════
   5 · THE STATE MACHINE
   The single most important block in this file.

   Base (no [data-stage] — meaning no JS, or reduced motion, or before
   the first scroll frame): EVERY panel is visible, stacked and labelled.
   The reader sees the whole process as one long readable panel.

   Once a driver sets [data-stage], and only then, panels become
   one-at-a-time. Nothing is ever hidden pending a class that may never
   arrive.
   ═══════════════════════════════════════════════════════════════════════ */

.pv__main { min-width: 0; }

.pv__panel + .pv__panel { margin-top: var(--space-4); }

.pv__panel-label { color: var(--teal-700); }

/* Staged: one panel at a time. */
[data-stage] .pv__panel { display: none; }
[data-stage] .pv__panel + .pv__panel { margin-top: 0; }

[data-stage="0"] .pv__panel[data-panel="0"],
[data-stage="1"] .pv__panel[data-panel="1"],
[data-stage="2"] .pv__panel[data-panel="2"],
[data-stage="3"] .pv__panel[data-panel="3"],
[data-stage="4"] .pv__panel[data-panel="4"] {
  display: block;
  animation: pv-panel-in var(--duration-lift) var(--ease-premium) both;
}

@keyframes pv-panel-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* ── Progressive annotations on the persistent side rail ─────────────
   `data-from="N"` means "this element belongs to stage N onward". Base
   shows everything; [data-stage] hides anything not yet reached. Nine
   rules total rather than one per element. */
[data-stage] [data-from]                   { opacity: 0; transform: translateY(3px); transition: opacity var(--duration-lift) var(--ease-premium), transform var(--duration-lift) var(--ease-premium); }
[data-stage="0"] [data-from="0"],
[data-stage="1"] [data-from="0"], [data-stage="1"] [data-from="1"],
[data-stage="2"] [data-from="0"], [data-stage="2"] [data-from="1"], [data-stage="2"] [data-from="2"],
[data-stage="3"] [data-from="0"], [data-stage="3"] [data-from="1"], [data-stage="3"] [data-from="2"], [data-stage="3"] [data-from="3"],
[data-stage="4"] [data-from="0"], [data-stage="4"] [data-from="1"], [data-stage="4"] [data-from="2"], [data-stage="4"] [data-from="3"], [data-stage="4"] [data-from="4"] {
  opacity: 1;
  transform: none;
}

/* `data-only="N"` — present at exactly one stage. Used for the "reading
   this one now" marker that moves down the persistent queue. */
[data-stage] [data-only] { opacity: 0; }
[data-stage="0"] [data-only="0"],
[data-stage="1"] [data-only="1"],
[data-stage="2"] [data-only="2"],
[data-stage="3"] [data-only="3"],
[data-stage="4"] [data-only="4"] {
  opacity: 1;
}

/* ── Row stagger inside the active panel ────────────────────────────
   The "fields fill in one at a time" beat, done in pure CSS. Because
   switching stages flips the panel from display:none to display:block,
   the browser restarts these animations on every stage entry — so the
   sequence replays each time without a single line of JavaScript.

   Only applies while staged: with no JS every panel is visible at once
   and a staggered entrance would be meaningless. */
[data-stage] .pv__panel > * > li {
  animation: pv-row-in var(--duration-lift) var(--ease-premium) both;
}

[data-stage] .pv__panel > * > li:nth-child(1) { animation-delay: calc(var(--stagger-step) * 0.5); }
[data-stage] .pv__panel > * > li:nth-child(2) { animation-delay: calc(var(--stagger-step) * 1.5); }
[data-stage] .pv__panel > * > li:nth-child(3) { animation-delay: calc(var(--stagger-step) * 2.5); }
[data-stage] .pv__panel > * > li:nth-child(4) { animation-delay: calc(var(--stagger-step) * 3.5); }
[data-stage] .pv__panel > * > li:nth-child(5) { animation-delay: calc(var(--stagger-step) * 4.5); }
[data-stage] .pv__panel > * > li:nth-child(6) { animation-delay: calc(var(--stagger-step) * 5.5); }
/* Clamp, so a seven-row panel's last row does not wait half a second. */
[data-stage] .pv__panel > * > li:nth-child(n+7) { animation-delay: calc(var(--stagger-step) * 6.5); }

@keyframes pv-row-in {
  from { opacity: 0; transform: translateX(6px); }
  to   { opacity: 1; transform: none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   5b · ONE FORM PER SERVICE
   The five services share the stage MECHANICS (data-stage, panels, the
   rail) but deliberately do NOT share a visual container. Five identical
   window mockups made the page read as a template and flattened five
   genuinely different kinds of work into one shape.

   So each visual takes the form of the thing it actually is:
     DocTrace        desktop console + document queue   (the reference)
     SupportRoute    a phone, because support is lived on a phone
     ProspectRoute   a board where the card physically moves across it
     UnifyCore       two systems facing each other across a connector
     KnowledgeAssist a shelf of paper, no window chrome at all
   ═══════════════════════════════════════════════════════════════════════ */

/* ── SupportRoute: a phone whose SCREEN changes with the stage ──────── */
.pv-phone { max-width: 328px; margin: 0 auto; }

.pv-phone .pv__main {
  padding: 0.55rem 0.5rem 0.7rem;
  min-height: 372px;
  background: #ECEDF1;
  background-image: radial-gradient(circle, rgba(30, 43, 94, 0.06) 1px, transparent 1px);
  background-size: 20px 20px;
}

.pv-phone .pv__panel-label { margin-bottom: 0.45rem; font-size: 0.56rem; }
.pv-phone .pv__row { font-size: 0.66rem; padding: 0.38rem 0.45rem; gap: 0.4rem; }
.pv-phone .pv__row-icon { width: 19px; height: 19px; font-size: 0.62rem; }
.pv-phone .pv__row-meta { font-size: 0.58rem; }
.pv-phone .pv__kv { font-size: 0.65rem; padding: 0.32rem 0.45rem; }
.pv-phone .pv__dest { font-size: 0.64rem; padding: 0.36rem 0.45rem; }
.pv-phone .pv__dest-sys { font-size: 0.55rem; }
.pv-phone .pv__dest-what { font-size: 0.6rem; }
.pv-phone .pv__note { font-size: 0.6rem; }
.pv-phone .chat-app__body { height: auto; max-height: 296px; border: none; background: transparent; padding: 0.2rem 0; }

/* ── ProspectRoute: the card crosses the board as the stage advances ──
   Reuses .lead-demo from main.css:2157. The original keyed the position
   off data-stage on the CARD; ours is on the section root, so these
   rules replace that mapping. Five stages onto three columns. */
.pv-board .lead-demo { min-height: 196px; }

[data-stage="0"] .pv-board .lead-demo__card,
[data-stage="1"] .pv-board .lead-demo__card { left: 1.5%; }
[data-stage="2"] .pv-board .lead-demo__card { left: 34.8%; }
[data-stage="3"] .pv-board .lead-demo__card,
[data-stage="4"] .pv-board .lead-demo__card { left: 68%; }

[data-stage="1"] .pv-board .lead-demo__card-badge--firm,
[data-stage="2"] .pv-board .lead-demo__card-badge--firm,
[data-stage="3"] .pv-board .lead-demo__card-badge--firm,
[data-stage="4"] .pv-board .lead-demo__card-badge--firm,
[data-stage="2"] .pv-board .lead-demo__card-badge--score,
[data-stage="3"] .pv-board .lead-demo__card-badge--score,
[data-stage="4"] .pv-board .lead-demo__card-badge--score,
[data-stage="3"] .pv-board .lead-demo__card-badge--rep,
[data-stage="4"] .pv-board .lead-demo__card-badge--rep {
  opacity: 1;
  transform: none;
}

.lead-demo__card-badge--firm { background: rgba(20, 30, 69, 0.08); color: var(--color-text-secondary); }

/* Highlight the column the card is currently sitting in. */
[data-stage="0"] .pv-board .lead-demo__col:nth-child(1),
[data-stage="1"] .pv-board .lead-demo__col:nth-child(1),
[data-stage="2"] .pv-board .lead-demo__col:nth-child(2),
[data-stage="3"] .pv-board .lead-demo__col:nth-child(3),
[data-stage="4"] .pv-board .lead-demo__col:nth-child(3) {
  border-style: solid;
  border-color: var(--color-accent);
  background: rgba(61, 213, 160, 0.05);
}

/* ── UnifyCore: two systems facing each other across a connector ────── */
.pv-sync__pair {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0.6rem;
}

.pv-sync__box {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: #fff;
  overflow: hidden;
}

.pv-sync__box-bar {
  padding: 0.3rem 0.5rem;
  background: #F2F3F7;
  border-bottom: 1px solid var(--color-border);
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
}

.pv-sync__box-body {
  padding: 0.45rem 0.5rem;
  font-size: 0.64rem;
  color: var(--color-text-primary);
  min-height: 46px;
}

.pv-sync__box-body span { display: block; }
/* --color-text-muted is 3.14:1 on white — fails WCAG 4.5:1 at this size. */
.pv-sync__box-body em { font-style: normal; color: var(--color-text-secondary); font-size: 0.6rem; }

/* The connector: two arrowheads, because this syncs both ways. The
   travelling dot only runs while the section is on screen (§6). */
.pv-sync__link {
  position: relative;
  width: 42px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-accent);
}

.pv-sync__link::before,
.pv-sync__link::after {
  content: "";
  position: absolute;
  left: 4px;
  right: 4px;
  height: 1.5px;
  background: currentColor;
  opacity: 0.35;
}
.pv-sync__link::before { top: 7px; }
.pv-sync__link::after { bottom: 7px; }

.pv-sync__dot {
  position: absolute;
  top: 4.5px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 6px 1px rgba(61, 213, 160, 0.6);
  animation: pv-sync-right 2.4s ease-in-out infinite;
}

.pv-sync__dot--back {
  top: auto;
  bottom: 4.5px;
  animation: pv-sync-left 2.4s ease-in-out infinite;
  animation-delay: 1.2s;
}

@keyframes pv-sync-right {
  0%   { left: 2px;  opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { left: 34px; opacity: 0; }
}

@keyframes pv-sync-left {
  0%   { right: 2px;  opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { right: 34px; opacity: 0; }
}

/* ── KnowledgeAssist: paper, not software ───────────────────────────
   No window chrome at all. A shelf of stacked documents, so the reader
   reads "your own files" rather than "another dashboard". */
.pv-shelf {
  background: linear-gradient(180deg, #FBFBFD 0%, #F4F5F8 100%);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 0.9rem;
  box-shadow: var(--shadow-md);
}

.pv-shelf__title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 0.7rem;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* --color-text-muted is 3.14:1 on this near-white shelf — fails WCAG 4.5:1. */
  color: var(--color-text-secondary);
}

/* Each source looks like a sheet of paper with pages behind it. */
.pv-shelf .pv__row {
  position: relative;
  background: #fff;
  border-radius: 3px 3px var(--radius-sm) var(--radius-sm);
  box-shadow: 0 1px 0 0 #fff, 0 2px 0 0 rgba(20, 30, 69, 0.07),
              0 4px 0 0 #fff, 0 5px 0 0 rgba(20, 30, 69, 0.05);
}

.pv-shelf .pv__rows { gap: 0.6rem; }

.pv-shelf .pv__row-icon {
  font-family: var(--font-mono);
  font-size: 0.54rem;
  font-weight: 700;
  background: rgba(35, 118, 89, 0.1);
  color: var(--teal-700);
}

/* The answer card is clearly a different object from the sources. */
.pv-answer {
  margin-top: 0.7rem;
  padding: 0.7rem;
  border: 1px solid rgba(61, 213, 160, 0.45);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: #fff;
}


/* ═══════════════════════════════════════════════════════════════════════
   6 · AMBIENT ANIMATION GATING
   The infinite keyframes in main.css (doc-scan, chart-draw,
   approve-*-cycle) currently run for the entire session regardless of
   viewport — on services.html that is five chapters' worth of timers and
   compositor work burning while the reader is at the footer.

   `animation-play-state` is the right lever: it does not touch layout,
   does not restart the animation on re-entry, and is untouched by the
   reduced-motion !important block at main.css:224-232, which only
   overrides animation-duration and iteration-count.

   Every gated animation's paused frame must still look correct, since
   that is what an off-screen or reduced-motion reader sees.
   doc-scan's 0% is { top: 6%; opacity: 0 } — invisible, which is fine.
   ═══════════════════════════════════════════════════════════════════════ */

.doc-demo__scanline,
.chart-demo__line,
.chart-demo__forecast,
.chart-demo__alert,
.approve-demo__pending,
.approve-demo__approved,
.demo-pulse,
.pv-sync__dot {
  animation-play-state: paused;
}

[data-in-view] .doc-demo__scanline,
[data-in-view] .chart-demo__line,
[data-in-view] .chart-demo__forecast,
[data-in-view] .chart-demo__alert,
[data-in-view] .approve-demo__pending,
[data-in-view] .approve-demo__approved,
[data-in-view] .demo-pulse,
[data-in-view] .pv-sync__dot {
  animation-play-state: running;
}

/* chart-draw starts at a full stroke-dashoffset, i.e. an ABSENT line.
   Pausing there would leave an empty chart, breaking the end-state rule,
   so the forecast chart holds its drawn state when not running. */
.chart-demo__line { stroke-dashoffset: 0; }


/* ═══════════════════════════════════════════════════════════════════════
   6b · ENGAGEMENT TIMELINE (services hero)
   The connector draws itself left-to-right and the four stages arrive in
   sequence, so the hero reads as a process running rather than four icons
   sitting in a row. Driven entirely off the existing .reveal observer —
   no new JavaScript.
   ═══════════════════════════════════════════════════════════════════════ */

.svc-timeline__line {
  stroke-dasharray: 940;
  stroke-dashoffset: 940;
  transition: stroke-dashoffset 1.5s var(--ease-premium) 0.15s;
}

.process-timeline.visible .svc-timeline__line { stroke-dashoffset: 0; }

/* Base is fully visible: with JS off .visible never arrives, and a hero
   that renders as an empty box is worse than one that does not animate. */
.process-timeline .svc-timeline__node { opacity: 1; }

.process-timeline.reveal:not(.visible) .svc-timeline__node { opacity: 0; }

.process-timeline .svc-timeline__node {
  transition: opacity var(--duration-lift) var(--ease-premium);
}

.process-timeline.visible .svc-timeline__node:nth-of-type(1) { transition-delay: 0.30s; }
.process-timeline.visible .svc-timeline__node:nth-of-type(2) { transition-delay: 0.62s; }
.process-timeline.visible .svc-timeline__node:nth-of-type(3) { transition-delay: 0.94s; }
.process-timeline.visible .svc-timeline__node:nth-of-type(4) { transition-delay: 1.26s; }


/* ═══════════════════════════════════════════════════════════════════════
   7 · SCROLL PROGRESS BAR
   A 2px accent line pinned under the navbar. These pages are long now;
   this is the cheapest possible orientation cue.
   ═══════════════════════════════════════════════════════════════════════ */

.scroll-progress {
  position: fixed;
  top: var(--nav-height);
  left: 0;
  right: 0;
  height: 2px;
  z-index: var(--z-sticky, 100);
  background: transparent;
  pointer-events: none;
}

.scroll-progress__bar {
  height: 100%;
  width: 100%;
  transform: scaleX(var(--progress, 0));
  transform-origin: 0 50%;
  background: linear-gradient(90deg, var(--teal-700), var(--color-accent));
}


/* ═══════════════════════════════════════════════════════════════════════
   8 · CHAPTER SUB-NAV (services.html)
   Five staged pipelines is a long page. This is the mitigation: a reader
   who only wants UnifyCore gets there in one click instead of scrolling
   through four other processes.
   ═══════════════════════════════════════════════════════════════════════ */

.chapter-nav {
  position: sticky;
  top: var(--nav-height);
  z-index: 90;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--color-border);
}

.chapter-nav__list {
  display: flex;
  gap: var(--space-1);
  margin: 0;
  padding: 0.55rem 0;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
}

.chapter-nav__list::-webkit-scrollbar { display: none; }

.chapter-nav__link {
  display: block;
  flex: 0 0 auto;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius-full);
  font-size: 0.76rem;
  font-weight: 600;
  color: var(--color-text-secondary);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--duration-snap) var(--ease-premium),
              color var(--duration-snap) var(--ease-premium);
}

.chapter-nav__link:hover { background: rgba(20, 30, 69, 0.05); color: var(--color-text-primary); }

.chapter-nav__link[aria-current="true"] {
  background: rgba(61, 213, 160, 0.14);
  color: var(--teal-700);
}


/* ═══════════════════════════════════════════════════════════════════════
   9 · SOLMIRIX MODULE PIPELINE MAP
   Shows where the selected in-progress module sits relative to the two
   modules that are live today — so a reader understands Forecasting
   depends on Unify and Ask, rather than reading four tabs as four
   unrelated products.
   ═══════════════════════════════════════════════════════════════════════ */

.pipeline-map {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin: 0 0 var(--space-5);
  padding: 0.5rem 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.03);
  font-size: 0.68rem;
}

/* Every module is live, so the map's job is only to show ORDER — which
   steps come before this one — not which ones exist yet. Base colour is
   therefore the active teal, with the current step boxed. */
.pipeline-map__step {
  color: rgba(133, 232, 200, 0.8);
  font-weight: 600;
}

.pipeline-map__step--live { color: var(--teal-300, #85E8C8); }

.pipeline-map__step--here {
  padding: 0.14rem 0.5rem;
  border-radius: var(--radius-full);
  background: rgba(61, 213, 160, 0.16);
  color: var(--white);
  font-weight: 700;
}

.pipeline-map__sep { color: rgba(255, 255, 255, 0.25); }

/* ── The module step list becomes the stage rail for its panel's demo ──
   These four panels are tab-switched rather than scroll-scrubbed, so
   there is no scroll position to drive them. Instead each step lights up
   in turn on a loop timed to the demo animation beside it (chart-draw
   runs 3.4s, so four steps make a 13.6s cycle and each step holds for
   exactly one quarter of it).

   Done entirely in CSS: no JavaScript, and it inherits the same
   [data-in-view] gate as everything else in §6, so it stops when the
   section is off screen. Base state is fully legible, so a reader with
   reduced motion or no JS sees a normal, complete step list. */
[data-in-view] .service-demo__steps > li {
  animation: step-cycle 13.6s linear infinite;
}

[data-in-view] .service-demo__steps > li:nth-child(1) { animation-delay: 0s; }
[data-in-view] .service-demo__steps > li:nth-child(2) { animation-delay: 3.4s; }
[data-in-view] .service-demo__steps > li:nth-child(3) { animation-delay: 6.8s; }
[data-in-view] .service-demo__steps > li:nth-child(4) { animation-delay: 10.2s; }

@keyframes step-cycle {
  0%, 24.9% {
    color: rgba(255, 255, 255, 0.92);
    border-left-color: var(--color-accent);
  }
  25%, 100% {
    color: rgba(255, 255, 255, 0.6);
    border-left-color: transparent;
  }
}

/* The rail the highlight travels down. Present at rest too, so the list
   does not visibly gain a border only once animation starts. */
.service-demo__steps > li {
  border-left: 2px solid transparent;
  /* main.css gives these items a counter badge at `position:absolute;
     left:0; width:1.5rem` and reserves room for it with padding-left:2rem.
     This rule previously set padding-left:0.7rem for the rail alone, which
     pulled the text back UNDER the badge — rendering "Learn" as "1arn".
     The rail needs 0.7rem AND the badge needs 1.5rem, so reserve both, and
     shift the badge clear of the border. */
  padding-left: calc(0.7rem + 1.75rem);
  transition: color var(--duration-snap) var(--ease-premium),
              border-left-color var(--duration-snap) var(--ease-premium);
}

.service-demo__steps > li::before {
  left: 0.7rem;
  /* teal-700 is a dark green — near-invisible on the navy panel these lists
     actually sit on. Use the light-on-dark pair instead. */
  background: rgba(61, 213, 160, 0.18);
  color: var(--teal-300, #85E8C8);
}

/* main.css sets `.service-demo__steps strong { color: var(--color-text-primary) }`
   — which resolves to --grey-900, near-black ink meant for light surfaces.
   But the very next rule puts the list on rgba(255,255,255,0.7), i.e. always
   on navy. So the one word that matters in each row ("Learn", "Forecast",
   "Flag", "Refine") rendered black-on-navy and was effectively invisible.

   `inherit` makes the label follow the row's own animated colour, so the
   active step's label brightens with it — which is what the step-cycle
   highlight was trying to convey in the first place. */
.service-demo__steps > li strong {
  color: inherit;
  font-weight: var(--font-weight-semibold, 600);
}


/* ═══════════════════════════════════════════════════════════════════════
   10 · DARK-SECTION VARIANTS OF THE .pv PRIMITIVES
   The SolmiriX chapters render on .section--dark, but the .app-window
   body is white on both pages, so most primitives need no variant. Only
   the pieces that sit OUTSIDE the window do.
   ═══════════════════════════════════════════════════════════════════════ */

/* alpha 0.55 was 3.75:1 on white, below WCAG 4.5:1; 0.7 clears it at ~6.1:1. */
.chapter .pv__note { color: rgba(20, 30, 69, 0.7); }


/* ═══════════════════════════════════════════════════════════════════════
   11 · CINEMATIC LIVE PANELS (.lp)
   ───────────────────────────────────────────────────────────────────────
   The heroes on index.html, solmirix.html and ai-training.html used to be
   flat <svg> pictures with the text baked into <text> nodes: unselectable,
   unresizable, invisible to translation, and — the actual complaint —
   motionless. A product page whose hero is a screenshot of a product reads
   as a mockup, not as software.

   These are the same illustrations rebuilt as real HTML that MOVES. They
   reuse the .app-window chrome and the whole .pv__* primitive vocabulary
   from section 4, so there is very little new surface area here: mostly
   this file just describes WHEN each existing primitive arrives.

   ── THE CONTRACT ──────────────────────────────────────────────────────
   Identical in spirit to [data-stage] in section 5, and for the same
   reason. Two classes, both written ONLY by initCinematicLoop() in
   main.js:

     .is-cine        the engine has taken over. This is the switch that
                     starts hiding the future. Without it — no JS, reduced
                     motion, engine errored — nothing below applies and the
                     panel renders as its COMPLETE FINAL FRAME.
     .lp-b0 … .lp-bN cumulative beat classes. They only ever accumulate as
                     the sequence plays, so every rule is a plain
                     `.lp-bN .thing` with no ">= N" selector gymnastics.
                     A loop restart removes all of them at once.

   Never write a base rule that hides content pending .is-cine. The base
   IS the end state — see the header of this file.
   ═══════════════════════════════════════════════════════════════════════ */

.lp {
  --lp-in: 620ms;
  max-width: 640px;
  margin-inline: auto;
  /* Both heroes centre their contents — .page-hero (main.css:3140) and
     .hero via .hero__content. Right for a headline, wrong for a UI panel
     that happens to sit inside one. A rule matching .lp directly beats
     an inherited value whatever its specificity. */
  text-align: left;
}

/* ── Containment: this is a WIDGET, not hero copy ─────────────────────
   .page-hero p (main.css:3201) and .hero p (components.css:968) paint
   EVERY descendant paragraph white at --text-lg, capped at 640px with an
   auto margin. Inside a panel whose body is white that is not a cosmetic
   problem — it makes the answer text invisible, wraps the query pill onto
   two lines, and centres the table rows.

   Both of those are (0,1,1), and so is `.lp p`. Since process.css loads
   after main.min.css and components.min.css, the tie breaks on source
   order and this wins — no !important needed. The panel's own component
   rules are all written `.lp .lp__thing` at (0,2,0), so they in turn
   beat this reset. */
.lp p {
  color: var(--color-text-primary);
  font-size: inherit;
  max-width: none;
  margin: 0;
}

/* .pv__side-head (section 4) is authored as a <p>, so the reset above
   outranks it. Restore the properties it actually loses.

   It also switches from space-between to stacked here: in the chapters
   the label is one short word against a two-character count, but this
   panel's count carries the full "5 sources · 15,738 rows" line, which
   squeezes the label into a two-line column beside it. Stacked is also
   how the illustration this replaced set the same two lines. */
.lp .pv__side-head {
  display: block;
  /* --color-text-muted is 3.14:1 on the same white .app-window__body card
     used elsewhere — fails WCAG 4.5:1 at this size. */
  color: var(--color-text-secondary);
  font-size: 0.62rem;
  margin: 0 0 0.55rem;
}

.lp .pv__side-head .pv__count {
  display: inline-block;
  margin-top: 0.25rem;
}

/* SHADOWS components.css:1101 — `.dashboard-mockup svg` sets width:100%,
   display:block and a 40px drop-shadow. That was written for a single
   full-bleed illustration; now that the wrapper holds real markup, those
   rules would blow the 13px search glyph and the 26px hub up to the full
   panel width and give each of them its own shadow. The panel carries
   its own shadow on .app-window instead. */
.dashboard-mockup .lp svg {
  width: auto;
  height: auto;
  filter: none;
}

.lp .app-window {
  box-shadow: 0 24px 56px -16px rgba(0, 0, 0, 0.55);
}

/* Contain layout so a counter ticking or a bar growing does not
   invalidate layout for the whole hero. Not `paint` — the window carries
   a box-shadow that paint containment would clip. */
.lp .app-window { contain: layout; }

/* A running-lights pill in the title bar. The point of the whole
   exercise is "this is live", so the chrome should say so too. */
.lp .lp__live {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.32rem;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: rgba(94, 221, 180, 0.9);
}

.lp__live::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 0 rgba(61, 213, 160, 0.6);
  animation: lp-live-pulse 2.4s var(--ease-premium) infinite;
}

@keyframes lp-live-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(61, 213, 160, 0.55); }
  70%  { box-shadow: 0 0 0 7px rgba(61, 213, 160, 0); }
  100% { box-shadow: 0 0 0 0 rgba(61, 213, 160, 0); }
}

/* ── Layout: three zones, matching the illustration it replaces ────────
   Sources rail → the engine → the answer. Single column on narrow
   screens, where the engine collapses to a horizontal divider instead of
   a vertical one. */
.lp__body {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.85rem;
  padding: 0.9rem;
  align-content: start;
}

@media (min-width: 720px) {
  .lp__body {
    grid-template-columns: 0.95fr auto 1.15fr;
    gap: 0.75rem;
    padding: 1rem;
  }
}

.lp__zone { min-width: 0; }


/* ── Generic beat reveal ───────────────────────────────────────────────
   `data-at="N"` means "this element arrives at beat N". Deliberately the
   same idea as `[data-from]` in section 5, and written the same way: one
   small block of explicit selectors rather than a rule per element, so
   new panels are authored in HTML and never need CSS.

   Cumulative beat classes are what make this work — once .lp-b4 has been
   added, .lp-b1 is still on the root, so everything that has already
   arrived stays arrived. */
.is-cine [data-at] { opacity: 0; }

.is-cine.lp-b0 [data-at="0"],
.is-cine.lp-b1 [data-at="1"],
.is-cine.lp-b2 [data-at="2"],
.is-cine.lp-b3 [data-at="3"],
.is-cine.lp-b4 [data-at="4"],
.is-cine.lp-b5 [data-at="5"],
.is-cine.lp-b6 [data-at="6"],
.is-cine.lp-b7 [data-at="7"],
.is-cine.lp-b8 [data-at="8"] {
  animation: pv-row-in var(--lp-in) var(--ease-premium) both;
  animation-delay: calc(var(--i, 0) * 110ms);
}


/* ═══ ZONE 1 · the sources, and the mess in them ═══════════════════════ */

/* Rows stream in one after another rather than all at once — the single
   cheapest thing that makes a panel read as "loading real data" instead
   of "a picture of some rows". */
.is-cine.lp-b0 .lp__src {
  animation: pv-row-in var(--lp-in) var(--ease-premium) both;
  animation-delay: calc(var(--i, 0) * 130ms);
}

.is-cine:not(.lp-b0) .lp__src { opacity: 0; }

/* The three differently-spelled names. Amber because they are the
   problem, not the answer — same semantic as .pv__row--warn. */
.lp .lp__spell {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 600;
  color: #8A5A00;
  background: rgba(245, 166, 35, 0.16);
  border-radius: var(--radius-full);
  padding: 0.1rem 0.4rem;
  white-space: nowrap;
}

/* Beat 1: the three spellings notice each other. */
.is-cine.lp-b1 .lp__spell {
  animation: lp-spell-flag 900ms var(--ease-premium) both;
  animation-delay: calc(var(--i, 0) * 160ms);
}

@keyframes lp-spell-flag {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(245, 166, 35, 0); }
  45%  { transform: scale(1.11); box-shadow: 0 0 0 5px rgba(245, 166, 35, 0.22); }
  100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(245, 166, 35, 0); }
}

/* The line that says the quiet part out loud. */
.lp .lp__collide {
  margin: 0.6rem 0 0;
  padding: 0.5rem 0.6rem;
  border: 1px solid rgba(245, 166, 35, 0.42);
  border-radius: var(--radius-md);
  background: rgba(245, 166, 35, 0.09);
  font-size: 0.66rem;
  line-height: 1.5;
  color: var(--color-text-secondary);
}

.lp .lp__collide strong { color: #8A5A00; }

.is-cine:not(.lp-b1) .lp__collide { opacity: 0; transform: translateY(6px); }

.is-cine.lp-b1 .lp__collide {
  animation: pv-row-in 520ms var(--ease-premium) 480ms both;
}


/* ═══ ZONE 2 · the engine ══════════════════════════════════════════════ */

.lp__engine {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  padding: 0.2rem 0;
}

@media (min-width: 720px) {
  .lp__engine {
    flex-direction: column;
    padding: 0 0.15rem;
  }
}

.lp__hub {
  width: 54px;
  height: 54px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--navy-800);
  border: 1.5px solid var(--color-accent);
  display: flex;
  align-items: center;
  justify-content: center;
}

.lp__hub svg { width: 26px; height: 26px; display: block; }

/* Beat 2: the engine spins up as the merge happens. One rotation, eased
   — not an idle infinite spinner, which would read as "waiting". */
.is-cine.lp-b2 .lp__hub svg {
  animation: lp-hub-spin 1100ms var(--ease-premium) both;
}

@keyframes lp-hub-spin {
  from { transform: rotate(-160deg) scale(0.82); opacity: 0.5; }
  to   { transform: rotate(0deg)    scale(1);    opacity: 1; }
}

.lp .lp__engine-label {
  font-family: var(--font-mono);
  font-size: 0.53rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  text-align: center;
}

/* The payoff chip: 3 rows → 1. */
.lp .lp__merge {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.16rem 0.5rem;
  border-radius: var(--radius-full);
  background: rgba(61, 213, 160, 0.16);
  color: var(--teal-700);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  white-space: nowrap;
}

.is-cine:not(.lp-b2) .lp__merge,
.is-cine:not(.lp-b2) .lp__engine-label { opacity: 0; }

.is-cine.lp-b2 .lp__merge {
  animation: lp-merge-snap 620ms var(--ease-spring) 620ms both;
}

.is-cine.lp-b2 .lp__engine-label {
  animation: pv-row-in 460ms var(--ease-premium) 420ms both;
}

@keyframes lp-merge-snap {
  from { opacity: 0; transform: scale(0.7); }
  to   { opacity: 1; transform: scale(1); }
}

/* The dashed feed lines. Horizontal on mobile, vertical on desktop. */
.lp__feed {
  flex: 1 1 auto;
  height: 1px;
  min-width: 14px;
  background: repeating-linear-gradient(to right,
    rgba(20, 30, 69, 0.28) 0 3px, transparent 3px 8px);
}

@media (min-width: 720px) {
  .lp__feed {
    width: 1px;
    min-width: 0;
    min-height: 18px;
    background: repeating-linear-gradient(to bottom,
      rgba(20, 30, 69, 0.28) 0 3px, transparent 3px 8px);
  }
}


/* ═══ ZONE 3 · a real question, a real answer ══════════════════════════ */

.lp .lp__ask {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: #F7F8FA;
  font-size: 0.72rem;
  color: var(--color-text-primary);
  min-height: 2rem;
}

.lp__ask-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: var(--color-accent);
}

.lp__ask-icon svg { width: 100%; height: 100%; display: block; }

/* The typed text. JS empties this and refills it character by character;
   with no JS it simply renders in full. */
.lp__typed { min-width: 0; }

/* Caret: only exists while the engine is typing, so a static panel never
   shows a stray blinking bar. */
.lp__caret {
  display: none;
  width: 1px;
  height: 0.95em;
  background: var(--color-accent);
  flex-shrink: 0;
}

.is-cine.lp-b3 .lp__caret {
  display: block;
  animation: lp-caret 900ms steps(1, end) infinite;
}

.is-cine.lp-b5 .lp__caret { display: none; }

@keyframes lp-caret {
  0%, 49%   { opacity: 1; }
  50%, 100% { opacity: 0; }
}

.is-cine:not(.lp-b3) .lp__ask { opacity: 0; }
.is-cine.lp-b3 .lp__ask { animation: pv-row-in 420ms var(--ease-premium) both; }

.lp .lp__answer-label {
  display: block;
  margin: 0.7rem 0 0.3rem;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--teal-700);
}

.is-cine:not(.lp-b4) .lp__answer-label { opacity: 0; }
.is-cine.lp-b4 .lp__answer-label { animation: pv-row-in 380ms var(--ease-premium) both; }

.lp .lp__answer {
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--color-text-primary);
  font-weight: 500;
}

/* Word-by-word arrival. Authored as one <b> per word in the HTML so the
   sentence stays real, selectable, copyable text — it is only the
   ANIMATION that is per-word. */
.lp__answer b { font-weight: inherit; }

.is-cine:not(.lp-b5) .lp__answer b { opacity: 0; }

.is-cine.lp-b5 .lp__answer b {
  animation: lp-word-in 420ms var(--ease-premium) both;
  animation-delay: calc(var(--i, 0) * 52ms);
}

@keyframes lp-word-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

.lp .lp__figure {
  font-weight: 700;
  color: #8A5A00;
}

/* ── The chart ─────────────────────────────────────────────────────────
   Real elements with real labels, so the numbers scale with the reader's
   font size instead of being frozen into an SVG at 9.5px. */
.lp__chart {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  align-items: end;
  gap: 0.5rem;
  height: 96px;
  margin-top: 0.85rem;
  padding-bottom: 1.15rem;
  border-bottom: 1px solid var(--color-border);
  position: relative;
}

.lp__col {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  height: 100%;
  gap: 0.2rem;
}

.lp .lp__col-val {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--color-text-muted);
}

.lp__bar {
  width: 100%;
  max-width: 38px;
  height: var(--h, 50%);
  border-radius: 3px 3px 0 0;
  background: linear-gradient(to bottom, var(--teal-400), var(--teal-700));
  transform-origin: bottom center;
}

.lp .lp__col-label {
  position: absolute;
  bottom: 0;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  color: var(--color-text-muted);
}

/* The dip, and the month that has not happened yet. */
.lp__col--dip .lp__bar { background: var(--amber-500, #F5A623); }
.lp__col--dip .lp__col-val,
.lp__col--dip .lp__col-label { color: #8A5A00; font-weight: 700; }

.lp__col--future .lp__bar { background: rgba(20, 30, 69, 0.1); }
.lp__col--future .lp__col-label { opacity: 0.5; }

/* Beat 6: bars grow up out of the axis, left to right. scaleY rather
   than height so it stays on the compositor. */
.is-cine:not(.lp-b6) .lp__bar { transform: scaleY(0); }
.is-cine:not(.lp-b6) .lp__col-val { opacity: 0; }

.is-cine.lp-b6 .lp__bar {
  animation: lp-bar-grow 700ms var(--ease-premium) both;
  animation-delay: calc(var(--i, 0) * 110ms);
}

.is-cine.lp-b6 .lp__col-val {
  animation: pv-row-in 380ms var(--ease-premium) both;
  animation-delay: calc(var(--i, 0) * 110ms + 260ms);
}

@keyframes lp-bar-grow {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

/* Beat 7: provenance. The line that separates a usable answer from a
   confident one. */
.lp .lp__from {
  margin: 0.6rem 0 0;
  font-size: 0.62rem;
  color: var(--color-text-muted);
}

.is-cine:not(.lp-b7) .lp__from { opacity: 0; }
.is-cine.lp-b7 .lp__from { animation: pv-row-in 460ms var(--ease-premium) both; }


/* ═══ .lp--compact — the home-page hero ════════════════════════════════
   The SolmiriX panel above tells a three-zone story across 640px, which
   is right for a page about the platform. The home hero has ~468px in a
   side column and a different job: the H1 promises "answers from your own
   data, and an end to the busywork around it", i.e. BOTH tracks. So this
   variant shows one morning's queue clearing itself — four things
   arriving, each landing in the system that owns it, and one of them
   being a question that gets answered.

   Single column at every width; it is never wide enough for three. */
.lp--compact { max-width: 480px; }

.lp--compact .lp__body {
  grid-template-columns: 1fr;
  gap: 0.7rem;
  padding: 0.85rem;
}

@media (min-width: 720px) {
  .lp--compact .lp__body { grid-template-columns: 1fr; padding: 0.85rem; }
}

.lp--compact .lp__answer { font-size: 0.78rem; }
.lp--compact .lp__ask { font-size: 0.68rem; }

/* A resolved item and where it landed, tucked under its own row. */
.lp .lp__res {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0.45rem;
  margin: 0.28rem 0 0 1.6rem;
  padding: 0.3rem 0.5rem;
  border-left: 2px solid var(--color-accent);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  background: rgba(61, 213, 160, 0.08);
  font-size: 0.64rem;
}

.lp .lp__res-sys {
  font-family: var(--font-mono);
  font-size: 0.56rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--teal-700);
  white-space: nowrap;
}

.lp .lp__res-what { color: var(--color-text-secondary); }

/* The closing tally. The whole promise of the page in one line. */
.lp .lp__tally {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  margin: 0.15rem 0 0;
  padding-top: 0.55rem;
  border-top: 1px dashed var(--color-border);
  font-size: 0.66rem;
  color: var(--color-text-muted);
}

.lp .lp__tally strong {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--teal-700);
}


/* ═══ The home-page hero: on screen at EVERY width ═════════════════════
   components.css:996 sets `.hero__visual { display: none }` and only
   un-hides it above 1100px, so every phone and most tablets got no hero
   visual at all — on the page that carries the most traffic. The live
   panel is the strongest thing on the page, so below 1100px it stops
   being a decorative side element and stacks under the copy at full
   width instead of disappearing. */
@media (max-width: 1099.98px) {
  .hero:has(.hero__visual--live) { flex-wrap: wrap; }
  .hero:has(.hero__visual--live) > .container { flex: 1 1 100%; }

  .hero__visual--live {
    display: block;
    position: static;
    transform: none;
    width: auto;
    flex: 1 1 100%;
    max-width: 620px;
    margin-inline: auto;
    padding-inline: var(--container-pad);
    padding-bottom: var(--space-16);
  }

  /* The copy no longer needs to hold the full viewport on its own. */
  .hero:has(.hero__visual--live) .hero__content {
    padding-block: var(--space-20) var(--space-8);
  }
}

/* :has() is unsupported on older Safari/Firefox ESR. Without it the
   wrapper stays a flex sibling of .container and would sit beside the
   copy in a squeezed column, so fall back to the previous behaviour —
   hidden — rather than to a broken layout. The panel is decorative
   reinforcement of copy that stands on its own. */
@supports not selector(:has(*)) {
  @media (max-width: 1099.98px) {
    .hero__visual--live { display: none; }
  }
}

/* Above 1100px the existing absolute placement from components.css
   applies; the live panel just needs room to be legible. */
@media (min-width: 1100px) {
  .hero__visual--live { width: min(38vw, 468px); }
}


/* ── Reduced motion ───────────────────────────────────────────────────
   initCinematicLoop() already refuses to start under reduced motion, so
   .is-cine never lands and none of the above applies. This is the
   belt-and-braces pass for the two ambient loops that are NOT gated on
   .is-cine, plus anything a mid-session preference flip leaves behind. */
@media (prefers-reduced-motion: reduce) {
  .lp__live::before,
  .lp__caret { animation: none; }

  .lp.is-cine,
  .lp.is-cine * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════════
   12 · AI CONSULTING & TRAINING (ai-training.html)
   The course catalogue reuses .feature-card wholesale — white cards on a
   .section--dark band, the same treatment .app-window__body already gets
   on that background. Only two additions are needed, both of them the
   lines a buyer actually scans for: who the course is for, and what they
   walk out holding.
   ═══════════════════════════════════════════════════════════════════════ */

.course-who {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  font-style: italic;
}

/* The promise at the end of each card. Set apart from the syllabus above
   it, because "you leave with a working automation" is the claim the
   whole page rests on. */
.course-leave {
  margin: var(--space-4) 0 0;
  padding-top: var(--space-3);
  border-top: 1px dashed var(--color-border);
  font-size: var(--text-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text-secondary);
}

.course-leave strong { color: var(--teal-700); }

/* The two pills sit on one line above the course title. .category-pill
   and .timeline-pill are both inline-block with their own margins, so
   they need no layout here beyond a little breathing room. */
.feature-card .timeline-pill { margin-left: var(--space-2); }

/* ── The home page's third track ───────────────────────────────────────
   .track-grid (main.css:3216) is a hard two-column grid, written when
   there were exactly two ways to work with us. Consulting & Training is
   the third, so the modifier below opts that one instance into three
   without disturbing the two-column grid on any other page. The
   intermediate two-column step matters: three 1fr columns at 768px give
   each card about 230px, which is not enough for a list of three
   bullets. */
@media (min-width: 768px) {
  .track-grid--3 { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1040px) {
  .track-grid--3 { grid-template-columns: repeat(3, 1fr); }
}


/* ═══════════════════════════════════════════════════════════════════════
   13 · PHOTOGRAPHIC HEROES (.hero--photo)
   ───────────────────────────────────────────────────────────────────────
   Before this, the entire site contained exactly ONE content photograph
   (the founder shot on about.html). Every other page was type, SVG and
   card borders for seven to twenty-two screens, which is why it read as
   a wall of text no matter how good the writing was.

   The hero now leads with a real photograph and about 30 words, down
   from 106. The photo is decorative reinforcement, never the message:
   the scrim below guarantees text contrast independently of which image
   sits underneath, so swapping the photo can never break legibility.
   ═══════════════════════════════════════════════════════════════════════ */

.hero--photo {
  position: relative;
  isolation: isolate;
}

.hero--photo .hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
}

.hero--photo .hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 38%;
  display: block;
  /* Tone is NOT set here. The image is graded by .brandshot (the duotone
     utility below), and a rule at this specificity would outrank it and
     silently cancel the grade. Framing only. */
}

/* The scrim. Two layers, and the reason the copy is readable over ANY
   photograph rather than over this particular one:

     · a horizontal ramp, opaque behind the text column and clearing
       toward the right so the image is still legible as an image;
     · a vertical wash that darkens the top behind the fixed navbar and
       the bottom behind the trust strip.

   Both are pinned to --navy-900, so the hero still reads as the brand's
   dark band and not as "a stock photo with words on it". */
.hero--photo .hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom,
      rgba(13, 21, 32, 0.88) 0%,
      rgba(13, 21, 32, 0.24) 34%,
      rgba(13, 21, 32, 0.32) 70%,
      rgba(13, 21, 32, 0.86) 100%),
    linear-gradient(105deg,
      rgba(13, 21, 32, 0.95) 0%,
      rgba(13, 21, 32, 0.84) 40%,
      rgba(13, 21, 32, 0.48) 68%,
      rgba(13, 21, 32, 0.22) 100%);
}

/* A single accent sweep, so the photo picks up the brand teal instead of
   sitting there as neutral stock. Very low opacity on purpose. */
.hero--photo .hero__media::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: radial-gradient(78% 62% at 92% 12%,
    rgba(61, 213, 160, 0.13) 0%, transparent 62%);
}

/* The photo is 960px native (the largest the CC0 source offers), so at
   desktop widths it is being scaled up. Under a 0.9-opacity scrim that
   is invisible, but a hair of blur on the image itself removes any
   upscaling crunch in the highlights without costing sharpness that a
   viewer could perceive through the scrim anyway. */
@media (min-width: 1400px) {
  .hero--photo .hero__media img { filter: blur(0.3px); }
}

.hero--photo .hero__content {
  position: relative;
  z-index: 1;
  max-width: 760px;
}

/* One claim, set large. This is the whole point of the redesign: the
   headline should be readable from across a room, not a paragraph. */
.hero--photo h1 {
  font-size: clamp(2.6rem, 6.4vw, 4.4rem);
  line-height: 1.03;
  letter-spacing: -0.028em;
  margin-bottom: var(--space-5);
  text-wrap: balance;
}

.hero--photo .hero__lede {
  font-size: clamp(1.06rem, 1.9vw, 1.32rem);
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.88);
  max-width: 34em;
  margin-bottom: var(--space-8);
}

.hero--photo .hero__lede strong { color: var(--teal-300, #85E8C8); font-weight: 600; }


/* ── Brand duotone for photography ────────────────────────────────────
   Every photograph on this site is licensed stock, and licensed stock
   read straight looks like licensed stock — the single fastest way to
   make a site feel generic. Grading it into the brand's own two colours
   fixes that: the images stop being "photos someone bought" and become
   part of the same visual system as the navy bands and the teal accents.

   Standard duotone recipe, done in the compositor rather than baked into
   the files, so the palette can change without reprocessing anything:

     1 the container paints the SHADOW colour (navy)
     2 the image is desaturated and `screen`ed over it, which lifts every
       black in the photograph to navy
     3 an overlay `multiply`s the HIGHLIGHT colour (teal) over the top,
       which pulls every white down into brand green

   --duo-mix controls how far it goes, so one utility covers a subtle
   grade on a hero and a full duotone on a card. */
.brandshot {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--navy-900, #0D1520);
}

.brandshot picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* Desaturate, but keep the photograph's own tonal range intact. The
   grade is applied by the overlay below, NOT by blending the image
   itself — an earlier `screen` recipe lifted every shadow toward navy
   and then multiplied teal over the top, which blew mid-tones out to
   neon mint and made the photos unreadable. */
.brandshot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.04) brightness(1.02);
}

/* `color` takes HUE and SATURATION from this gradient and LUMINANCE from
   the photograph underneath. That is what makes it a duotone rather than
   a colour wash: darks stay dark, highlights stay bright, and only the
   hue moves into the brand's navy-to-teal ramp. --duo-mix fades the
   whole effect, so one utility covers a light grade and a full duotone. */
.brandshot::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(142deg,
    var(--navy-700, #1E2B5E) 0%,
    var(--teal-700, #237659) 52%,
    var(--teal-500, #3DD5A0) 100%);
  mix-blend-mode: color;
  opacity: var(--duo-mix, 0.85);
  pointer-events: none;
}

/* A lighter grade, for images that still need to read as a photograph of
   real people rather than as brand texture. */
.brandshot--soft { --duo-mix: 0.62; }

/* Blend modes are the one thing here with a genuine fallback need: without
   them the overlay would paint as a flat opaque slab over the photo.
   Feature-query back to a plain desaturated image, which is merely
   un-branded rather than broken. */
@supports not (mix-blend-mode: color) {
  .brandshot img { filter: none; }
  .brandshot::after { display: none; }
}


/* ── Photographic inner-page hero ─────────────────────────────────────
   .page-hero (main.css:3136) is the centred dark band every inner page
   opens with. This is the same band with a graded photograph behind it,
   so services.html and the detail pages get the same visual weight the
   home page now has, without changing their layout or their type. */
.page-hero--photo { isolation: isolate; }

.page-hero--photo .hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
}

.page-hero--photo .hero__media .brandshot,
.page-hero--photo .hero__media picture,
.page-hero--photo .hero__media img {
  width: 100%;
  height: 100%;
}

.page-hero--photo .hero__media img { object-fit: cover; object-position: center 42%; }

/* Centred copy needs a centred scrim, unlike the left-aligned home hero:
   a vertical wash plus a soft centre pool, so the middle of the band is
   always the darkest part of it whatever the photograph is doing. */
.page-hero--photo .hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(72% 68% at 50% 52%,
      rgba(13, 21, 32, 0.9) 0%, rgba(13, 21, 32, 0.66) 60%, rgba(13, 21, 32, 0.5) 100%),
    linear-gradient(to bottom,
      rgba(13, 21, 32, 0.94) 0%,
      rgba(13, 21, 32, 0.55) 34%,
      rgba(13, 21, 32, 0.62) 68%,
      rgba(13, 21, 32, 0.95) 100%);
}


/* ── The glance strip ─────────────────────────────────────────────────
   Directly under the hero, and the answer to the four questions a
   first-time visitor actually has — what do you do, how long does it
   take, what does it cost, can I trust you — none of which were
   answerable without reading prose. Four cells, no sentences. */
.glance {
  background: var(--navy-800);
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  padding-block: var(--space-8);
}

.glance__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-6) var(--space-4);
}

@media (min-width: 900px) {
  .glance__grid { grid-template-columns: repeat(4, 1fr); }
}

.glance__cell { min-width: 0; }

.glance__k {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  /* Was 0.42 alpha = 3.91:1 on navy-800, fails WCAG 4.5:1. 0.55 clears it. */
  color: rgba(255, 255, 255, 0.55);
  margin-bottom: var(--space-2);
}

.glance__v {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 2vw, 1.45rem);
  font-weight: 700;
  line-height: 1.2;
  color: var(--white);
}

.glance__v em {
  display: block;
  font-family: var(--font-body);
  font-style: normal;
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.6);
  margin-top: var(--space-1);
}


/* ═══════════════════════════════════════════════════════════════════════
   14 · SERVICE CARDS + PHOTO SECTION HEADERS
   ───────────────────────────────────────────────────────────────────────
   services.html was 2,357 words over 22.7 screens with no images at all:
   five full scroll-scrubbed pipelines stacked one after another, each
   preceded by four paragraphs. The pipelines are the best thing on the
   site — the problem was never the pipelines, it was that you had to
   read a page and a half of prose to reach the first one.

   Each pipeline now lives on its own page, and this grid is the index:
   photo, category, name, one sentence. A visitor picks in seconds
   instead of scrolling for two minutes, and each service gets a URL that
   can rank on its own name.
   ═══════════════════════════════════════════════════════════════════════ */

.svc-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
}

@media (min-width: 640px) {
  .svc-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1040px) {
  .svc-grid { grid-template-columns: repeat(3, 1fr); }
}

.svc-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  background: var(--color-bg-card);
  text-decoration: none;
  color: inherit;
  transition: transform var(--duration-lift) var(--ease-premium),
              box-shadow var(--duration-lift) var(--ease-premium),
              border-color var(--duration-lift) var(--ease-premium);
}

.svc-card:hover,
.svc-card:focus-visible {
  transform: translateY(-6px);
  border-color: var(--color-accent);
  box-shadow: 0 18px 40px -18px rgba(20, 30, 69, 0.4);
}

.svc-card__media {
  aspect-ratio: 3 / 2;
  flex-shrink: 0;
}

/* The duotone eases off on hover, so the photograph resolves into full
   colour under the pointer. Cheap, and it makes a static grid feel
   responsive without any JavaScript. */
.svc-card .brandshot { --duo-mix: 0.88; transition: none; }
.svc-card:hover .brandshot,
.svc-card:focus-visible .brandshot { --duo-mix: 0.15; }

.svc-card .brandshot img,
.svc-card .brandshot::after {
  transition: filter var(--duration-lift) var(--ease-premium),
              opacity var(--duration-lift) var(--ease-premium);
}

.svc-card__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  padding: var(--space-5) var(--space-5) var(--space-6);
}

.svc-card__body h3 {
  margin: var(--space-3) 0 var(--space-2);
  font-size: var(--text-xl);
}

.svc-card__body p {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--line-height-normal);
  color: var(--color-text-secondary);
}

.svc-card__cta {
  margin-top: auto;
  padding-top: var(--space-4);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--teal-700);
}

.svc-card:hover .svc-card__cta { color: var(--color-accent); }

/* ── Photographic band {
  position: relative;
  isolation: isolate;
  min-height: 320px;
  display: flex;
  align-items: center;
  overflow: hidden;
}


/* ═══════════════════════════════════════════════════════════════════════
   15 · THE MATURITY CHEVRONS (.maturity)
   ───────────────────────────────────────────────────────────────────────
   SolmiriX is the whole descriptive → diagnostic → predictive →
   prescriptive ladder, not "a thing you type questions into". The page
   already said so, but it said it as four numbered cards with small
   arrows between them, which reads as four features rather than as one
   escalating capability.

   Chevrons carry that meaning structurally: each stage physically slots
   into the next, so direction and progression are in the SHAPE and do
   not depend on reading the labels. The fourth is deliberately NOT a
   chevron — it is a bordered card, because it is the destination rather
   than another step on the way.
   ═══════════════════════════════════════════════════════════════════════ */

.maturity {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
  margin-top: var(--space-10);
}

@media (min-width: 560px) {
  .maturity { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1040px) {
  .maturity {
    grid-template-columns: repeat(4, 1fr);
    gap: 5px;
  }
}

.maturity__step {
  --notch: 0px;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 156px;
  padding: var(--space-5);
  border-radius: var(--radius-lg);
  background: var(--navy-700);
}

/* The arrow geometry only exists once the four sit in a single row.
   Below that they are stacked cards, where a rightward point would be
   pointing at nothing. */
@media (min-width: 1040px) {
  .maturity__step {
    --notch: 24px;
    border-radius: 0;
    padding-left: calc(var(--space-5) + var(--notch));
    padding-right: calc(var(--space-4) + var(--notch));
    clip-path: polygon(
      0 0,
      calc(100% - var(--notch)) 0,
      100% 50%,
      calc(100% - var(--notch)) 100%,
      0 100%,
      var(--notch) 50%);
  }

  /* Nothing feeds the first one, so it gets a flat back and a normal
     left edge rather than a notch waiting for an arrow that never comes. */
  .maturity__step:first-child {
    padding-left: var(--space-5);
    border-radius: var(--radius-lg) 0 0 var(--radius-lg);
    clip-path: polygon(
      0 0,
      calc(100% - var(--notch)) 0,
      100% 50%,
      calc(100% - var(--notch)) 100%,
      0 100%);
  }
}

/* Stage three is where the platform stops reporting and starts
   predicting, so the fill starts carrying brand colour. */
.maturity__step--predictive {
  background: linear-gradient(115deg, var(--navy-700) 40%, var(--teal-700) 190%);
}

/* Stage four is the outcome, not a step: a card with a border, no point,
   and the accent colour it has been building toward. */
.maturity__step--outcome {
  background: var(--navy-800);
  border: 1.5px solid var(--color-accent);
  border-radius: var(--radius-lg);
  box-shadow: 0 0 0 1px rgba(61, 213, 160, 0.14), 0 16px 40px -18px rgba(61, 213, 160, 0.45);
}

@media (min-width: 1040px) {
  .maturity__step--outcome {
    clip-path: none;
    padding-left: calc(var(--space-5) + var(--notch));
    margin-left: 4px;
  }
}

.maturity__k {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.42);
  margin-bottom: var(--space-3);
}

.maturity__step--predictive .maturity__k { color: var(--teal-300, #85E8C8); }
.maturity__step--outcome .maturity__k { color: var(--color-accent); }

.maturity__q {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  line-height: 1.25;
  color: var(--white);
}

.maturity__d {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.66);
}

/* The module each stage is actually delivered by — the line that turns a
   generic analytics diagram into a description of this product. */
.maturity__by {
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.4);
}

.maturity__step--outcome .maturity__by { color: rgba(133, 232, 200, 0.75); }


/* ═══════════════════════════════════════════════════════════════════════
   16 · PRINT
   Five 300vh sticky sections would otherwise print as pages of
   whitespace. print.min.css loads before this file, so it cannot help.
   ═══════════════════════════════════════════════════════════════════════ */

@media print {
  /* Print the finished frame, whichever beat it happened to be on. */
  .lp.is-cine,
  .lp.is-cine * {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }

  .lp__caret { display: none !important; }

  .is-scrubbed .svc-chapter__visual,
  .is-scrubbed .chapter__visual {
    position: static;
  }

  .is-scrubbed .stage-step,
  .is-scrubbed .stage-step:last-child {
    min-height: 0;
    opacity: 1;
    display: block;
  }

  .stage-chips,
  .chapter-nav,
  .scroll-progress {
    display: none;
  }

  /* Print the whole process, not whichever stage happened to be showing. */
  [data-stage] .pv__panel { display: block; }
  [data-stage] [data-from],
  [data-stage] [data-only],
  [data-stage] .doc-demo__field-value,
  [data-stage] .lead-demo__card-badge {
    opacity: 1;
    transform: none;
  }
}

/* ─────────────────────────────────────────────────────────────────
   §14  LEDE PARAGRAPH
   ─────────────────────────────────────────────────────────────────
   Replaces the "In plain terms:" label that used to open each of these
   paragraphs (6 occurrences across ai-training, service-doctrace and
   solmirix). Announcing that a sentence is plain tells the reader the
   previous one was not. The plainness is now shown typographically —
   slightly larger, slightly darker, a little more air beneath — rather
   than declared.

   SHADOWS main.css — no equivalent rule there; this is additive, so no
   main.min.css sync is required.
   ───────────────────────────────────────────────────────────────── */
.lede {
  font-size: clamp(1.05rem, 1.6vw, 1.1875rem);
  line-height: 1.55;
  color: var(--color-text-primary, #111827);
  margin-bottom: var(--space-5, 1.25rem);
  max-width: 62ch; /* measure control — the site otherwise has almost none */
}

/* Inside a dark section the primary ink would disappear. */
.section--dark .lede,
.section--darker .lede {
  color: var(--color-text-on-dark, #FFFFFF);
}

/* ─────────────────────────────────────────────────────────────────
   §15  BOOKING CALENDAR SKELETON
   ─────────────────────────────────────────────────────────────────
   Replaces the bare "Loading available times…" line that used to sit in
   front of an Apps Script cold start. A calendar-shaped placeholder keeps
   the layout stable and makes the wait legible as a calendar loading.

   Additive only — no main.css counterpart, so no min-sync needed.
   ───────────────────────────────────────────────────────────────── */
.scheduling-skeleton { padding: var(--space-2, 0.5rem) 0; }

.scheduling-skeleton__head {
  height: 20px;
  width: 42%;
  margin: 0 auto var(--space-5, 1.25rem);
  border-radius: var(--radius-sm, 4px);
  background: var(--grey-100, #EEF0F4);
}

.scheduling-skeleton__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-2, 0.5rem);
}

.scheduling-skeleton__cell {
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  background: var(--grey-100, #EEF0F4);
}

@media (prefers-reduced-motion: no-preference) {
  .scheduling-skeleton__head,
  .scheduling-skeleton__cell { animation: sched-skeleton-pulse 1.4s ease-in-out infinite; }
}

@keyframes sched-skeleton-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

/* ═════════════════════════════════════════════════════════════════
   §16  SERVICE CARD VISUALS  (.svc-viz)
   ═════════════════════════════════════════════════════════════════
   Replaces the five CC0 stock photographs that used to sit at the top
   of each service card — a handshake, a telephone keypad, network
   cables, library shelves and a desk of paperwork. Those were generic
   ("the single fastest way to make a site feel generic", per §13's own
   comment), carried alt="" so they said nothing to anyone, occupied the
   top 40% of every card, and the navy→teal duotone existed purely to
   disguise their origin — dropping to 15% opacity on hover, which
   revealed the raw stock photo to anyone who moused over a card.

   Each card now shows a small, honest interface artifact instead: the
   actual shape of what that service does. Built from CSS and text only
   — no images, no licensing, no duotone, and it stays sharp at any DPI.

   Type floor is 12px deliberately (§11 of the audit): below that a mock
   reads as an illustration of software rather than software.
   ═════════════════════════════════════════════════════════════════ */

.svc-viz {
  position: relative;
  aspect-ratio: 3 / 2;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background:
    radial-gradient(120% 100% at 100% 0%, rgba(61, 213, 160, 0.10), transparent 62%),
    var(--navy-900, #0D1520);
  font-family: var(--font-body, 'Inter', system-ui, sans-serif);
  overflow: hidden;
}

/* Top label — what screen you are looking at. */
.svc-viz__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.09);
}

.svc-viz__title {
  font-family: var(--font-mono, 'JetBrains Mono', monospace);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.72);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.svc-viz__tag {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 600;
  color: var(--teal-300, #85E8C8);
}

.svc-viz__tag::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--teal-500, #3DD5A0);
}

/* Rows — one line of real-looking work. */
.svc-viz__rows { display: flex; flex-direction: column; gap: 6px; }

.svc-viz__row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid rgba(255, 255, 255, 0.06);
  font-size: 12.5px;
  line-height: 1.25;
  color: rgba(255, 255, 255, 0.9);
}

.svc-viz__row--done {
  background: rgba(61, 213, 160, 0.11);
  border-color: rgba(61, 213, 160, 0.3);
}

.svc-viz__row--muted { opacity: 0.55; }

.svc-viz__k {
  font-family: var(--font-mono, monospace);
  font-size: 11.5px;
  color: rgba(255, 255, 255, 0.55);
  flex-shrink: 0;
}

.svc-viz__v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.svc-viz__ok {
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 700;
  color: var(--teal-300, #85E8C8);
  font-variant-numeric: tabular-nums;
}

/* Closing line — the outcome, in the site's own voice. */
.svc-viz__foot {
  margin-top: auto;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.6);
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  padding-top: 8px;
}

/* The card already lifts on hover; the visual brightens with it rather
   than un-grading a disguised photograph the way .brandshot did. */
.svc-card:hover .svc-viz,
.svc-card:focus-visible .svc-viz {
  background:
    radial-gradient(120% 100% at 100% 0%, rgba(61, 213, 160, 0.18), transparent 62%),
    var(--navy-900, #0D1520);
}

@media (max-width: 640px) {
  .svc-viz { aspect-ratio: 2 / 1; }
}

/* §17  H1 qualifier — lets a product-name H1 also carry the words a buyer
   actually searches for ("invoice data extraction"), without shouting them
   at the same weight as the product name. */
.h1-sub {
  display: block;
  margin-top: 0.3em;
  font-size: 0.42em;
  font-weight: var(--font-weight-semibold, 600);
  letter-spacing: 0;
  line-height: 1.3;
  color: var(--color-text-on-dark-muted, rgba(255,255,255,0.82));
}

/* ═════════════════════════════════════════════════════════════════
   §18  CARD HOVER — ONE VOCABULARY
   ═════════════════════════════════════════════════════════════════
   Cards a visitor scans in sequence on the same page were lifting by
   three different distances (2px / 4px / 6px) with two different shadow
   colours (neutral grey vs teal glow). The --ease-premium token's own
   comment states the goal: "so hover/entry motion feels like one hand
   designed it, not several" — and lift distance plus shadow colour are
   exactly the two things a one-hand system standardises first.

   One lift, one shadow, everywhere. Loaded after main.min.css on the
   nine pages that carry process.css; the values match what the majority
   of cards already used, so this is a convergence, not a redesign.
   ═════════════════════════════════════════════════════════════════ */
.card:hover,
.feature-card:hover,
.proof-card:hover,
.track-card:hover,
.flow-step:hover,
.service-detail-card:hover,
.svc-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-glow-accent, 0 12px 32px rgba(61, 213, 160, 0.14));
}

@media (prefers-reduced-motion: reduce) {
  .card:hover, .feature-card:hover, .proof-card:hover,
  .track-card:hover, .flow-step:hover, .service-detail-card:hover, .svc-card:hover {
    transform: none;
  }
}

/* §19  Radius consistency — two dialogs doing the same job used 24px and
   12px. Both settle on the larger, which is the site's modal convention. */
.opening-modal { border-radius: var(--radius-2xl, 24px); }

/* §20  Accent-stripe weight — the border-left pattern is 3px everywhere
   except .phase-callout, which was 4px. */
.phase-callout { border-left-width: 3px; }

/* §21  .card__icon set stroke-width 1.5 while the site standard is 2,
   so generic card icons rendered visibly lighter than every other icon. */
.card__icon svg { stroke-width: 2; }

/* §22  Garnet's own token comment reserves it for "exactly one purpose:
   the two high-intent CTA buttons... not a repaint". A demo badge had
   already taken it for a third meaning (lead heat). Moved to amber, which
   is the site's existing "warn / attention" hue. */
.pv__badge--hot {
  background: rgba(245, 166, 35, 0.16);
  color: #F0C776;
  border-color: rgba(245, 166, 35, 0.34);
}

/* §23  STAGE CHIPS — a real progress indicator
   These were aria-hidden="true" and inert, while being styled like tappable
   tabs: sighted users tapped them and nothing happened, and screen-reader
   users lost the only plain-text summary of a pipeline whose copy is 85%
   inside a scroll-scrubbed section. Now announced, with the live stage
   marked via aria-current, and visually reading as progress rather than
   as something to press. */
.stage-chips { list-style: none; }

.stage-chip {
  cursor: default;
  transition: color .2s ease, background .2s ease, border-color .2s ease;
}

.stage-chip.is-active,
.stage-chip[aria-current="step"] {
  background: rgba(61, 213, 160, 0.14);
  border-color: rgba(61, 213, 160, 0.42);
  color: var(--teal-700, #0F7A57);
  font-weight: var(--font-weight-semibold, 600);
}

.section--dark .stage-chip[aria-current="step"],
.section--darker .stage-chip[aria-current="step"] {
  color: var(--teal-300, #85E8C8);
}

/* ═════════════════════════════════════════════════════════════════
   §24  FIXED-CORNER STACKING (FAB / cookie banner / scroll-to-top)
   ═════════════════════════════════════════════════════════════════
   Three independently-positioned fixed elements shared the same
   bottom:24px band and had no awareness of each other:

     .contact-fab      bottom:24px right:24px  z-index 500
     .cookie-banner    bottom:0    full width  z-index 300  (90-130px tall on mobile)
     .scroll-to-top    bottom:24px left:24px   z-index 10

   Because the FAB has a higher z-index than the banner and a fixed
   offset, the 56px FAB circle rendered directly ON TOP of the banner's
   bottom-right action button for every first-time mobile visitor — so
   their first tap in that corner had a real chance of hitting the wrong
   control, on the one interaction that carries a consent decision.

   Fixed by lifting both corner controls above the banner while it is
   present. main.js adds .has-cookie-banner to <body> while the banner
   is showing and removes it once a choice is made.
   ═════════════════════════════════════════════════════════════════ */
body.has-cookie-banner .contact-fab,
body.has-cookie-banner .scroll-to-top {
  bottom: calc(24px + var(--cookie-banner-h, 132px));
  transition: bottom var(--transition-normal, 0.25s) ease;
}

@media (prefers-reduced-motion: reduce) {
  body.has-cookie-banner .contact-fab,
  body.has-cookie-banner .scroll-to-top { transition: none; }
}

/* ═════════════════════════════════════════════════════════════════
   §25  MOCK-UI TYPE FLOOR
   ═════════════════════════════════════════════════════════════════
   The product mocks read as diagrams rather than screenshots for a
   measurable reason: their primary row text ran 9.9–11.5px, below the
   12–13px floor real product UI (Linear, Notion, Stripe Dashboard) uses.
   Anything smaller registers — even subconsciously — as an illustration
   of an app scaled down to fit a card.

   Lifted here as an override rather than by editing ~53 scattered values,
   so it is one reviewable block and trivially reversible. Only the roles
   a user would actually READ are raised; genuinely secondary micro-labels
   (badges, axis ticks, the phone variant's cramped rows) keep their size
   so panel geometry is not disturbed.

   NEEDS VISUAL CHECK: these panels are sized to their content. Verify the
   five service pages and solmirix at 1440px and 390px before shipping.
   ═════════════════════════════════════════════════════════════════ */
.pv__row  { font-size: 0.78rem; }   /* was 0.72rem — 11.5px -> 12.5px */
.pv__kv   { font-size: 0.78rem; }   /* was 0.71rem */
.pv__dest { font-size: 0.78rem; }   /* was 0.71rem */
.pv__note { font-size: 0.75rem; }   /* was 0.66rem — 10.6px -> 12px   */
.pv__log  { font-size: 0.72rem; }   /* was 0.62rem —  9.9px -> 11.5px, monospace trace */

/* The phone mock is deliberately narrower; a smaller step keeps it from
   wrapping mid-row while still clearing the worst of the shrinkage. */
.pv-phone .pv__row { font-size: 0.72rem; }  /* was 0.66rem */

/* §26  Demo icons dropped to whatever stroke-width their source SVG
   carried, while the site standard is 2 — so demo iconography rendered
   visibly thinner than the real icons on the same page. */
.pv__row-icon svg { stroke-width: 2; }

/* §27  Named integration examples inside the tool chips. The chips used to
   carry bare categories ("CRM systems", "ERPs"), which reads as "no
   connectors built yet" and matches no query anyone actually searches.
   The example names sit as a lighter second line inside the same chip. */
.tool-chip em {
  display: block;
  margin-top: 1px;
  font-style: normal;
  font-size: 0.72em;
  font-weight: var(--font-weight-regular, 400);
  color: var(--color-text-secondary, #3D4559); /* Lighthouse: --color-text-muted (grey-400) on white was 3.14:1, below the 4.5:1 WCAG minimum. --color-text-secondary (grey-700) passes at 9.57:1. */
  letter-spacing: 0;
}

.section--dark .tool-chip em,
.section--darker .tool-chip em { color: rgba(255, 255, 255, 0.6); }

/* ═════════════════════════════════════════════════════════════════
   §28  COURSE PICKER  (ai-training.html hero)
   ═════════════════════════════════════════════════════════════════
   Replaces the animated fake-LMS window ("Day 2 of 3", "IN SESSION")
   that used to sit here — a screen the buyer never sees and does not
   buy. This is a real interactive tool: pick the situation closest to
   yours, get a genuine recommendation drawn from the six courses
   further down the page, with the reasoning shown. Nothing autoplays;
   nothing is fabricated; it only shows a result once the visitor has
   chosen one, and every recommendation text is a real, shortened
   version of that course's own outline.
   ═════════════════════════════════════════════════════════════════ */
.course-picker {
  background: var(--navy-900, #0D1520);
  border-radius: var(--radius-xl, 20px);
  padding: clamp(20px, 3vw, 32px);
  box-shadow: var(--shadow-xl, 0 24px 60px rgba(0,0,0,.28));
}

.course-picker__head { margin-bottom: 18px; }

.course-picker__eyebrow {
  display: block;
  font-size: var(--text-xs, 12px);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--teal-300, #85E8C8);
  margin-bottom: 6px;
}

.course-picker__q {
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-size: clamp(1.15rem, 2.4vw, 1.5rem);
  font-weight: 700;
  color: #fff;
  margin: 0;
}

.course-picker__options {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}

@media (max-width: 640px) {
  .course-picker__options { grid-template-columns: 1fr; }
}

.course-picker__opt {
  text-align: left;
  background: rgba(255,255,255,0.045);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  padding: 13px 15px;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, transform .15s ease;
  font-family: var(--font-body, 'Inter', sans-serif);
}

.course-picker__opt:hover {
  background: rgba(255,255,255,0.08);
  border-color: rgba(61,213,160,0.35);
}

.course-picker__opt:focus-visible {
  outline: 2px solid var(--teal-400, #5EDDB4);
  outline-offset: 2px;
}

.course-picker__opt.is-active {
  background: rgba(61,213,160,0.14);
  border-color: var(--teal-400, #5EDDB4);
}

.course-picker__opt-title {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  color: #fff;
  line-height: 1.35;
}

.course-picker__opt-sub {
  display: block;
  margin-top: 3px;
  font-size: 12px;
  color: rgba(255,255,255,0.55);
  line-height: 1.4;
}

.course-picker__result {
  margin-top: 18px;
  padding-top: 18px;
  border-top: 1px solid rgba(255,255,255,0.1);
  animation: coursePickerIn .3s ease both;
}

@keyframes coursePickerIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .course-picker__result { animation: none; }
}

.course-picker__result-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--teal-300, #85E8C8);
  margin: 0 0 4px;
}

.course-picker__result-name {
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-size: 1.15rem;
  color: #fff;
  margin: 0 0 6px;
}

.course-picker__result-why {
  font-size: 13.5px;
  line-height: 1.55;
  color: rgba(255,255,255,0.75);
  margin: 0 0 14px;
  max-width: 60ch;
}

/* ═════════════════════════════════════════════════════════════════
   §29  HOMEPAGE ROI CALCULATOR
   ═════════════════════════════════════════════════════════════════
   Replaces the four animated self-referential counters ("30-45 minutes
   per call", "<24 hours", "4 steps", "3-10 weeks") that used to sit
   directly under a line promising "no inflated stats" — undercutting
   the claim it sat beside. This is a real, live calculation on the
   visitor's own two numbers: no fabricated result, no case-study
   pretence, and it is the one screenshot-able artifact a visitor
   evaluating internally can actually take to whoever approves budget.
   ═════════════════════════════════════════════════════════════════ */
.roi-calc {
  background: var(--navy-700, #1B2A4E);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: var(--radius-xl, 20px);
  padding: clamp(22px, 3.5vw, 36px);
}

.roi-calc__head { margin-bottom: 20px; }

.roi-calc__head h3 {
  color: #fff;
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-size: clamp(1.15rem, 2.2vw, 1.4rem);
  margin: 0 0 6px;
}

.roi-calc__head p {
  color: rgba(255,255,255,0.65);
  font-size: 14px;
  margin: 0;
  max-width: 56ch;
}

.roi-calc__inputs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  margin-bottom: 22px;
}

@media (max-width: 560px) {
  .roi-calc__inputs { grid-template-columns: 1fr; }
}

.roi-calc__field { display: flex; flex-direction: column; gap: 6px; }

.roi-calc__field > span:first-child {
  font-size: 13px;
  font-weight: 600;
  color: rgba(255,255,255,0.85);
}

.roi-calc__field input {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.16);
  border-radius: 10px;
  padding: 11px 13px;
  color: #fff;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  font-family: var(--font-body, 'Inter', sans-serif);
  width: 100%;
}

.roi-calc__field input:focus-visible {
  outline: 2px solid var(--teal-400, #5EDDB4);
  outline-offset: 1px;
  border-color: transparent;
}

.roi-calc__hint {
  font-size: 11.5px;
  /* Was 0.45 alpha = 3.94:1 on navy-700, fails WCAG 4.5:1. 0.6 clears it. */
  color: rgba(255,255,255,0.6);
}

.roi-calc__result {
  background: rgba(61,213,160,0.09);
  border: 1px solid rgba(61,213,160,0.28);
  border-radius: 12px;
  padding: 16px 18px;
  margin-bottom: 16px;
}

.roi-calc__result-figure {
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-size: clamp(1.6rem, 4vw, 2.1rem);
  font-weight: 700;
  color: var(--teal-300, #85E8C8);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.roi-calc__result-figure span { font-variant-numeric: tabular-nums; }

.roi-calc__result-sub {
  margin: 6px 0 0;
  font-size: 13.5px;
  color: rgba(255,255,255,0.7);
  line-height: 1.5;
}

.roi-calc__result-sub span { font-weight: 700; color: #fff; }

.roi-calc__cta { color: #fff; border-color: rgba(255,255,255,0.4); }

/* ═══════════════════════════════════════════════════════════════════════
   17 · CLIENTS STRIP (index.html) + TRAINING LOG (ai-training.html,
        ai-training-ne.html) — populated by clients.js / training.js from
        the admin-managed Clients/Training feeds. Loaded on index.html and
        both ai-training pages, so these rules live here rather than in
        the services/solmirix-only sections above.
   ═══════════════════════════════════════════════════════════════════════ */

.clients-strip {
  padding: var(--space-12) 0;
  background: var(--grey-50, #F8F9FB);
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}

.clients-strip__label {
  text-align: center;
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold, 600);
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0 0 var(--space-6);
}

.clients-carousel { position: relative; }

.clients-carousel__track {
  display: flex;
  gap: var(--space-10);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
  padding: var(--space-2) var(--space-1);
}

.clients-carousel__track::-webkit-scrollbar { display: none; }

.clients-carousel__item {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 140px;
  height: 64px;
}

.clients-carousel__item img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  filter: grayscale(1);
  opacity: 0.6;
  transition: filter var(--transition-normal), opacity var(--transition-normal);
}

.clients-carousel__item:hover img,
.clients-carousel__item:focus-within img {
  filter: grayscale(0);
  opacity: 1;
}

.clients-carousel__nav {
  display: flex;
  justify-content: center;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

.clients-carousel__nav--prev,
.clients-carousel__nav--next {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-border);
  background: var(--color-bg-card);
  color: var(--color-text-secondary);
  cursor: pointer;
  font-size: var(--text-base);
  line-height: 1;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}

.clients-carousel__nav--prev:hover,
.clients-carousel__nav--next:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* ── Training log ──────────────────────────── */
.training-log-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-8);
}

@media (min-width: 640px) {
  .training-log-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1024px) {
  .training-log-grid { grid-template-columns: repeat(3, 1fr); }
}

.training-log-card {
  background: var(--color-bg-card);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-xl);
  padding: var(--space-6);
  box-shadow: var(--shadow-card);
  transition: all var(--duration-lift, 0.25s) var(--ease-premium, ease);
}

.training-log-card:hover {
  border-color: var(--color-accent);
  box-shadow: var(--shadow-glow-accent, var(--shadow-lg));
  transform: translateY(-4px);
}

.training-log-card__cover {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-4);
}

.training-log-card__title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  margin: 0 0 var(--space-2);
}

.training-log-card__org {
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-3);
}

.training-log-card__desc {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  line-height: 1.6;
  margin-bottom: var(--space-3);
}

.training-log-card__desc p { margin: 0 0 8px; }
.training-log-card__desc img { max-width: 100%; border-radius: var(--radius-md); }

.training-log-card__outcome {
  margin: var(--space-3) 0 0;
  padding-left: var(--space-3);
  border-left: 3px solid var(--color-accent);
  font-style: italic;
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
}
.roi-calc__cta:hover { background: rgba(255,255,255,0.1); border-color: #fff; color: #fff; }
