/* ═══════════════════════════════════════════════════════════════════
   SECTIONS + COMPONENTS  ·  pulse. landing
   Loaded by pulse-landing-new.html, which owns the tokens and the hero.

   Colours here were sampled from the PDF export, not estimated:
     ground      #0B1020
     glass fill  #131627  ==  rgba(255,255,255,.035) over the ground
     edge light  #D7DAEB top-left → #787B8C top → #474A5B bottom → 0 right
   ═══════════════════════════════════════════════════════════════════ */

/* — Layout shell ————————————————————————————————————————— */
.section { position: relative; padding: var(--section-y) 0; }

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 clamp(20px, 4vw, 64px);
}

.section-head { margin-bottom: var(--s-9); }

.pill {
  display: inline-block;
  padding: 9px var(--s-4);
  border: 1px solid color-mix(in srgb, var(--indigo-400) 55%, transparent);
  border-radius: var(--r-sm);
  font-size: 14px;
  font-weight: 500;
  color: var(--text-hi);
  background: color-mix(in srgb, var(--indigo-900) 40%, transparent);
}

/* The two-tone headline device, shared with the hero. */
.section-title {
  margin: var(--s-5) 0 0;
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: 1.06;
  letter-spacing: var(--ls-hero);
  text-wrap: balance;
  max-width: 22ch;
}
.section-title .dim { color: var(--text-dim); }
.section-title.is-centred { max-width: none; text-align: center; }

.eyebrow {
  margin: 0;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--text-low);
}

/* ═══════════════════════════════════════════════════════════════════
   GLASS
   One surface treatment, used by every panel on the page.

   The border is a gradient, not a flat stroke — that is the whole
   effect. A uniform 1px line reads as a drawn box; a border that is
   bright at the top-left and fades to nothing at the bottom-right
   reads as an edge catching light. Built with the two-background
   technique: fill painted to padding-box, edge to border-box, with
   the border itself transparent so the second gradient shows through.
   ═══════════════════════════════════════════════════════════════════ */
.glass {
  /* Default fill, sampled from the PDF: the card reads #131627 on a
     #0B1020 ground, which is white at ~3.5%. Individual components
     override it where the design sits lighter or darker. */
  --glass-fill: rgba(255, 255, 255, .035);
  position: relative;
  border: 1px solid transparent;
  border-radius: var(--r-lg);
  background:
    linear-gradient(var(--glass-fill), var(--glass-fill)) padding-box,
    linear-gradient(138deg,
      rgba(215, 218, 235, .92) 0%,
      rgba(150, 154, 172, .55) 4%,
      rgba(120, 123, 140, .42) 26%,
      rgba(90,  94, 112, .30) 58%,
      rgba(71,  74,  91, .22) 82%,
      rgba(71,  74,  91, .06) 100%) border-box;
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
}

/* Deeper frosting for the two product panels, where there is real
   light behind the glass for it to pick up. */
.glass.is-deep {
  --glass-fill: rgba(255, 255, 255, .028);
  backdrop-filter: blur(26px) saturate(1.4);
  -webkit-backdrop-filter: blur(26px) saturate(1.4);
}

.card {
  --glass-fill: rgba(255, 255, 255, .035);
  padding: var(--s-6);
  overflow: hidden;
  transition: transform .24s var(--ease-out), box-shadow .34s var(--ease-out);
}

/* The open state, from the component spec (510:5300).
   A violet wash enters from the RIGHT edge, brightest at mid-height,
   falling away to the left — not a uniform tint. Painted on a
   pseudo-element because .glass already uses both background layers
   for the fill and the gradient border. */
.card::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(126% 138% at 100% 50%,
    rgba(154, 162, 238, .50) 0%,
    rgba(104, 112, 200, .30) 32%,
    rgba(48,  56, 122, .13) 60%,
    transparent 84%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .42s var(--ease-out);
  z-index: 0;
}
.card > * { position: relative; z-index: 1; }

/* A hint of the same wash on hover, so the card previews what
   opening it will do rather than lighting up out of nowhere. */
.card:hover::after { opacity: .34; }
.card:hover { transform: translateY(-2px); }

/* Fully lit once the disclosure is open. :has() lets the parent react
   to <details> state with no JavaScript at all. */
.card:has(details[open])::after { opacity: 1; }
.card:has(details[open]) {
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, .13),
    0 26px 70px -34px rgba(96, 106, 224, .85);
}

/* ═══════════════════════════════════════════════════════════════════
   BUTTONS (page-level; the hero defines its own)
   ═══════════════════════════════════════════════════════════════════ */
.btn-outline {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-sm);
  border: 1px solid color-mix(in srgb, var(--indigo-400) 58%, transparent);
  background: color-mix(in srgb, var(--indigo-900) 34%, transparent);
  color: var(--text-hi);
  font-size: 15px;
  font-weight: 500;
  transition: border-color .2s var(--ease-out), background-color .2s var(--ease-out);
}
.btn-outline:hover {
  border-color: var(--indigo-400);
  background: color-mix(in srgb, var(--indigo-700) 30%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════
   2 — INTEGRATIONS
   ═══════════════════════════════════════════════════════════════════ */
.integrations { padding: var(--s-8) 0 var(--s-10); text-align: center; }

.integrations-title {
  margin: 0 0 var(--s-8);
  font-size: 16px;
  font-weight: 600;
  color: var(--text-hi);
}

.logo-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(var(--s-6), 5vw, var(--s-9));
}

.logo {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  height: 28px;
  /* Monochrome: six full-colour third-party marks would fight the
     ground and each other, and monochrome is the safer read of most
     brand guidelines. */
  filter: grayscale(1) brightness(0) invert(1);
  opacity: .46;
  transition: opacity .22s var(--ease-out);
}
.logo:hover { opacity: .8; }
.logo img { height: 100%; width: auto; display: block; }
.logo span { font-size: 15px; font-weight: 600; white-space: nowrap; }

/* Unbuilt marks. Deliberately styled to look unfinished so they
   cannot ship by accident. Drop the class when the asset lands. */
.logo.is-placeholder {
  filter: none;
  opacity: 1;
  height: auto;
  padding: 7px var(--s-4);
  border: 1px dashed color-mix(in srgb, var(--text-low) 55%, transparent);
  border-radius: var(--r-sm);
  color: var(--text-low);
  font-size: 13px;
  font-weight: 500;
}

/* ═══════════════════════════════════════════════════════════════════
   3 — THE PROBLEM
   ═══════════════════════════════════════════════════════════════════ */
.problem-grid {
  display: grid;
  grid-template-columns: 1fr 1.08fr;
  gap: clamp(var(--s-7), 5vw, var(--s-10));
  align-items: center;
}

/* The exported illustration replaces the CSS stand-in entirely.
   Transparent PNG, so it sits straight on the section ground. */
.orbit { position: relative; display: grid; place-items: center; }
.orbit img { width: 100%; max-width: 654px; height: auto; display: block; }

.orbit-core {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 46%;
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    radial-gradient(circle at 38% 32%,
      color-mix(in srgb, var(--indigo-400) 52%, transparent) 0%,
      color-mix(in srgb, var(--indigo-700) 60%, transparent) 42%,
      color-mix(in srgb, var(--indigo-900) 86%, transparent) 76%,
      transparent 100%);
  box-shadow:
    0 0 70px color-mix(in srgb, var(--indigo-700) 55%, transparent),
    inset 0 0 60px rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.10);
  display: grid;
  place-items: center;
}

.orbit-mini {
  width: 62%;
  border-radius: var(--r-sm);
  border: 1px solid rgba(255,255,255,.14);
  background: rgba(5,8,26,.72);
  padding: 9px;
  display: grid;
  gap: 5px;
}
.orbit-mini i { display: block; height: 4px; border-radius: 2px; background: rgba(255,255,255,.24); }
.orbit-mini i:nth-child(2) { width: 72%; }
.orbit-mini i:nth-child(3) { width: 54%; }
.orbit-mini i:nth-child(4) { width: 84%; }

/* Each role reads as a stacked pair of cards — an icon, a label, and
   two skeleton bars beneath, with a second card peeking out behind.
   That stacking is what makes them read as "a report someone owns"
   rather than a plain chip. */
.orbit-node {
  --glass-fill: rgba(255,255,255,.05);
  position: absolute;
  width: 176px;
  padding: 11px 13px 12px;
  border-radius: var(--r-md);
  color: var(--text-hi);
  box-shadow: 0 14px 34px -18px rgba(0,0,0,.95);
}
/* the card behind */
.orbit-node::before {
  content: '';
  position: absolute;
  inset: 10px -10px -10px 10px;
  border-radius: inherit;
  background: rgba(255,255,255,.028);
  border: 1px solid rgba(255,255,255,.06);
  z-index: -1;
}
.orbit-node .row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13.5px;
  font-weight: 500;
  white-space: nowrap;
}
.orbit-node .ico {
  width: 15px; height: 15px;
  flex: none;
  border-radius: 3px;
  background: linear-gradient(150deg, var(--indigo-300), var(--indigo-600));
  box-shadow: 0 0 9px color-mix(in srgb, var(--indigo-400) 70%, transparent);
}
.orbit-node i {
  display: block;
  height: 5px;
  margin-top: 7px;
  border-radius: 3px;
  background: rgba(255,255,255,.17);
}
.orbit-node i:last-child { width: 62%; }
.orbit-node.n1 { left: 0%;  top: 14%; }
.orbit-node.n2 { right: -2%; top: 40%; }
.orbit-node.n3 { left: 10%; bottom: 10%; }

.role-list { display: grid; gap: var(--s-4); }
.role-card { padding: var(--s-5) var(--s-6); }
.role-top { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4); }
.role-num { font-size: 13px; font-weight: 500; color: var(--text-low); font-variant-numeric: tabular-nums; }
.role-title { margin: var(--s-3) 0 var(--s-2); font-size: 21px; font-weight: 700; letter-spacing: -.015em; }
.role-body { margin: 0; font-size: 14.5px; line-height: 1.55; color: var(--text-mid); max-width: 62ch; }

/* Native <details> — accessible, keyboard-operable, zero JS. */
.why { margin-top: var(--s-4); }
.why > summary {
  display: inline-flex; align-items: center; gap: 7px;
  cursor: pointer; list-style: none;
  font-size: 14.5px; font-weight: 500; color: var(--text-hi);
  border-radius: var(--r-sm);
}
.why > summary::-webkit-details-marker { display: none; }
.why > summary::after {
  content: '';
  width: 8px; height: 8px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .2s var(--ease-out);
}
.why[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.why-body { margin: var(--s-3) 0 0; font-size: 14.5px; line-height: 1.6; color: var(--text-mid); max-width: 62ch; }

/* ═══════════════════════════════════════════════════════════════════
   4 — HOW IT WORKS
   ═══════════════════════════════════════════════════════════════════ */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-5); }
.step { display: flex; flex-direction: column; gap: var(--s-5); padding: var(--s-6); }
.step-title { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.015em; }
.step-body { margin: 0; font-size: 14.5px; line-height: 1.6; color: var(--text-mid); }

/* Each step carries a small diagram. Abstract on purpose — legible
   product detail at this size would be neither legible nor product. */
.step-art {
  margin-top: auto;
  position: relative;
  min-height: 190px;
  border-radius: var(--r-md);
  background:
    radial-gradient(70% 60% at 50% 108%,
      color-mix(in srgb, var(--indigo-700) 46%, transparent) 0%,
      transparent 72%);
  overflow: hidden;
  padding: var(--s-4);
  display: grid;
  align-content: start;
  gap: 9px;
}

.chip {
  --glass-fill: rgba(255,255,255,.06);
  display: flex; align-items: center; gap: var(--s-2);
  padding: 8px 11px;
  border-radius: 9px;
  font-size: 12.5px; font-weight: 500;
  color: var(--text-hi);
}
.chip i {
  width: 6px; height: 6px; border-radius: 2px; flex: none;
  background: var(--indigo-400);
  box-shadow: 0 0 8px var(--indigo-400);
}
.chip.is-wide { justify-self: stretch; }
.chip.is-half { width: 74%; }

.art-core {
  justify-self: center;
  align-self: center;
  padding: 12px 20px;
  border-radius: var(--r-pill);
  font-size: 17px; font-weight: 700; letter-spacing: -.03em;
  color: var(--text-hi);
  --glass-fill: rgba(255,255,255,.07);
  box-shadow: 0 0 44px color-mix(in srgb, var(--indigo-400) 35%, transparent);
}

.art-orb {
  justify-self: center;
  width: 132px; aspect-ratio: 1;
  border-radius: 50%;
  display: grid; place-items: center;
  text-align: center;
  font-size: 13.5px; font-weight: 600;
  color: var(--text-hi);
  border: 1px solid rgba(255,255,255,.18);
  background: radial-gradient(circle at 40% 34%,
    rgba(255,255,255,.10) 0%,
    color-mix(in srgb, var(--indigo-700) 40%, transparent) 55%,
    transparent 100%);
  box-shadow: 0 0 60px color-mix(in srgb, var(--indigo-700) 45%, transparent);
}

/* ═══════════════════════════════════════════════════════════════════
   5 + 6 — SUITE PANELS
   One component, two instances. The only difference is the list.
   ═══════════════════════════════════════════════════════════════════ */
.suite { padding: var(--s-5); border-radius: var(--r-lg); }
.suite-grid { display: grid; grid-template-columns: .82fr 1.6fr; gap: var(--s-5); }

.suite-list { display: grid; align-content: start; gap: var(--s-2); padding: var(--s-3); }
.suite-item {
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  border: 1px solid transparent;
  transition: background-color .2s var(--ease-out), border-color .2s var(--ease-out);
}
.suite-item h3 { margin: 0 0 5px; font-size: 19px; font-weight: 600; letter-spacing: -.01em; }
.suite-item p  { margin: 0; font-size: 13.5px; line-height: 1.5; color: var(--text-mid); }
.suite-item.is-active {
  border-color: rgba(255,255,255,.14);
  background: linear-gradient(120deg,
    color-mix(in srgb, var(--indigo-600) 40%, transparent) 0%,
    color-mix(in srgb, var(--indigo-900) 46%, transparent) 100%);
  box-shadow: 0 0 46px -14px color-mix(in srgb, var(--indigo-500) 70%, transparent);
}
.suite-item:not(.is-active):hover { background: rgba(255,255,255,.035); }

/* The product mockup half. Real captures exist at
   landing/assets/bento/bento-{revenue,marketing}-dark.png — but they
   use the app's cyan `ds-*` palette, which clashes with this indigo.
   Rebuilt here in the landing palette instead; swap to the real
   screenshots if you decide accuracy beats coherence. */
.suite-panel {
  display: grid;
  grid-template-columns: 168px 1fr;
  gap: 1px;
  border-radius: var(--r-md);
  overflow: hidden;
  background: rgba(255,255,255,.07);
}
.suite-rail {
  padding: var(--s-5) var(--s-4);
  display: grid; align-content: start; gap: 11px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--indigo-600) 52%, transparent),
    color-mix(in srgb, var(--indigo-800) 60%, transparent));
}
.suite-rail .mark { font-size: 20px; font-weight: 700; letter-spacing: -.045em; margin-bottom: var(--s-4); }
.suite-rail i { display: block; height: 7px; border-radius: 4px; background: rgba(255,255,255,.32); }
.suite-rail i:nth-of-type(even) { width: 78%; }

.suite-body { padding: var(--s-5); background: rgba(6,9,26,.55); display: grid; gap: var(--s-4); align-content: start; }

.kpi-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-3); }
.kpi {
  --glass-fill: rgba(255,255,255,.045);
  padding: var(--s-4);
  border-radius: var(--r-md);
}
.kpi-head { display: flex; align-items: baseline; gap: var(--s-2); margin-bottom: var(--s-3); }
.kpi-head b { font-size: 13px; font-weight: 600; }
.kpi-head span { font-size: 12px; color: var(--text-mid); }
.kpi-rule { height: 1px; background: rgba(255,255,255,.12); margin-bottom: var(--s-3); }
.kpi-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--s-3); }
.kpi-label { font-size: 12px; color: var(--text-mid); display: block; margin-bottom: 3px; }
.kpi-value { font-size: 21px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.kpi-delta {
  font-size: 11.5px; font-weight: 600;
  padding: 3px 7px; border-radius: 5px;
  color: #A9B6FF;
  background: color-mix(in srgb, var(--indigo-500) 32%, transparent);
  font-variant-numeric: tabular-nums;
}

.chart {
  --glass-fill: rgba(255,255,255,.03);
  padding: var(--s-4);
  border-radius: var(--r-md);
}
.chart-title { font-size: 13px; font-weight: 600; margin: 0 0 var(--s-4); }
.chart-plot {
  display: flex; align-items: flex-end; gap: 5px;
  height: 132px;
  padding-bottom: 2px;
  border-bottom: 1px solid rgba(255,255,255,.10);
}
.bar {
  flex: 1;
  border-radius: 2px 2px 0 0;
  background: linear-gradient(180deg, var(--indigo-400), var(--indigo-600));
  height: var(--h, 40%);
  position: relative;
}
.bar::after {
  content: '';
  position: absolute; inset: -3px 0 auto 0;
  height: 3px; border-radius: 2px;
  background: var(--cap, var(--indigo-300));
}
.chart-axis {
  display: flex; justify-content: space-between;
  margin-top: 9px;
  font-size: 10.5px;
  /* Neutral grey, never a series colour — the app's charts follow the
     same rule (frontend/CLAUDE.md §Chart standards). */
  color: #9CA3AF;
}

/* ═══════════════════════════════════════════════════════════════════
   7 — ICS
   ═══════════════════════════════════════════════════════════════════ */
/* The whole diagram is one positioned stage with an SVG spanning it,
   so the curves can start at a pill and end at the cube regardless of
   how the boxes reflow. Everything sits on the same 1400×500 grid. */
.ics-stage { position: relative; min-height: 500px; }

.ics-wires {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
.ics-wires .wire {
  fill: none;
  stroke: rgba(86, 96, 255, .34);
  stroke-width: 1.1;
}

/* The signals. Each is the same path drawn again with a short dash and
   a long gap, then the offset animated — so one bright segment runs
   the length of the wire. pathLength="100" normalises every path to
   the same nominal length, so a long curve and a short one travel at
   the same apparent speed instead of the short one whipping. */
.ics-wires .signal {
  fill: none;
  stroke: var(--indigo-400);
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-dasharray: 6 94;
  filter: drop-shadow(0 0 5px var(--indigo-400));
  animation: signal-flow var(--flow, 3.4s) linear var(--flow-delay, 0s) infinite;
}
@keyframes signal-flow { to { stroke-dashoffset: -100; } }

@media (prefers-reduced-motion: reduce) {
  .ics-wires .signal { animation: none; opacity: .55; stroke-dasharray: none; }
}

/* — Sources ————————————————————————————————————————————
   Dark pill with the light source behind its left half, exactly as
   the spec shows: the glow reads as something arriving from off-canvas
   rather than as a filled button. */
.ics-sources {
  position: absolute;
  left: 0; top: 50%;
  transform: translateY(-50%);
  width: 232px;
  display: grid;
  gap: 18px;
}
.ics-source {
  position: relative;
  padding: 13px var(--s-4);
  border-radius: 10px;
  text-align: center;
  font-size: 16px; font-weight: 500;
  color: var(--text-hi);
  background:
    radial-gradient(74% 150% at 8% 50%,
      rgba(60, 72, 236, .95) 0%,
      rgba(38, 45, 170, .70) 34%,
      rgba(16, 20, 62, .82) 68%,
      rgba(11, 16, 32, .92) 100%);
  box-shadow: 0 0 26px -10px rgba(70, 82, 240, .75);
}
/* The connection point each wire leaves from. */
.ics-source::after {
  content: '';
  position: absolute;
  right: -22px; top: 50%;
  width: 6px; height: 6px;
  margin-top: -3px;
  border-radius: 50%;
  background: #E6E9FB;
  box-shadow: 0 0 8px rgba(230, 233, 251, .8);
}

/* — Cube and orb ————————————————————————————————————————
   Both are cropped out of the supplied ics-flow.png (1490×507) with
   background-position, rather than re-drawn. Only these two regions
   are taken from the export: the wires are left to the SVG above so
   the signals can actually move — the PNG has its dashes baked in and
   a baked dash cannot flow. */
.ics-cube-wrap {
  position: absolute;
  left: 47%; top: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  justify-items: center;
  gap: var(--s-4);
}
.ics-cube {
  width: 195px; height: 240px;
  background-image: url('ics-flow.png');
  background-size: 1490px 507px;
  background-position: -615px -100px;
  background-repeat: no-repeat;
  filter: drop-shadow(0 18px 44px rgba(0,0,0,.55));
}
.ics-cube-label { font-size: 22px; font-weight: 500; color: var(--text-hi); }

.ics-orb-wrap {
  position: absolute;
  right: 3%; top: 50%;
  transform: translateY(-50%);
  display: grid; place-items: center;
}
.ics-orb {
  width: 205px; height: 205px;
  background-image: url('ics-flow.png');
  background-size: 1490px 507px;
  background-position: -1175px -103px;
  background-repeat: no-repeat;
  filter: drop-shadow(0 0 70px rgba(110, 122, 255, .35));
}

/* ═══════════════════════════════════════════════════════════════════
   8 — ABOUT
   ═══════════════════════════════════════════════════════════════════ */
.about { position: relative; overflow: hidden; text-align: center; }

/* The side lights. In the spec they bleed in from both edges at
   roughly mid-height — the section is otherwise empty, so they carry
   it. They drift inward and fade up when the section is reached
   rather than being painted on: that is the "flow" you asked for.
   Class is added by an IntersectionObserver, once, and never removed. */
.about::before,
.about::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 52vw;
  max-width: 760px;
  aspect-ratio: 1 / 1.1;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(58, 74, 226, .58) 0%,
    rgba(36, 46, 160, .32) 40%,
    rgba(16, 22, 78, .12) 68%,
    transparent 100%);
  opacity: 0;
  filter: blur(28px);
  transition: opacity 1.25s var(--ease-out), transform 1.5s var(--ease-out);
}
/* Resting fully outside, then entering by 30% of their own width —
   so the light reads as spilling in from off-canvas rather than as a
   circle parked at the edge. */
.about::before { left: 0;  transform: translate(-100%, -50%); }
.about::after  { right: 0; transform: translate( 100%, -50%); }

.about.is-visible::before { opacity: 1; transform: translate(-70%, -50%); }
.about.is-visible::after  { opacity: 1; transform: translate( 70%, -50%); }

@media (prefers-reduced-motion: reduce) {
  .about::before, .about::after { transition: none; }
  .about.is-visible::before { transform: translate(-70%, -50%); }
  .about.is-visible::after  { transform: translate( 70%, -50%); }
}

.about > .container { position: relative; z-index: 1; }

.about-title {
  margin: var(--s-5) auto 0;
  font-size: clamp(30px, 4.2vw, 62px);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: var(--ls-hero);
  max-width: 24ch;
  text-wrap: balance;
}
.about-body { margin: var(--s-5) auto var(--s-7); max-width: 62ch; font-size: var(--fs-lede); line-height: 1.6; color: var(--text-mid); }

/* ═══════════════════════════════════════════════════════════════════
   9 + 10 — ACCORDIONS
   Trust rows and FAQ tiles are the same component at two widths.
   ═══════════════════════════════════════════════════════════════════ */
.split-grid { display: grid; grid-template-columns: .8fr 1.4fr; gap: clamp(var(--s-7), 6vw, var(--s-10)); align-items: start; }

.acc-list { display: grid; gap: var(--s-3); }
.acc {
  --glass-fill: rgba(255,255,255,.035);
  border-radius: var(--r-md);
  overflow: hidden;
}
.acc > summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  cursor: pointer; list-style: none;
  font-size: 17px; font-weight: 500; color: var(--text-hi);
}
.acc > summary::-webkit-details-marker { display: none; }
.acc > summary::after {
  content: '';
  flex: none;
  width: 26px; height: 26px;
  border-radius: 50%;
  background:
    linear-gradient(currentColor, currentColor) center/11px 1.6px no-repeat,
    linear-gradient(currentColor, currentColor) center/1.6px 11px no-repeat,
    rgba(255,255,255,.16);
  color: var(--bg-void);
  transition: transform .24s var(--ease-out);
}
.acc[open] > summary::after { transform: rotate(135deg); }
.acc-body { margin: 0; padding: 0 var(--s-6) var(--s-5); font-size: 14.5px; line-height: 1.65; color: var(--text-mid); max-width: 70ch; }

.faq-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-7); flex-wrap: wrap; margin-bottom: var(--s-8); }
.faq-aside { text-align: right; display: grid; gap: var(--s-4); justify-items: end; }
.faq-aside p { margin: 0; font-size: 14px; color: var(--text-mid); max-width: 34ch; }
.faq-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); align-items: start; }

/* ═══════════════════════════════════════════════════════════════════
   11 — FINAL CTA
   ═══════════════════════════════════════════════════════════════════ */
.final {
  --glass-fill: rgba(255,255,255,.03);
  position: relative;
  overflow: hidden;
  padding: clamp(var(--s-8), 8vw, 118px) var(--s-6);
  text-align: center;
  border-radius: 26px;
  transition: transform .5s var(--ease-out), box-shadow .5s var(--ease-out);
}

/* Hover: a light rises from beneath the card and the whole panel
   lifts. Two layers — a broad bloom from the bottom edge, and a
   narrow sheen across the top — so it reads as the surface catching
   light rather than simply getting brighter. */
.final::before {
  content: '';
  position: absolute;
  inset: -40% -10% auto -10%;
  height: 180%;
  pointer-events: none;
  background:
    radial-gradient(58% 46% at 50% 100%,
      rgba(84, 96, 240, .50) 0%,
      rgba(48, 56, 168, .24) 42%,
      transparent 74%);
  opacity: .35;
  transition: opacity .5s var(--ease-out), transform .8s var(--ease-out);
  transform: translateY(6%);
}
.final::after {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg,
    transparent, rgba(255,255,255,.55) 50%, transparent);
  opacity: 0;
  transition: opacity .5s var(--ease-out);
}
.final:hover { transform: translateY(-4px); box-shadow: 0 40px 90px -50px rgba(70, 82, 236, .9); }
.final:hover::before { opacity: 1; transform: translateY(0); }
.final:hover::after  { opacity: 1; }

.final > * { position: relative; z-index: 1; }

.final-title { margin: 0; font-size: clamp(30px, 4.2vw, 66px); font-weight: 700; line-height: 1.06; letter-spacing: var(--ls-hero); }
.final-sub { margin: var(--s-4) 0 var(--s-6); font-size: var(--fs-lede); color: var(--text-mid); }
.final-note { margin: var(--s-5) auto 0; max-width: 74ch; font-size: 12.5px; line-height: 1.6; color: var(--text-low); }

/* The CTA inside picks up its own lift, so the button still reads as
   the target rather than being swallowed by the panel hover. */
.final .btn-primary { transition: transform .3s var(--ease-out), box-shadow .3s var(--ease-out), background-color .3s var(--ease-out); }
.final:hover .btn-primary {
  box-shadow: 0 0 0 1px rgba(255,255,255,.18) inset, 0 14px 40px -14px rgba(90,100,255,.95);
}

/* ═══════════════════════════════════════════════════════════════════
   12 — FOOTER
   ═══════════════════════════════════════════════════════════════════ */
/* The supplied footer-glow.png is a starfield with a planet-edge bloom.
   It anchors to the very bottom of the page and bleeds past both sides,
   so the page ends on light rather than just stopping. Sits behind the
   footer card, which keeps its own frosted fill on top. */
.footer-shell {
  position: relative;
  padding-bottom: var(--s-10);
  overflow: hidden;
  isolation: isolate;
}
.footer-glow {
  position: absolute;
  left: 50%;
  bottom: -2%;
  transform: translateX(-50%);
  width: max(1728px, 118vw);
  height: auto;
  z-index: -1;
  pointer-events: none;
  user-select: none;
  /* The export is a hard-edged rectangle; feathering the top edge stops
     a visible seam where the starfield begins. */
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 26%, #000 100%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 26%, #000 100%);
}
.footer-shell > .container { position: relative; z-index: 1; }
.footer {
  --glass-fill: rgba(255,255,255,.022);
  border-radius: 26px;
  padding: clamp(var(--s-7), 5vw, 76px) clamp(var(--s-6), 4vw, 64px) var(--s-6);
}
.footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: var(--s-7); }
.footer-brand .mark { font-size: 34px; font-weight: 700; letter-spacing: -.045em; display: block; }
.footer-brand .sub  { font-size: 9px; color: var(--text-low); display: block; margin-top: -3px; }
.footer-brand p { margin: var(--s-5) 0 0; font-size: 15px; line-height: 1.55; color: var(--text-hi); max-width: 34ch; }

.footer-col h3 { margin: 0 0 var(--s-5); font-size: 15px; font-weight: 500; color: var(--text-mid); }
.footer-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-4); }
.footer-col a { font-size: 15px; color: var(--text-hi); transition: color .18s var(--ease-out); }
.footer-col a:hover { color: var(--indigo-300); }

.footer-rule { height: 1px; background: rgba(255,255,255,.12); margin: var(--s-8) 0 var(--s-5); }
.footer-base { display: flex; flex-wrap: wrap; gap: var(--s-5); justify-content: space-between; font-size: 13.5px; color: var(--text-low); }
.footer-base a { color: var(--text-low); }
.footer-base a:hover { color: var(--text-hi); }

/* ═══════════════════════════════════════════════════════════════════
   RESPONSIVE
   The Figma frame is a fixed 1728. These are the breakpoints that
   keep it coherent everywhere else — device optimisation was on the
   open list, so this is a first pass, not a finished one.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 1100px) {
  .suite-grid { grid-template-columns: 1fr; }
  .ics-grid   { grid-template-columns: 1fr; gap: var(--s-8); }
  .ics-sources { grid-template-columns: repeat(3, 1fr); }
  .footer-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 900px) {
  .problem-grid { grid-template-columns: 1fr; }
  .orbit { min-height: 320px; }
  .steps { grid-template-columns: 1fr; }
  .split-grid { grid-template-columns: 1fr; }
  .faq-grid { grid-template-columns: 1fr; }
  .faq-aside { text-align: left; justify-items: start; }
  .section-title { max-width: none; }
  .suite-panel { grid-template-columns: 1fr; }
  .suite-rail { display: none; }
}
@media (max-width: 620px) {
  .kpi-row { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; }
  .acc > summary { padding: var(--s-4) var(--s-5); font-size: 15.5px; }
}


/* ═══════════════════════════════════════════════════════════════════
   OVERRIDES FROM THE SUPPLIED CARD SPECS
   Every value below was decoded out of the reference PNGs rather than
   eyeballed — the exports carry alpha, so what an image viewer shows
   (composited over white) is not what they look like on a dark ground.
   Appended last so these win on source order.
   ═══════════════════════════════════════════════════════════════════ */

/* — Card rest state ————————————————————————————————————
   Sampled uniform across the whole face: #E3E3E3 at 3.5%. */
.card, .glass.card { --glass-fill: rgba(227, 227, 227, .035); }

/* — Card open state ————————————————————————————————————
   NOT a single sweep. Sampled left→right it is: deep indigo at the
   left edge, falling to nothing around 42%, then rising again to a
   pale periwinkle at the right. Two lights, one from each side, with
   the middle left dark. */
.card::after {
  background: linear-gradient(90deg,
    rgba(41,  45, 129, .51)  0%,
    rgba(49,  52, 132, .33)  18%,
    rgba(227, 227, 227, .04) 42%,
    rgba(85,  92, 203, .15)  62%,
    rgba(126, 129, 220, .52) 86%,
    rgba(178, 182, 234, .48) 100%);
}
/* Contained: the wash must not bleed past the corner radius. */
.card { overflow: hidden; }

/* — Accordions (Trust + FAQ) ————————————————————————————
   Rest is effectively nothing — #000 at 0.4%, i.e. the border alone
   carries the shape. Open uses the same two-sided wash, slightly
   cooler than the problem cards. */
.acc { --glass-fill: rgba(0, 0, 0, .004); overflow: hidden; }
.acc::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity .42s var(--ease-out);
  background: linear-gradient(90deg,
    rgba(53,  60, 184, .28)  0%,
    rgba(93, 100, 201, .13)  18%,
    rgba(227, 227, 227, .04) 42%,
    rgba(71,  71, 142, .17)  62%,
    rgba(111, 113, 173, .49) 86%,
    rgba(170, 174, 209, .51) 100%);
}
.acc > * { position: relative; z-index: 1; }
.acc:hover::after { opacity: .3; }
.acc[open]::after  { opacity: 1; }

/* — How it works ————————————————————————————————————————
   Three-colour bloom behind each card — #1B1F7A, #2B33C4 and white —
   blurred at 180px and placed differently per card so the trio does
   not read as one repeated tile. Positions come in as --g1x/--g1y etc. */
.step { position: relative; overflow: hidden; }
.step::before {
  content: '';
  position: absolute;
  inset: -45%;
  pointer-events: none;
  z-index: 0;
  filter: blur(180px);
  background:
    radial-gradient(circle at var(--g1x) var(--g1y), #2B33C4 0%, transparent 58%),
    radial-gradient(circle at var(--g2x) var(--g2y), #1B1F7A 0%, transparent 62%),
    radial-gradient(circle at var(--g3x) var(--g3y), rgba(255,255,255,.85) 0%, transparent 42%);
}
.step > * { position: relative; z-index: 1; }

/* The artwork is the supplied export, anchored to the bottom of the
   card and allowed to run past the edge exactly as the spec shows. */
.step-art {
  min-height: 300px;
  border-radius: 0;
  background: none;
  padding: 0;
  overflow: hidden;
  display: block;
}
.step-art img {
  display: block;
  width: 100%;
  max-width: none;
  height: auto;
  margin-bottom: calc(var(--s-6) * -1);
}

/* — Suite panels ————————————————————————————————————————— */
.suite {
  background-image: url('suite-bg.png');
  background-size: cover;
  background-position: center;
  border: 1px solid rgba(255,255,255,.09);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Active list item, sampled opaque: darker through the middle with
   indigo lifting at both edges. */
.suite-item.is-active {
  border-color: rgba(255,255,255,.12);
  background: linear-gradient(90deg,
    #2D3156 0%, #343755 18%, #1A1E2E 42%,
    #1A1E2E 62%, #1F244D 86%, #272D73 100%);
  box-shadow: 0 0 46px -18px rgba(45, 49, 115, .9);
}

.suite-screen { display: block; width: 100%; height: auto; border-radius: var(--r-md); }

/* — Final CTA ————————————————————————————————————————————
   Hover bloom sampled strongest at the bottom edge (83%) and through
   the centre (41%), thinning at the sides — a light rising from under
   the card, not a uniform brighten. Contained by overflow. */
.final { overflow: hidden; }
.final::before {
  inset: 0;
  height: auto;
  transform: none;
  background:
    radial-gradient(86% 74% at 50% 104%,
      rgba(103, 109, 209, .83)  0%,
      rgba(87,  92, 165, .41)  34%,
      rgba(46,  48, 133, .30)  62%,
      transparent 88%);
}
.final:hover::before { transform: none; }

/* — Footer card ————————————————————————————————————————— */
.footer {
  background: rgba(0, 0, 0, .30);
  backdrop-filter: blur(60px);
  -webkit-backdrop-filter: blur(60px);
  border: 1px solid rgba(255,255,255,.08);
}


/* ═══════════════════════════════════════════════════════════════════
   LIQUID GLASS  ·  problem / trust / FAQ cards
   Spec: fill #FFFFFF at 0% · backdrop blur 180 · specular light at
   -45° at 80% · refraction 80% · depth 20% · dispersion 50% · r18.

   Nothing here is a fill — the surface is genuinely transparent, so
   the whole read comes from the edges: a specular streak along the
   -45° axis, a bright refractive rim on the lit side falling to a
   dark one opposite, and a faint chromatic split standing in for
   dispersion. Those four are what "liquid glass" actually is.
   ═══════════════════════════════════════════════════════════════════ */
.card, .glass.card, .acc, .glass.acc {
  --glass-fill: rgba(255, 255, 255, 0);
  position: relative;
  border: 0;
  border-radius: 18px;
  overflow: hidden;
  isolation: isolate;
  background: rgba(255, 255, 255, 0);
  backdrop-filter: blur(180px) saturate(1.8);
  -webkit-backdrop-filter: blur(180px) saturate(1.8);
  box-shadow:
    /* depth 20% — the card sits above the ground, lightly */
    0 16px 40px -22px rgba(0, 0, 0, .55),
    /* refraction 80% — bright rim on the lit edge, dark opposite */
    inset  1px  1px 0 rgba(255, 255, 255, .55),
    inset -1px -1px 0 rgba(255, 255, 255, .07),
    inset 0 0 26px rgba(255, 255, 255, .05),
    /* dispersion 50% — the chromatic split glass gives at its edges */
    inset  2px  2px 1px rgba(120, 190, 255, .16),
    inset -2px -2px 1px rgba(255, 150, 210, .12);
}

/* Specular streak on the -45° axis at 80%. */
.card::before, .acc::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  pointer-events: none;
  opacity: .8;
  background: linear-gradient(-45deg,
    rgba(255, 255, 255, .34) 0%,
    rgba(255, 255, 255, .10) 9%,
    rgba(255, 255, 255, 0)   26%,
    rgba(255, 255, 255, 0)   72%,
    rgba(255, 255, 255, .05) 90%,
    rgba(255, 255, 255, .20) 100%);
}

/* Three coloured discs behind the surface, lit on open. z-index 0 sits
   them under the content but inside the card; `overflow: hidden` plus
   `isolation: isolate` is what stops the bloom escaping the radius. */
.card::after, .acc::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity .5s var(--ease-out);
  filter: blur(46px);
  background:
    radial-gradient(circle 190px at 10% 52%,  #1B1F7A 0%, rgba(27,31,122,0)  72%),
    radial-gradient(circle 150px at 50% 118%, #FFFFFF 0%, rgba(255,255,255,0) 68%),
    radial-gradient(circle 210px at 90% 44%,  #2B33C4 0%, rgba(43,51,196,0)  72%);
}

.card > *, .acc > * { position: relative; z-index: 2; }

.card:hover::after, .acc:hover::after { opacity: .35; }
.card:has(details[open])::after,
.acc[open]::after { opacity: 1; }

/* The old two-sided wash and rim are superseded by the above. */
.card:has(details[open]) { box-shadow:
    0 16px 40px -22px rgba(0, 0, 0, .55),
    inset  1px  1px 0 rgba(255, 255, 255, .62),
    inset -1px -1px 0 rgba(255, 255, 255, .09),
    inset 0 0 30px rgba(255, 255, 255, .07); }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS — the export IS the card
   Each PNG already carries its own card ground and artwork, so wrapping
   it in a second surface produced a card inside a card. The step is now
   the image itself with the copy laid over the top.
   ═══════════════════════════════════════════════════════════════════ */
.step, .glass.step {
  position: relative;
  display: block;
  border: 0;
  border-radius: 18px;
  overflow: hidden;
  padding: var(--s-6);
  min-height: 572px;
  background-color: transparent;
  background-size: cover;
  background-position: center bottom;
  background-repeat: no-repeat;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}
.step::before, .step::after { content: none; }

.step-title { position: relative; z-index: 1; margin: 0 0 var(--s-4); }
.step-body  { position: relative; z-index: 1; margin: 0; max-width: 40ch; }
.step-art   { display: none; }

/* ═══════════════════════════════════════════════════════════════════
   SUITE PANELS — dashboard flush left, no gap
   ═══════════════════════════════════════════════════════════════════ */
.suite-grid { gap: 0; }
.suite-panel { gap: 0; }
.suite-screen {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0;
  object-fit: cover;
  object-position: left center;
  margin: 0;
}

/* ═══════════════════════════════════════════════════════════════════
   RHYTHM — 15px more air under every headline
   ═══════════════════════════════════════════════════════════════════ */
.section-title { margin-top: calc(var(--s-5) + 15px); }
.section-head  { margin-bottom: calc(var(--s-9) + 15px); }
.about-title   { margin-top: calc(var(--s-5) + 15px); }
.about-body    { margin-top: calc(var(--s-5) + 15px); }
.role-title    { margin-top: calc(var(--s-3) + 15px); }
.final-sub     { margin-top: calc(var(--s-4) + 15px); }


/* ═══════════════════════════════════════════════════════════════════
   RHYTHM  ·  64px between every headline and its content
   Supersedes the 15px pass above.
   ═══════════════════════════════════════════════════════════════════ */
.section-head { margin-bottom: 64px; }
.about-body   { margin-top: 64px; }
.faq-head     { margin-bottom: 64px; }
.ics-stage    { margin-top: 0; }   /* .section-head already gives the 64 */

/* ═══════════════════════════════════════════════════════════════════
   PROBLEM  ·  the illustration must not drift
   It was centre-aligned in the grid, so opening a Why card grew the
   right column and the image re-centred against it. Anchoring both
   columns to the top holds it still; sticky keeps it in view while
   the cards are read.
   ═══════════════════════════════════════════════════════════════════ */
.problem-grid { align-items: start; }
.orbit {
  position: sticky;
  top: 96px;
  align-self: start;
}

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  liquid glass, 448×572, artwork merged per corner
   Same surface spec as the problem/trust/FAQ cards. The artwork is a
   transparent PNG placed against a corner and allowed to bleed off it.
   ═══════════════════════════════════════════════════════════════════ */
.steps { align-items: start; }

.step, .glass.step {
  position: relative;
  display: block;
  width: 100%;
  max-width: 448px;
  min-height: 572px;
  height: 572px;
  padding: var(--s-6);
  border: 0;
  border-radius: 18px;
  overflow: hidden;
  isolation: isolate;
  background-color: rgba(255, 255, 255, 0);
  background-image: none;
  backdrop-filter: blur(180px) saturate(1.8);
  -webkit-backdrop-filter: blur(180px) saturate(1.8);
  box-shadow:
    0 16px 40px -22px rgba(0, 0, 0, .55),
    inset  1px  1px 0 rgba(255, 255, 255, .55),
    inset -1px -1px 0 rgba(255, 255, 255, .07),
    inset 0 0 26px rgba(255, 255, 255, .05),
    inset  2px  2px 1px rgba(120, 190, 255, .16),
    inset -2px -2px 1px rgba(255, 150, 210, .12);
}

/* The bloom, positioned per card — same idea as the reference sheet,
   where each of the three carries its light in a different place. */
.step::before {
  content: '';
  position: absolute;
  inset: -30%;
  z-index: 0;
  pointer-events: none;
  filter: blur(180px);
  background:
    radial-gradient(circle at var(--g1x) var(--g1y), #2B33C4 0%, rgba(43,51,196,0) 58%),
    radial-gradient(circle at var(--g2x) var(--g2y), #1B1F7A 0%, rgba(27,31,122,0) 62%),
    radial-gradient(circle at var(--g3x) var(--g3y), rgba(255,255,255,.85) 0%, rgba(255,255,255,0) 42%);
}

/* Specular streak, matching the other glass surfaces. */
.step::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  pointer-events: none;
  opacity: .8;
  background: linear-gradient(-45deg,
    rgba(255,255,255,.34) 0%,
    rgba(255,255,255,.10) 9%,
    rgba(255,255,255,0)   26%,
    rgba(255,255,255,0)   72%,
    rgba(255,255,255,.05) 90%,
    rgba(255,255,255,.20) 100%);
}

.step-title { position: relative; z-index: 2; margin: 0 0 var(--s-4); }
.step-body  { position: relative; z-index: 2; margin: 0; max-width: 38ch; }

/* Artwork layer. Corner anchoring per card, bleeding off the edge. */
.step-art {
  display: block;
  position: absolute;
  z-index: 1;
  pointer-events: none;
}
.step-art img { display: block; width: 100%; height: auto; }

.step-art.at-br { right: 0;  bottom: 0; width: 426px; }
.step-art.at-bl { left: 0;   bottom: 0; width: 415px; }
.step-art.at-c  { left: 50%; top: 58%; width: 224px; transform: translate(-50%, -50%); }

/* ═══════════════════════════════════════════════════════════════════
   SUITE  ·  dashboard merged to the right edge
   ═══════════════════════════════════════════════════════════════════ */
.suite-grid { grid-template-columns: .78fr 1.7fr; gap: 0; align-items: center; }
.suite-panel { display: block; background: none; border-radius: 0; overflow: visible; }
.suite-rail { display: none; }
.suite-screen {
  display: block;
  width: 100%;
  height: auto;
  margin-right: calc(var(--s-5) * -1);
  border-radius: var(--r-md) 0 0 var(--r-md);
  object-fit: cover;
  object-position: left center;
}


/* ═══════════════════════════════════════════════════════════════════
   FINAL SPACING PASS
   ═══════════════════════════════════════════════════════════════════ */
#ics .section-head { margin-bottom: 20px; }
.about-body        { margin-bottom: 10px; }

/* ═══════════════════════════════════════════════════════════════════
   ICS DOTS
   Promoted from ::after to real elements. A pseudo-element cannot be
   measured by getBoundingClientRect, and the wires have to start from
   wherever these actually land — see the layout pass in the page JS.
   ═══════════════════════════════════════════════════════════════════ */
.ics-source { position: relative; }
.ics-source::after { content: none; }

.ics-dot {
  position: absolute;
  right: -22px;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -3px;
  border-radius: 50%;
  background: #E6E9FB;
  box-shadow: 0 0 8px rgba(230, 233, 251, .85);
}


/* ═══════════════════════════════════════════════════════════════════
   FOLD RHYTHM  ·  20px, and one uninterrupted ground
   ═══════════════════════════════════════════════════════════════════ */
.hero { background: var(--bg-page) !important; }
.hero::after { display: none; }   /* the faint grid was the other seam */

.section-head,
#ics .section-head,
.faq-head { margin-bottom: 20px; }

.section-title { margin-top: 20px; }

/* How it works — 480px */
.step, .glass.step { height: 480px; min-height: 480px; }
.step-art.at-c { top: 62%; }

/* Who built this — 10px between every element */
.about-title { margin-top: 10px; }
.about-body  { margin-top: 10px; margin-bottom: 10px; }

/* Footer — 10px off the bottom */
.footer-shell { padding-bottom: 10px; }


/* ═══════════════════════════════════════════════════════════════════
   BOOK A DEMO  ·  same surface as the FAQ cards
   Identical liquid-glass build to .acc — transparent fill, blur 180,
   specular streak on the -45° axis, refractive rim, dispersion at the
   edges. The difference is the trigger: the FAQ card lights on open,
   this one lights on hover alone.
   ═══════════════════════════════════════════════════════════════════ */
.final {
  position: relative;
  border: 0;
  border-radius: 18px;
  overflow: hidden;
  isolation: isolate;
  background: rgba(255, 255, 255, 0);
  backdrop-filter: blur(180px) saturate(1.8);
  -webkit-backdrop-filter: blur(180px) saturate(1.8);
  box-shadow:
    0 16px 40px -22px rgba(0, 0, 0, .55),
    inset  1px  1px 0 rgba(255, 255, 255, .55),
    inset -1px -1px 0 rgba(255, 255, 255, .07),
    inset 0 0 26px rgba(255, 255, 255, .05),
    inset  2px  2px 1px rgba(120, 190, 255, .16),
    inset -2px -2px 1px rgba(255, 150, 210, .12);
  transition: box-shadow .45s var(--ease-out);
}

/* Three coloured discs behind the surface. z-index 0 keeps them under
   the copy; overflow + isolation is what holds the bloom inside the
   radius so nothing spills past the corners. */
.final::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  height: auto;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transform: none;
  transition: opacity .5s var(--ease-out);
  filter: blur(46px);
  background:
    radial-gradient(circle 240px at 10% 54%,  #1B1F7A 0%, rgba(27,31,122,0)   72%),
    radial-gradient(circle 200px at 50% 116%, #FFFFFF 0%, rgba(255,255,255,0) 68%),
    radial-gradient(circle 260px at 90% 46%,  #2B33C4 0%, rgba(43,51,196,0)   72%);
}

/* Specular streak, matching every other glass surface on the page. */
.final::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  height: auto;
  border-radius: inherit;
  pointer-events: none;
  opacity: .8;
  background: linear-gradient(-45deg,
    rgba(255,255,255,.34) 0%,
    rgba(255,255,255,.10) 9%,
    rgba(255,255,255,0)   26%,
    rgba(255,255,255,0)   72%,
    rgba(255,255,255,.05) 90%,
    rgba(255,255,255,.20) 100%);
}

.final > * { position: relative; z-index: 2; }

.final:hover { transform: none; }
.final:hover::before { opacity: 1; transform: none; }
.final:hover::after  { opacity: .8; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  510px, artwork at 70%
   ═══════════════════════════════════════════════════════════════════ */
.step, .glass.step { height: 510px; min-height: 510px; }

/* 426 × .7 = 298 and 415 × .7 = 291 — both still anchored to their
   corner and allowed to bleed off it. */
.step-art.at-br { width: 298px; right: 0; bottom: 0; }
.step-art.at-bl { width: 291px; left: 0;  bottom: 0; }

/* Card three drops out of absolute positioning so it sits in flow,
   40px below the copy — a fixed offset cannot hold that gap when the
   body wraps to a different number of lines. */
.step-art.at-c {
  position: static;
  width: 224px;
  margin: 40px auto 0;
  left: auto; top: auto;
  transform: none;
}


/* ═══════════════════════════════════════════════════════════════════
   80px BETWEEN SECTIONS
   Padding on one side only. Splitting it 10/10 would collapse to the
   right total but leave every section's own top and bottom uneven,
   and the first fold would carry a stray 10px against the hero.
   ═══════════════════════════════════════════════════════════════════ */
.section        { padding: 80px 0 0; }
.integrations   { padding: 80px 0 0; }
.footer-shell   { padding-top: 80px; padding-bottom: 10px; }
.section:first-of-type { padding-top: 80px; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  artwork sits ON the bottom edge
   The card carries 32px of padding, and an absolutely positioned child
   offsets from the PADDING box — so bottom:0 was landing the artwork
   32px short of the card edge. Negative offsets cancel that and put it
   flush against the bottom, where the spec has it.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br {
  bottom: calc(var(--s-6) * -1);
  right:  calc(var(--s-6) * -1);
  top: auto; left: auto;
  width: 298px;
  transform: none;
}
.step-art.at-bl {
  bottom: calc(var(--s-6) * -1);
  left:   calc(var(--s-6) * -1);
  top: auto; right: auto;
  width: 291px;
  transform: none;
}
.step-art img { display: block; width: 100%; height: auto; margin: 0; }


/* ═══════════════════════════════════════════════════════════════════
   PROBLEM ILLUSTRATION  ·  genuinely fixed in place
   It was `position: sticky`, which pins then travels with the scroll —
   movement by definition. Back to static, anchored to the top of its
   grid row, so opening a Why card grows the right column without the
   image shifting at all.
   ═══════════════════════════════════════════════════════════════════ */
.problem-grid { align-items: start; }
.orbit {
  position: relative;
  top: auto;
  align-self: start;
  display: grid;
  place-items: start center;
}

/* ═══════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  200px padding
   ═══════════════════════════════════════════════════════════════════ */
.about { padding: 200px 0; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  cards 1 and 2 artwork up 20%
   298 → 358 and 291 → 349, both still hard against the bottom edge.
   The negative offsets cancel the card's 32px padding, since an
   absolutely positioned child measures from the padding box.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br {
  width: 358px;
  bottom: calc(var(--s-6) * -1);
  right:  calc(var(--s-6) * -1);
  top: auto; left: auto;
  transform: none;
}
.step-art.at-bl {
  width: 349px;
  bottom: calc(var(--s-6) * -1);
  left:   calc(var(--s-6) * -1);
  top: auto; right: auto;
  transform: none;
}


/* ═══════════════════════════════════════════════════════════════════
   120px THROUGH THE CLOSING RUN
   Trust → FAQ → Book a demo → Footer. These four are all cards on an
   empty ground with no illustration between them, so the 80px used
   higher up reads as cramped here. Applied as top padding on each, so
   the gap between any two of them is exactly 120.
   ═══════════════════════════════════════════════════════════════════ */
#trust        { padding-top: 120px; }
#faqs         { padding-top: 120px; }
#demo         { padding-top: 120px; }
.footer-shell { padding-top: 120px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS CUBE  ·  animated GIF replaces the cropped still
   Sits above the wire SVG in stacking order, so the lines running to
   its centre are hidden behind it — they read as entering the object
   rather than stopping short of it.
   ═══════════════════════════════════════════════════════════════════ */
.ics-cube {
  display: block;
  width: 260px;
  height: 260px;
  background: none;
  filter: drop-shadow(0 18px 44px rgba(0, 0, 0, .55));
}
.ics-cube-wrap { z-index: 2; }
.ics-orb-wrap  { z-index: 2; }
.ics-wires     { z-index: 0; }


/* ═══════════════════════════════════════════════════════════════════
   ICS CUBE  ·  no plate behind it
   The GIF carries its own alpha (green keyed out), so the wrapper must
   not paint anything of its own — otherwise the transparency has a
   background to reveal.
   ═══════════════════════════════════════════════════════════════════ */
.ics-cube,
.ics-cube-wrap {
  background: none;
  background-color: transparent;
  background-image: none;
  border: 0;
  box-shadow: none;
  -webkit-mask: none;
  mask: none;
}
.ics-cube { filter: drop-shadow(0 18px 44px rgba(0, 0, 0, .5)); }


/* ═══════════════════════════════════════════════════════════════════
   STICKY HEADER
   The nav now sits outside .hero. It could not stick from inside:
   .hero sets overflow:hidden to contain the tiles, and an
   overflow-clipped ancestor becomes the sticky element's scroll
   container — so it would have pinned to the hero, not the viewport.
   ═══════════════════════════════════════════════════════════════════ */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(8, 11, 28, .72);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-bottom: 1px solid rgba(255, 255, 255, .07);
}

/* The hero begins under the bar rather than after it, so the fold
   still fills the viewport and the composition is unchanged. */
.hero { margin-top: -84px; padding-top: 84px; }

/* ═══════════════════════════════════════════════════════════════════
   HERO HEADLINE  ·  flat colours, gradient removed
   ═══════════════════════════════════════════════════════════════════ */
.headline {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: #E4E5FB;
}
.headline .dim { color: rgba(228, 229, 251, .5); }

/* ═══════════════════════════════════════════════════════════════════
   PROBLEM CARDS  ·  10px between every text element
   ═══════════════════════════════════════════════════════════════════ */
.role-card  { gap: 10px; }
.role-title { margin: 10px 0 0; }
.role-body  { margin: 10px 0 0; }
.why        { margin-top: 10px; }
.why-body   { margin-top: 10px; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  490px, card three tightened to 12px
   ═══════════════════════════════════════════════════════════════════ */
.step, .glass.step { height: 490px; min-height: 490px; }
.step-art.at-c     { margin-top: 12px; }

/* ═══════════════════════════════════════════════════════════════════
   TRUST  ·  hidden for now
   Display-only, so the markup and its copy stay in place for whenever
   it comes back.
   ═══════════════════════════════════════════════════════════════════ */
#trust { display: none; }


/* ═══════════════════════════════════════════════════════════════════
   ONE FIXED ATMOSPHERE FOR THE WHOLE PAGE

   The ambient light was painted inside .hero, so it scrolled away with
   the fold and every section below it sat on flat ground. It is now a
   viewport-fixed layer on <body>: the light holds still while the
   copy, the tiles and every section travel over it.

   `position: fixed` rather than `background-attachment: fixed` — the
   latter is expensive to repaint on scroll and is ignored outright on
   most mobile browsers. A fixed pseudo-element is composited on the
   GPU and behaves identically everywhere.

   z-index -1 puts it behind page content but still above the body's
   own background colour, so --bg-page shows through the transparent
   parts of the gradients.
   ═══════════════════════════════════════════════════════════════════ */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(62% 46% at 50% 22%,
      color-mix(in srgb, var(--indigo-700) 58%, transparent) 0%,
      color-mix(in srgb, var(--indigo-800) 24%, transparent) 44%,
      transparent 74%),
    radial-gradient(40% 32% at 74% 8%,
      color-mix(in srgb, var(--indigo-600) 26%, transparent) 0%,
      transparent 70%),
    radial-gradient(52% 40% at 16% 84%,
      color-mix(in srgb, var(--indigo-800) 30%, transparent) 0%,
      transparent 72%);
  filter: blur(8px);
}

/* The hero keeps its tiles and its copy, but no longer paints light of
   its own — otherwise the two would stack and the fold would read
   brighter than everything under it. */
.hero::before { content: none; }
.hero {
  background:
    radial-gradient(90% 95% at 88% 6%,   #CDD3F2 0%, transparent 58%),
    radial-gradient(70% 80% at 12% 4%,   #FBEFF3 0%, transparent 55%),
    radial-gradient(105% 110% at 6% 96%, #D5DAF4 0%, transparent 62%),
    radial-gradient(85% 90% at 100% 78%, #E9E4F5 0%, transparent 50%),
    linear-gradient(118deg, #EDEFFB 0%, #F5F5FD 42%, #FFFAFA 70%, #E8EBF9 100%) !important;
}

/* Sections must stay transparent so the fixed layer reads through. */
.section, .integrations, .footer-shell { background: transparent; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  headings only, 420px
   ═══════════════════════════════════════════════════════════════════ */
.step, .glass.step { height: 420px; min-height: 420px; }
.step-body { display: none; }
.step-title { margin: 0 0 12px; }
.step-art.at-c { margin-top: 12px; }

/* ═══════════════════════════════════════════════════════════════════
   SUITE  ·  the list is interactive now
   ═══════════════════════════════════════════════════════════════════ */
.suite-item[data-screen] { cursor: pointer; }
.suite-item[data-screen]:focus-visible {
  outline: 2px solid var(--indigo-400);
  outline-offset: 2px;
}
.suite-screen { transition: opacity .2s var(--ease-out); }

/* The ICS cube is animated again — no plate behind it. */
.ics-cube { width: 260px; height: 260px; background: none; }


/* ═══════════════════════════════════════════════════════════════════
   LOTTIE MOUNT
   Aspect ratio reserved up front so the section does not reflow when
   the animation loads — the layout shift would otherwise land right
   as the reader arrives at it.
   ═══════════════════════════════════════════════════════════════════ */
.lottie {
  width: 100%;
  max-width: 654px;
  aspect-ratio: 1 / 1;
  margin: 0 auto;
}
.lottie svg { display: block; width: 100% !important; height: 100% !important; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  380px, card three lifted off the bottom
   ═══════════════════════════════════════════════════════════════════ */
.step, .glass.step { height: 380px; min-height: 380px; }

.step-art.at-c {
  position: absolute;
  left: 50%;
  bottom: 40px;
  top: auto;
  margin: 0;
  width: 224px;
  transform: translateX(-50%);
}


/* ═══════════════════════════════════════════════════════════════════
   FLAT GLASSMORPHISM
   The specular streak, the refractive rim and the chromatic dispersion
   all come off. What is left is the honest version: a translucent
   fill, a real backdrop blur, and one quiet hairline border.
   ═══════════════════════════════════════════════════════════════════ */
.card, .glass.card,
.acc,  .glass.acc,
.step, .glass.step,
.final {
  border: 1px solid rgba(255, 255, 255, .09);
  box-shadow: none;
}

/* The reflection lived on ::before for the cards and the steps, and on
   ::after for the final CTA. All of them go. */
.card::before,
.acc::before,
.step::after,
.final::after { content: none; }

/* Keep a little fill so the surface still reads as a surface once the
   highlight is gone — with a 0% fill and no rim there would be nothing
   left but the border. */
.card, .glass.card,
.acc,  .glass.acc { background: rgba(255, 255, 255, .028); }
.step, .glass.step { background: rgba(255, 255, 255, .028); }
.final { background: rgba(255, 255, 255, .028); }

.card:has(details[open]),
.acc[open] { box-shadow: none; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  30px under the heading on card three
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-c {
  position: static;
  left: auto; bottom: auto; top: auto;
  transform: none;
  margin: 30px auto 0;
  width: 224px;
}

/* The problem illustration is a plain image again. */
.orbit img { width: 100%; max-width: 654px; height: auto; display: block; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  new artwork, same three positions
   Widths follow the new intrinsic sizes so nothing is up-scaled:
   430 / 453 / 448. Anchors are unchanged — card one bottom-right,
   card two bottom-left, card three in flow under the heading.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br { width: 430px; }
.step-art.at-bl { width: 453px; }
.step-art.at-c  { width: 448px; max-width: 100%; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  re-exported artwork, positions unchanged
   Widths track the new intrinsic sizes so nothing is up-scaled.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br { width: 430px; }
.step-art.at-bl { width: 453px; }
.step-art.at-c  { width: 448px; max-width: 100%; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card three
   80px under the heading, and flush to the left edge. The negative
   left margin cancels the card's own 32px padding — an in-flow child
   sits inside the padding box, so without it the image would stop
   32px short of the edge.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-c {
  position: static;
  left: auto; right: auto; bottom: auto; top: auto;
  transform: none;
  width: 448px;
  max-width: calc(100% + var(--s-6));
  margin: 80px 0 0 calc(var(--s-6) * -1);
}


/* ═══════════════════════════════════════════════════════════════════
   WORDMARK  ·  header + footer
   The supplied file was 4501x4501 with the mark floating in the middle
   — 70% of it empty canvas. Cropped to the alpha bounds and scaled to
   900x441, so the visible edges are the real edges and it can be
   aligned like any other element.
   ═══════════════════════════════════════════════════════════════════ */
.brand-logo {
  display: block;
  height: 42px;
  width: auto;
}
.footer-logo {
  display: block;
  height: 58px;
  width: auto;
  margin-bottom: var(--s-2);
}

/* The link is the label now; the img carries the alt text. */
.brand { display: block; line-height: 0; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card three, 130px under the heading
   80px plus the 50px you asked to add.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-c { margin-top: 130px; }


/* ═══════════════════════════════════════════════════════════════════
   INTEGRATIONS  ·  one continuous line
   The set is rendered twice and the track translated by exactly -50%,
   so at the end of a cycle the second copy sits precisely where the
   first began — the loop has no seam and no jump.
   ═══════════════════════════════════════════════════════════════════ */
.logo-marquee {
  position: relative;
  overflow: hidden;
  /* Fades both edges. A mask rather than an overlaying gradient, so it
     works whatever is behind — the fixed atmosphere layer means a
     solid-colour fade would show as two dark blocks. */
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0, #000 10%, #000 90%, transparent 100%);
  mask-image: linear-gradient(90deg,
    transparent 0, #000 10%, #000 90%, transparent 100%);
}

.logo-track {
  display: flex;
  width: max-content;
  animation: logo-marquee var(--marquee-duration, 38s) linear infinite;
}
.logo-set {
  display: flex;
  align-items: center;
  flex: none;
  gap: clamp(var(--s-7), 6vw, var(--s-9));
  padding-right: clamp(var(--s-7), 6vw, var(--s-9));
}

@keyframes logo-marquee { to { transform: translateX(-50%); } }

/* Pause on hover so a logo can actually be read. */
.logo-marquee:hover .logo-track { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .logo-track { animation: none; }
  .logo-marquee { overflow-x: auto; }
}

.logo-row { display: none; }
.logo { flex: none; }

/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card three, +30px
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-c { margin-top: 160px; }


/* ═══════════════════════════════════════════════════════════════════
   SUITE VIEWS  ·  stacked panels, one shown at a time
   ═══════════════════════════════════════════════════════════════════ */
.suite-views { position: relative; }
.suite-view  { display: none; }
.suite-view.is-shown { display: block; }

/* ═══════════════════════════════════════════════════════════════════
   BUILT SCREEN  ·  Rate insights
   Metrics and compset come from the app's own RateCalendar view, so
   the numbers are the product's, not invented for the page.
   ═══════════════════════════════════════════════════════════════════ */
.screen {
  border-radius: var(--r-md);
  background: rgba(6, 9, 26, .55);
  border: 1px solid rgba(255, 255, 255, .08);
  padding: var(--s-5);
  display: grid;
  gap: var(--s-4);
}
.screen-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4); }
.screen-title { font-size: 15px; font-weight: 600; }
.screen-chip {
  font-size: 11.5px; color: var(--text-mid);
  padding: 4px 9px; border-radius: 6px;
  background: rgba(255,255,255,.05);
}

.screen-kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-3); }
.skpi {
  padding: var(--s-4);
  border-radius: var(--r-md);
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(255,255,255,.07);
  display: grid; gap: 4px;
}
.skpi-l { font-size: 11.5px; color: var(--text-mid); }
.skpi-v { font-size: 21px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.skpi-d { font-size: 11px; color: var(--text-low); font-variant-numeric: tabular-nums; }
.skpi-d.is-up { color: #A9B6FF; }

.screen-table { border-radius: var(--r-md); overflow: hidden; border: 1px solid rgba(255,255,255,.07); }
.strow {
  display: grid;
  grid-template-columns: 84px 1fr 1fr 1fr 1fr 1fr 78px;
  gap: var(--s-2);
  padding: 9px var(--s-4);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--text-mid);
  border-bottom: 1px solid rgba(255,255,255,.05);
}
.strow > span:not(:first-child) { text-align: right; }
.strow.is-head {
  font-size: 10.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-low);
  background: rgba(255,255,255,.035);
}
/* Our own rate is the column the reader is comparing from — the app
   tints it #2D4DE0 for the same reason. */
.strow .is-ours { color: #A9B6FF; font-weight: 600; }
.strow.is-head .is-ours { color: #8E9BF5; }
.strow.is-foot {
  border-bottom: 0;
  background: rgba(255,255,255,.035);
  font-weight: 600;
  color: var(--text-hi);
}
.d-up { color: #7FD9A6; }
.d-dn { color: #F0857A; }

@media (max-width: 1100px) {
  .strow { grid-template-columns: 70px 1fr 1fr 1fr 68px; }
  .strow > span:nth-child(5), .strow > span:nth-child(6) { display: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   DASHBOARD SCREEN  ·  Groups & displacement
   The shell — sidebar, gradient, logo, nav placeholders, divider — is
   the supplied UI CARD.png used as a background image, so it is
   preserved byte-for-byte rather than approximated. Only the main area
   is built, and it sits inside the 20.6% the sidebar occupies.

   Accents are the app's own ds-* tokens rather than the landing
   palette: blue #22D3EE, green #34D399, coral #F87171 — so the card
   reads as a screen from the product, not a page element.

   Everything scales in cqw units against the card itself, so the
   density holds at any panel width instead of only at 920px.
   ═══════════════════════════════════════════════════════════════════ */
.dash {
  position: relative;
  aspect-ratio: 920 / 504;
  border-radius: var(--r-md);
  overflow: hidden;
  background: url('dash-shell.png') center / 100% 100% no-repeat;
  container-type: inline-size;
  font-variant-numeric: tabular-nums;
}

/* 190/920 = 20.65% — the sidebar's exact share of the shell. */
.dash-main {
  position: absolute;
  inset: 0 0 0 20.65%;
  padding: 2.4cqw 2.6cqw;
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 1.6cqw;
  overflow: hidden;
}

.dash-kpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.2cqw; }

.dkpi, .dmet, .dash-panel {
  border-radius: 1cqw;
  background: rgba(255, 255, 255, .04);       /* ds-card */
  border: 1px solid rgba(255, 255, 255, .07); /* ds-border */
}

.dkpi { padding: 1.2cqw 1.4cqw; display: grid; gap: .25cqw; align-content: start; }
.dkpi-l { font-size: 1.35cqw; font-weight: 600; color: #E8EEFF; }
.dkpi-s { font-size: 1.05cqw; color: #8892AA; }
.dkpi-v { font-size: 2.6cqw; font-weight: 700; letter-spacing: -.02em; color: #E8EEFF; margin-top: .5cqw; }
.dkpi-c { font-size: 1cqw; color: #8892AA; }
.dkpi-c b { font-weight: 600; }
.dkpi-c .up { color: #34D399; }
.dkpi-c .dn { color: #F87171; }

.dash-panel { padding: 1.3cqw 1.5cqw; display: grid; gap: 1cqw; align-content: start; }
.dash-ph { display: flex; align-items: center; gap: .7cqw; font-size: 1.35cqw; font-weight: 600; color: #E8EEFF; }
.dash-ph .info {
  width: 1.5cqw; height: 1.5cqw;
  border-radius: 50%;
  border: 1px solid rgba(255,255,255,.28);
  color: #8892AA;
  font-size: 1cqw; font-style: normal;
  display: grid; place-items: center;
}

/* — Stacked bars ———————————————————————————————————————— */
.dash-chart { display: grid; grid-template-columns: 3.2cqw 1fr; gap: .8cqw; height: 13cqw; }
.dax { display: flex; flex-direction: column; justify-content: space-between; font-size: .95cqw; color: #8892AA; text-align: right; }
.dplot {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1cqw;
  /* Subtle gridlines, matching the app's chart treatment. */
  background: repeating-linear-gradient(180deg,
    rgba(255,255,255,.07) 0 1px, transparent 1px 25%);
}
.dcol { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; }
.dcol i { display: block; height: var(--h); border-radius: .2cqw .2cqw 0 0; }
.dcol i + i { border-radius: 0; }
.dcol i:last-child { border-radius: 0 0 .2cqw .2cqw; }
.dcol .s1 { background: #22D3EE; }   /* On the books */
.dcol .s2 { background: #34D399; }   /* Definite     */
.dcol .s3 { background: #F87171; }   /* Tentative    */

.dlabels { display: flex; justify-content: space-between; padding-left: 4cqw; font-size: .95cqw; color: #8892AA; }
.dlegend { display: flex; gap: 1.6cqw; font-size: 1cqw; color: #8892AA; }
.dlegend span { display: inline-flex; align-items: center; gap: .5cqw; }
.dlegend i { width: .9cqw; height: .9cqw; border-radius: .2cqw; }
.dlegend .k1 { background: #22D3EE; }
.dlegend .k2 { background: #34D399; }
.dlegend .k3 { background: #F87171; }

/* — Displacement row ———————————————————————————————————— */
.dmetrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1cqw; }
.dmet { padding: 1cqw 1.1cqw; display: grid; gap: .2cqw; align-content: start; }
.dmet .dkpi-v { font-size: 2cqw; }
.spark { width: 100%; height: 2.6cqw; margin-top: .6cqw; overflow: visible; }
.spark polyline { fill: none; stroke: #22D3EE; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }


/* ═══════════════════════════════════════════════════════════════════
   DASHBOARD SCREENS  ·  shared additions
   Rate insights, Demand insights and Custom dashboards carry far more
   content than Groups in the same 920x504 frame, so .is-dense scales
   the whole type and spacing system down rather than letting any one
   section overflow. Still cqw-based, so density holds at any width.
   ═══════════════════════════════════════════════════════════════════ */
.dash.is-dense .dash-main { padding: 1.9cqw 2.1cqw; gap: 1.1cqw; grid-template-rows: auto; align-content: start; }
.dash.is-dense .dkpi { padding: .9cqw 1.1cqw; }
.dash.is-dense .dkpi-l { font-size: 1.2cqw; }
.dash.is-dense .dkpi-s { font-size: .92cqw; }
.dash.is-dense .dkpi-v { font-size: 2.1cqw; margin-top: .3cqw; }
.dash.is-dense .dkpi-c { font-size: .9cqw; }
.dash.is-dense .dash-panel { padding: 1cqw 1.2cqw; gap: .7cqw; }
.dash.is-dense .dash-ph { font-size: 1.2cqw; }

.dash-h { margin: 0; font-size: 1.9cqw; font-weight: 700; letter-spacing: -.02em; color: #E8EEFF; }
.dash-top { display: flex; align-items: center; justify-content: space-between; gap: 1.2cqw; }

/* — Buttons + toolbar (Custom dashboards) ——————————————— */
.dacts { display: flex; gap: .7cqw; }
.dbtn {
  font-size: 1.05cqw; padding: .65cqw 1.1cqw;
  border-radius: .7cqw;
  border: 1px solid rgba(255,255,255,.12);
  color: #E8EEFF;
}
.dbtn.is-primary { background: #2D4DE0; border-color: transparent; }

.dbar { display: flex; gap: .7cqw; }
.dsel {
  display: inline-flex; align-items: center; gap: .5cqw;
  font-size: 1.05cqw; padding: .6cqw 1cqw;
  border-radius: .7cqw;
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.07);
  color: #E8EEFF;
}
.dsel b { color: #8892AA; font-weight: 400; }
.dsel.is-ghost { color: #8892AA; }

/* — Segmented control (Rate trend) ——————————————————————— */
.dseg {
  margin-left: auto;
  display: inline-flex; gap: .2cqw;
  padding: .2cqw;
  border-radius: .7cqw;
  background: rgba(255,255,255,.05);
  border: 1px solid rgba(255,255,255,.07);
}
.dseg b {
  font-size: .95cqw; font-weight: 500;
  padding: .4cqw .8cqw;
  border-radius: .5cqw;
  color: #8892AA;
}
.dseg b.on { background: #2D4DE0; color: #fff; }

/* — Line charts ————————————————————————————————————————
   One component at four sizes. The area is a translucent wash under
   the stroke rather than a separate gradient element, which keeps the
   markup to a single <svg> per chart. */
.lc { display: block; width: 100%; height: 6cqw; overflow: visible; }
.lc-l { fill: none; stroke: #2D4DE0; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.lc-a { fill: rgba(45,77,224,.22); stroke: none; }
.lc.is-tall { height: 12cqw; }
.lc.is-card { height: 5.4cqw; margin-top: .4cqw; }
.lc.is-mini { height: 2.6cqw; margin-top: .5cqw; }
.lc.is-tiny { height: 3cqw; width: 34%; align-self: end; }

.dtrend { position: relative; }
.dtip {
  position: absolute; right: 0; top: -0.4cqw;
  font-size: 1cqw; font-weight: 600; color: #fff;
  background: #2D4DE0;
  padding: .3cqw .6cqw; border-radius: .4cqw;
}

/* — Layout variants ————————————————————————————————————— */
.dmetrics.is-3 { grid-template-columns: repeat(3, 1fr); }
.dmet.is-row { display: flex; align-items: flex-end; justify-content: space-between; gap: .8cqw; }
.dmet.is-row > div { display: grid; gap: .15cqw; }
.dash-panel.is-flat { background: rgba(255,255,255,.03); }
.dsplit { display: grid; grid-template-columns: 2fr 1fr; gap: 1.1cqw; }
.dlabels.is-flush { padding-left: 0; }
.dplot.is-bars { height: 8cqw; gap: .5cqw; background: repeating-linear-gradient(180deg, rgba(255,255,255,.07) 0 1px, transparent 1px 25%); }
.dplot.is-bars .dcol i { background: #2D4DE0; border-radius: .2cqw .2cqw 0 0; }

/* — Recommendations ———————————————————————————————————— */
.drecs { display: grid; grid-template-columns: repeat(3, 1fr); gap: .9cqw; }
.drec {
  display: flex; align-items: center; gap: .8cqw;
  padding: .9cqw 1cqw;
  border-radius: .8cqw;
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.07);
}
.drec > div { display: grid; gap: .15cqw; }
.drec-a { font-size: 1.2cqw; font-weight: 600; color: #E8EEFF; }
.drec .ar {
  width: 2cqw; height: 2cqw; flex: none;
  display: grid; place-items: center;
  border-radius: .5cqw;
  font-size: .9cqw; font-style: normal;
}
.drec .ar.up   { background: rgba(45,77,224,.22); color: #7F94FF; }
.drec .ar.flat { background: rgba(255,255,255,.06); color: #8892AA; }

/* — Booking pace scale —————————————————————————————————— */
.dpace { display: flex; align-items: center; gap: 1cqw; }
.dpace-e { font-size: .95cqw; color: #8892AA; flex: none; }
.dpace-t {
  position: relative; flex: 1; height: .55cqw;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(255,255,255,.06), rgba(45,77,224,.30));
}
.dpace-t .mid { position: absolute; left: 50%; top: -.5cqw; bottom: -.5cqw; width: 1px; background: rgba(255,255,255,.18); }
.dpace-t .mark {
  position: absolute; top: 50%;
  width: 1.1cqw; height: 1.1cqw;
  margin: -.55cqw 0 0 -.55cqw;
  border-radius: 50%;
  background: #2D4DE0;
  box-shadow: 0 0 0 .25cqw rgba(45,77,224,.28);
}
.dpace-t .mark b {
  position: absolute; left: 50%; bottom: 1.6cqw;
  transform: translateX(-50%);
  font-size: 1cqw; font-weight: 600; color: #E8EEFF;
  white-space: nowrap;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  artwork at 70%, lifted 30px off the bottom
   430 → 301, 453 → 317, 448 → 314.

   All three are absolutely positioned now, including card three: with
   a 30px offset from the bottom edge the anchor has to be the bottom,
   and an in-flow element cannot be measured from there. The horizontal
   negative offsets still cancel the card's 32px padding so the artwork
   meets the side edges.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br {
  position: absolute;
  bottom: 30px;
  right: calc(var(--s-6) * -1);
  top: auto; left: auto;
  width: 301px;
  margin: 0;
  transform: none;
}
.step-art.at-bl {
  position: absolute;
  bottom: 30px;
  left: calc(var(--s-6) * -1);
  top: auto; right: auto;
  width: 317px;
  margin: 0;
  transform: none;
}
.step-art.at-c {
  position: absolute;
  bottom: 30px;
  left: calc(var(--s-6) * -1);
  top: auto; right: auto;
  width: 314px;
  margin: 0;
  transform: none;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  re-exported artwork at 90%
   430 → 387, 453 → 408, 469 → 422. Anchors and the 30px bottom
   offset are unchanged.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br { width: 387px; }
.step-art.at-bl { width: 408px; }
.step-art.at-c  { width: 422px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS WIRES  ·  restyled
   Was a flat 1.1px line at uniform opacity with a hard 6-unit dash
   running along it. Now:

   · the wire carries a gradient, dim at the source and bright at ICS,
     so the line states the direction of flow on its own
   · the travelling pulse is longer with a soft cap, reading as a comet
     with a tail rather than a ticking dash
   · a wider, dimmer companion stroke sits underneath, which is what
     gives the thin line its glow instead of a blur filter
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire {
  stroke: url(#wireGrad);
  stroke-width: 1.4;
  stroke-linecap: round;
}

.ics-wires .signal {
  stroke: #AEB6FF;
  stroke-width: 2;
  stroke-dasharray: 14 86;
  filter: drop-shadow(0 0 6px rgba(134, 146, 255, .95))
          drop-shadow(0 0 14px rgba(86, 96, 255, .55));
}

/* The halo. Same path, thicker and much dimmer, drawn under the bright
   core — cheaper and crisper than blurring the stroke itself. */
.ics-wires .signal::after { content: none; }
.ics-wires g:last-of-type .signal {
  paint-order: stroke;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  gaps measured from the text
   Back into normal flow. A gap "between text and image" has to be
   measured from the bottom of the copy, and an absolutely positioned
   element anchored to the card's bottom edge cannot express that —
   its distance from the text is whatever the card height leaves over.

   Horizontal placement is now margin-based: auto on one side pushes
   the image to the other, and the negative margin cancels the card's
   32px padding so it still meets the edge.
   ═══════════════════════════════════════════════════════════════════ */
.step-art.at-br,
.step-art.at-bl,
.step-art.at-c {
  position: static;
  top: auto; right: auto; bottom: auto; left: auto;
  transform: none;
}

/* Card 1 — 40px under the copy, flush right */
.step-art.at-br {
  width: 387px;
  margin: 40px calc(var(--s-6) * -1) 0 auto;
}

/* Card 2 — 20px under the copy, moved to the right corner */
.step-art.at-bl {
  width: 408px;
  margin: 20px calc(var(--s-6) * -1) 0 auto;
}

/* Card 3 — 40px under the copy, flush left */
.step-art.at-c {
  width: 422px;
  margin: 40px auto 0 calc(var(--s-6) * -1);
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  one composition, three zones
   The zones keep the existing card treatment untouched — same glass,
   border, radius, typography, bloom. Only the arrangement changed:
   equal thirds reading left to right, with the flow wires running
   behind them so the three read as one system rather than three
   separate cards.

   Heights are auto now. Fixed heights were clipping the artwork, and
   a composition that cuts off its own content cannot read as
   deliberate.
   ═══════════════════════════════════════════════════════════════════ */
.flow {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(var(--s-5), 3vw, var(--s-7));
  align-items: stretch;
}

.flow-zone {
  position: relative;
  z-index: 1;
  height: auto;
  min-height: 0;
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Numbering is real information here — the three zones are a sequence,
   source → signals → recommendation — so it earns its place. */
.flow-num {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .14em;
  color: var(--text-low);
  margin-bottom: 10px;
}

.flow-zone .step-title { margin: 0 0 var(--s-5); }

/* Artwork sits at the foot of its zone, keeping each export's own
   alignment: 01 right, 02 right, 03 left. */
.flow-zone .step-art {
  position: static;
  margin-top: auto;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}
.flow-zone .step-art.at-br { width: 387px; max-width: calc(100% + var(--s-6)); margin-left: auto;  margin-right: calc(var(--s-6) * -1); }
.flow-zone .step-art.at-bl { width: 408px; max-width: calc(100% + var(--s-6)); margin-left: auto;  margin-right: calc(var(--s-6) * -1); }
.flow-zone .step-art.at-c  { width: 422px; max-width: calc(100% + var(--s-6)); margin-right: auto; margin-left: calc(var(--s-6) * -1); }

/* — Connectors ————————————————————————————————————————
   Behind the zones, so they read as passing through rather than
   bridging between — the difference between a system and a flowchart. */
.flow-wires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  overflow: visible;
  pointer-events: none;
}
.flow-wires .fwire { fill: none; stroke: url(#flowGrad); stroke-width: 1.4; stroke-linecap: round; }
.flow-wires .fsignal {
  fill: none;
  stroke: #AEB6FF;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 12 88;
  filter: drop-shadow(0 0 6px rgba(134,146,255,.9));
  animation: signal-flow var(--flow, 3.6s) linear var(--flow-delay, 0s) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .flow-wires .fsignal { animation: none; opacity: .5; stroke-dasharray: none; }
}

/* — Responsive ————————————————————————————————————————
   Tablet keeps the three-zone relationship at reduced scale; mobile
   stacks 01 → 02 → 03 and turns the wires vertical. */
@media (max-width: 1000px) {
  .flow { gap: var(--s-4); }
  .flow-zone { padding: var(--s-5); }
}
@media (max-width: 760px) {
  .flow { grid-template-columns: 1fr; gap: var(--s-6); }
  .flow-wires { display: none; }
  .flow::before {
    content: '';
    position: absolute;
    left: 50%; top: 0; bottom: 0;
    width: 1.4px;
    transform: translateX(-50%);
    background: linear-gradient(180deg,
      rgba(86,96,255,.10), rgba(86,96,255,.40), rgba(142,151,255,.70));
    z-index: 0;
  }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  per-card artwork placement
   Card 1 comes down to 90% and no longer reaches the side edges, so it
   is centred rather than left-anchored — a reduced image pinned to one
   negative margin would sit visibly off-axis.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-br {
  width: calc((100% + (var(--s-6) * 2)) * 0.9);
  margin: auto auto 5px;
}

/* Card 2 keeps the full bleed, lifted 10px off the bottom. */
.flow-zone .step-art.at-bl {
  width: calc(100% + (var(--s-6) * 2));
  margin: auto calc(var(--s-6) * -1) 10px;
}

/* Card 3 measures from the copy instead of the bottom edge, so the
   20px is a real gap under the text rather than whatever the card
   height happens to leave. */
.flow-zone .step-art.at-c {
  width: calc(100% + (var(--s-6) * 2));
  margin: 20px calc(var(--s-6) * -1) auto;
}

/* The wires running behind the cards are gone. */
.flow-wires { display: none; }
.flow::before { content: none; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  artwork placement, measured from the card edge
   The card carries 32px of padding, so offsets are expressed against
   that: -32px sits flush with the card's own edge, and -22px leaves
   exactly 10px of card showing beneath the artwork.
   ═══════════════════════════════════════════════════════════════════ */

/* Card 1 — hard into the bottom-right corner, no gap on either side. */
.flow-zone .step-art.at-br {
  width: calc((100% + (var(--s-6) * 2)) * 0.9);
  margin: auto calc(var(--s-6) * -1) calc(var(--s-6) * -1) auto;
}

/* Card 2 — edge to edge, lifted 10px clear of the bottom. */
.flow-zone .step-art.at-bl {
  width: calc(100% + (var(--s-6) * 2));
  margin: auto calc(var(--s-6) * -1) calc((var(--s-6) - 10px) * -1);
}

/* Card 3 — pushed a further 20px down from the copy. */
.flow-zone .step-art.at-c {
  width: calc(100% + (var(--s-6) * 2));
  margin: 40px calc(var(--s-6) * -1) auto;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2 artwork up 10%
   Was edge to edge at 100% + 64px; now 10% larger, so it overhangs the
   card on both sides. Centred via equal negative margins, which keeps
   the extra width split evenly instead of all spilling to one side.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  --bleed: calc((100% + (var(--s-6) * 2)) * 0.1 / 2);
  width: calc((100% + (var(--s-6) * 2)) * 1.1);
  margin: auto calc((var(--s-6) * -1) - var(--bleed))
              calc((var(--s-6) - 10px) * -1)
              calc((var(--s-6) * -1) - var(--bleed));
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2 artwork at 130%
   Written as flat calc(). The previous attempt nested a custom
   property inside the margin calc; that failed to resolve, the whole
   declaration was dropped, and the rule fell back to an earlier
   `width: 408px` — narrower than the edge-to-edge width, which is why
   it appeared to shrink rather than grow.

   Base edge-to-edge width is 100% + 64px (the card's 32px padding on
   each side). At 130% that is 130% + 83.2px, so the overhang per side
   is 15% + 9.6px, plus the 32px padding cancel = 15% + 41.6px.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  width: calc(130% + 83.2px);
  max-width: none;
  margin-top: auto;
  margin-right: calc(-15% - 41.6px);
  margin-bottom: -22px;   /* 10px of card visible below the artwork */
  margin-left: calc(-15% - 41.6px);
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2 back to exact edge-to-edge
   No overhang. Width is the card plus its 32px padding on each side,
   and the margins cancel exactly that — so the artwork meets both
   edges with nothing spilling past and nothing clipped. Same width as
   card 3, so the two line up.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  width: calc(100% + 64px);
  max-width: none;
  margin-top: auto;
  margin-right: -32px;
  margin-bottom: -22px;   /* leaves 10px of card below the artwork */
  margin-left: -32px;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2 lifted 20px
   Was -22px (10px of card showing below). Now -12px, leaving 20px —
   the artwork sits 20px clear of the card's bottom edge. Width and
   side bleed unchanged.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  width: calc(100% + 64px);
  max-width: none;
  margin-top: auto;
  margin-right: -32px;
  margin-bottom: -12px;   /* 32px padding − 12px = 20px of card below */
  margin-left: -32px;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2, 20px under the copy
   margin-top was `auto`, which pushed the artwork to the bottom and
   left the gap under the heading as whatever the card height happened
   to leave. A fixed 20px makes it a real measurement.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  width: calc(100% + 64px);
  max-width: none;
  margin-top: 20px;
  margin-right: -32px;
  margin-bottom: -12px;
  margin-left: -32px;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2, artwork 40px above the card's bottom edge
   margin-top:auto pins it to the bottom of the flex column, so the
   offset is measured from there rather than from the copy.

   The card's padding already holds it 32px clear, so a POSITIVE 8px
   bottom margin adds the remaining distance: 32 + 8 = 40px. Negative
   values were pulling it the other way — into the padding, closer to
   the edge — which is why it kept reading as unchanged.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  width: calc(100% + 64px);
  max-width: none;
  margin-top: auto;
  margin-right: -32px;
  margin-bottom: 8px;
  margin-left: -32px;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  matched to the reference export
   Sampled from ICS flow design.png (1490x507):
     flow line   y 215  = 42.4% of frame height
     line colour #2931C3, flat — not a gradient
     cube centre x 48.0%   ·  orb centre x 85.6%

   Cube and orb are now both centred on 42.4%, so they sit on exactly
   one level. The "ICS" caption is positioned absolutely beneath the
   cube rather than stacked with it — as a flow child it was dragging
   the cube's centre upward, which is what put the two out of line.
   ═══════════════════════════════════════════════════════════════════ */
.ics-cube-wrap {
  left: 48%;
  top: 42.4%;
  transform: translate(-50%, -50%);
  display: block;
  gap: 0;
}
.ics-cube { display: block; width: 260px; height: 260px; }
.ics-cube-label {
  position: absolute;
  left: 50%;
  top: calc(100% + 10px);
  transform: translateX(-50%);
  white-space: nowrap;
}

.ics-orb-wrap {
  left: 85.6%;
  right: auto;
  top: 42.4%;
  transform: translate(-50%, -50%);
}

/* Flat indigo, matching the export. The directional gradient is gone. */
.ics-wires .wire {
  stroke: #2931C3;
  stroke-width: 1.3;
  stroke-linecap: butt;
}

/* Dashes in the reference are only marginally brighter than the wire —
   a travelling thickening, not a glowing comet. */
.ics-wires .signal {
  stroke: #4B57E8;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-dasharray: 5 95;
  filter: none;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2 artwork up a further 40px
   Was 8px (sitting 40px clear of the card edge, with the 32px padding).
   Now 48px, so it clears the edge by 80px. Positive values move it up;
   negative pull it down into the padding.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  width: calc(100% + 64px);
  max-width: none;
  margin-top: auto;
  margin-right: -32px;
  margin-bottom: 48px;   /* 32px padding + 48px = 80px above the card edge */
  margin-left: -32px;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  three artworks as one continuous strip
   The reference reads across all three cards at once: the rows inside
   each export line up with the rows beside them, and the top of the
   composition bleeds off the card edge.

   That only holds if all three are anchored identically. They had
   drifted to three different treatments — bottom-flush, 80px up, and
   measured from the copy — which is what broke the alignment. All
   three now share one rule: full bleed on both sides, flush to the
   bottom edge, overflow clipped at the top.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art,
.flow-zone .step-art.at-br,
.flow-zone .step-art.at-bl,
.flow-zone .step-art.at-c {
  position: static;
  width: calc(100% + 64px);
  max-width: none;
  margin: auto -32px -32px;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}
.flow-zone .step-art img { display: block; width: 100%; height: auto; margin: 0; }

/* Equal card heights keep the strip reading straight across — the
   grid already stretches them, this just stops any one growing on
   its own content. */
.flow { align-items: stretch; }
.flow-zone { overflow: hidden; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  exports back in, placed per card
   Card padding is 20px now, so a -20px margin sits flush with the card
   edge and -16px leaves 4px of card showing beneath.
   Full-bleed width is therefore 100% + 40px.
   ═══════════════════════════════════════════════════════════════════ */

/* Card 1 — 90%, bottom-right, 4px clear of the bottom edge. */
.flow-zone .step-art.at-br {
  position: static;
  width: calc(90% + 36px);
  max-width: none;
  margin: auto -20px -16px auto;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}

/* Card 2 — edge to edge, flush bottom. */
.flow-zone .step-art.at-bl {
  position: static;
  width: calc(100% + 40px);
  max-width: none;
  margin: auto -20px -20px;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}

/* Card 3 — 90%, centred horizontally. */
.flow-zone .step-art.at-c {
  position: static;
  width: calc(90% + 36px);
  max-width: none;
  margin: auto auto -20px;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}

.flow-zone .step-art img { display: block; width: 100%; height: auto; margin: 0; }

/* The ambient field from the rebuild comes back off — the exports
   carry their own lighting, and layering a second one over the top
   washes them out. */
.flow-zone::after { content: none; }

/* Unused now that the compositions are images again. */
/* Scoped to the flow zones ONLY. The unscoped version of this rule
   also matched .tile in the hero — the nine drifting signal dots share
   that class name — and switched the entire hero animation off. */
.flow-zone .stage,
.flow-zone .tile,
.flow-zone .pill-connected,
.flow-zone .paths,
.flow-zone .pulse-node { display: none; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  offsets pinned to the real padding
   The card resolves to var(--s-5) = 24px, not the 20px the previous
   offsets assumed — which left a 4px gap on every edge that was meant
   to be flush. Padding is now stated explicitly so the two cannot
   drift apart again.

     full bleed        = 100% + 48px
     90% of full bleed = 90% + 43.2px
     flush             = -24px
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone { padding: 24px; }

/* Card 1 — 90%, hard against the right edge, 4px clear of the bottom. */
.flow-zone .step-art.at-br {
  position: static;
  width: calc(90% + 43.2px);
  max-width: none;
  margin: auto -24px -20px auto;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}

/* Card 2 — flush on both edges, no gap either side. */
.flow-zone .step-art.at-bl {
  position: static;
  width: calc(100% + 48px);
  max-width: none;
  margin: auto -24px -24px;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}

/* Card 3 — 90%, centred. */
.flow-zone .step-art.at-c {
  position: static;
  width: calc(90% + 43.2px);
  max-width: none;
  margin: auto auto -24px;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 1 shifted 10px further right
   Was -24px (flush with the card edge). Now -34px, so the artwork
   overhangs the right edge by 10px and is clipped there. Width and the
   4px bottom offset are unchanged.
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-br {
  position: static;
  width: calc(90% + 43.2px);
  max-width: none;
  margin: auto -34px -20px auto;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  card 2 truly edge to edge, lifted 15px
   The card carries a 1px border on top of its 24px padding, so a
   -24px margin lands on the padding box — one pixel inside the visible
   edge. -25px clears the border as well, which is the last hairline
   that was still reading as a gap.

     width  = 100% + 50px   (48px padding + 2px border)
     sides  = -25px         (flush with the outer edge)
     bottom = -9px          (24px padding − 9 = 15px lift)
   ═══════════════════════════════════════════════════════════════════ */
.flow-zone .step-art.at-bl {
  position: static;
  width: calc(100% + 50px);
  max-width: none;
  margin: auto -25px -9px;
  transform: none;
  top: auto; right: auto; bottom: auto; left: auto;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  visible flow on every wire
   All seven carry a signal — the six sources plus the outbound run to
   pulse. The dash was 5% of the path at a dim #4B57E8 with no glow,
   which at this line weight was close to invisible against the ground.

   Longer dash, brighter core, and a soft halo so the travelling packet
   reads clearly without turning the wire into a neon tube.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire {
  stroke: #2931C3;
  stroke-width: 1.3;
  stroke-linecap: butt;
  opacity: .9;
}

.ics-wires .signal {
  stroke: #99A4FF;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-dasharray: 9 91;
  filter: drop-shadow(0 0 4px rgba(120, 134, 255, .9));
  animation: signal-flow var(--flow, 3.4s) linear var(--flow-delay, 0s) infinite;
}

/* Staggered so the six inbound wires never fire together — a single
   synchronised pulse reads as a blink, not a flow. */
.ics-wires use:nth-of-type(1) { --flow: 3.4s; --flow-delay: -0.0s; }
.ics-wires use:nth-of-type(2) { --flow: 4.0s; --flow-delay: -1.3s; }
.ics-wires use:nth-of-type(3) { --flow: 3.1s; --flow-delay: -2.2s; }
.ics-wires use:nth-of-type(4) { --flow: 4.4s; --flow-delay: -0.7s; }
.ics-wires use:nth-of-type(5) { --flow: 3.7s; --flow-delay: -2.8s; }
.ics-wires use:nth-of-type(6) { --flow: 4.6s; --flow-delay: -1.1s; }
.ics-wires use:nth-of-type(7) { --flow: 2.9s; --flow-delay: -0.4s; }

@media (prefers-reduced-motion: reduce) {
  .ics-wires .signal { animation: none; opacity: .6; stroke-dasharray: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS CUBE  ·  video with alpha
   VP9/WebM keeps the transparency (verified: AlphaMode=1 in the
   container, alpha planes on every frame), so it composites straight
   onto the section ground with no plate behind it.

   The poster is a transparent PNG of frame one — it shows before the
   video decodes, and it is what Safari falls back to, since Safari
   cannot play VP9 alpha.
   ═══════════════════════════════════════════════════════════════════ */
video.ics-cube {
  display: block;
  width: 260px;
  height: 260px;
  background: none;
  object-fit: contain;
  pointer-events: none;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  single composition
   The three cards, their headings and the routing are one flat export
   now, so the section is a single image rather than three built
   surfaces. Everything the cards used to carry — glass, borders,
   bloom, tile positioning — lives inside the PNG.
   ═══════════════════════════════════════════════════════════════════ */
.how-strip { width: 100%; }
.how-strip img {
  display: block;
  width: 100%;
  max-width: 1449px;
  height: auto;
  margin: 0 auto;
}


/* ═══════════════════════════════════════════════════════════════════
   INTEGRATIONS  ·  bigger marks, names beneath, colour on hover
   The mark and its caption are stacked now, so .logo is a column and
   the height is set on the <img> rather than the wrapper — a fixed
   height on the wrapper would squash the two together.

   Resting state is monochrome white; hovering drops the filter and
   the mark returns to its own brand colours.
   ═══════════════════════════════════════════════════════════════════ */
.logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  height: auto;
  filter: grayscale(1) brightness(0) invert(1);
  opacity: .5;
  transition: filter .28s var(--ease-out), opacity .28s var(--ease-out);
}

.logo img {
  height: 56px;          /* was 28px */
  width: auto;
  max-width: 150px;
  object-fit: contain;
  display: block;
}

.logo span {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .01em;
  color: var(--text-mid);
  white-space: nowrap;
}

/* Hover restores the real mark. The filter is removed entirely rather
   than eased — a half-desaturated logo mid-transition looks broken,
   so only opacity carries the timing. */
.logo:hover {
  filter: none;
  opacity: 1;
}

/* The caption should not invert with the mark, so it sits outside the
   filtered flow at full strength on hover. */
.logo:hover span { color: var(--text-hi); }

/* A little more air now that the items are taller. */
.logo-set { gap: clamp(var(--s-8), 7vw, 120px); padding-right: clamp(var(--s-8), 7vw, 120px); }


/* ═══════════════════════════════════════════════════════════════════
   SECTION SUB-TEXT
   Sits to the right of the headline rather than beneath it. The head
   becomes a two-column grid: the title keeps its own measure on the
   left, the sub-line sits opposite and bottom-aligned so its baseline
   settles against the last line of the heading instead of floating at
   the top of a much taller block.
   ═══════════════════════════════════════════════════════════════════ */
#how .section-head,
#ics .section-head {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  column-gap: clamp(var(--s-6), 5vw, var(--s-9));
  align-items: end;
}

/* The pill and the title stay together in column one. */
#how .section-head > .pill,
#ics .section-head > .pill { grid-column: 1; justify-self: start; }
#how .section-head > .section-title,
#ics .section-head > .section-title { grid-column: 1; }

.section-sub {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  font-size: 18px;
  line-height: 1.55;
  color: var(--text-mid);
  max-width: 46ch;
  text-wrap: pretty;
}

/* Below the two-column threshold it drops under the heading, where the
   natural reading order already puts it. */
@media (max-width: 900px) {
  #how .section-head,
  #ics .section-head { grid-template-columns: 1fr; }
  .section-sub { grid-column: 1; grid-row: auto; margin-top: 20px; max-width: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   SECTION SUB-TEXT  ·  top-aligned with the headline
   align-items:end was settling the sub-line against the last line of
   the heading. start aligns their tops instead — and because the sub
   shares the heading's grid row, it lines up with the H1 itself rather
   than with the pill above it.
   ═══════════════════════════════════════════════════════════════════ */
#how .section-head,
#ics .section-head { align-items: start; }

.section-sub {
  grid-column: 2;
  grid-row: 2;
  align-self: start;
  margin: 0;
  font-size: 18px;
  line-height: 1.55;
  color: var(--text-mid);
  max-width: 46ch;
  text-wrap: pretty;
}

/* The heading's first line sits slightly below its box top because of
   line-height leading; this nudges the sub-line onto the same optical
   baseline rather than the same box edge. */
.section-sub { padding-top: 0.14em; }


/* ═══════════════════════════════════════════════════════════════════
   SECTION SUB-TEXT  ·  10px lower
   Replaces the 0.14em optical nudge with an explicit offset, so the
   two are not stacking. Top alignment with the H1 stays the baseline;
   this simply sits it 10px below that.
   ═══════════════════════════════════════════════════════════════════ */
.section-sub { padding-top: 10px; }

@media (max-width: 900px) {
  .section-sub { padding-top: 0; margin-top: 20px; }
}


/* ═══════════════════════════════════════════════════════════════════
   SECTION SUB-TEXT  ·  a further 20px down
   Was 10px below the H1's top; now 30px. Replaces the value rather
   than adding a second offset alongside it.
   ═══════════════════════════════════════════════════════════════════ */
.section-sub { padding-top: 30px; }

@media (max-width: 900px) {
  .section-sub { padding-top: 0; margin-top: 20px; }
}


/* ═══════════════════════════════════════════════════════════════════
   SPACING PASS
   80px between every section, 40px between a section's heading block
   and its content. Trust / FAQ / Book a demo / footer were on 120px
   from an earlier round; they come back to 80 so the whole page runs
   on one rhythm.

   ICS is deliberately excluded — its 20px gap is set separately below
   and stays as it is.
   ═══════════════════════════════════════════════════════════════════ */
.section        { padding: 80px 0 0; }
.integrations   { padding: 80px 0 0; }
#trust          { padding-top: 80px; }
#faqs           { padding-top: 80px; }
#demo           { padding-top: 80px; }
.footer-shell   { padding-top: 80px; padding-bottom: 10px; }

.section-head { margin-bottom: 40px; }
.faq-head     { margin-bottom: 40px; }
.about-body   { margin-bottom: 40px; }

/* ICS untouched. */
#ics .section-head { margin-bottom: 20px; }
#ics .ics-stage    { margin-top: 0; }


/* ═══════════════════════════════════════════════════════════════════
   SUITE SCREENS  ·  one card size for all
   The exports arrive at five different sizes (920x504, 1015x504,
   1015x584, 934x510, 920x553). The panel now holds a fixed 920:504
   box and every screen fits inside it, so each card occupies exactly
   the same footprint regardless of what it was exported at.

   `contain` rather than `cover`: these are dashboards with content
   running to the edges, and cropping 9-14% off the taller exports
   would cut real UI. A few pixels of dark letterbox is the cheaper
   compromise.
   ═══════════════════════════════════════════════════════════════════ */
.suite-view,
.suite-views { width: 100%; }

.suite-screen {
  display: block;
  width: 100%;
  aspect-ratio: 920 / 504;
  height: auto;
  object-fit: contain;
  object-position: center;
  border-radius: 0;
  margin: 0 -24px 0 0;
}


/* ═══════════════════════════════════════════════════════════════════
   SUITE SCREENS  ·  every export now genuinely 920x504
   The odd sizes were transparent padding, not content — each carried
   its real screen at 920x504 from the origin, with empty space on the
   right and bottom. Trimmed at the file level, so there is nothing
   left for CSS to compensate for.

   `cover` is safe now that every source matches the box exactly: no
   scaling, no letterbox, no crop. Same footprint and same alignment
   as Pickup and pace.
   ═══════════════════════════════════════════════════════════════════ */
.suite-screen {
  display: block;
  width: 100%;
  aspect-ratio: 920 / 504;
  height: auto;
  object-fit: cover;
  object-position: center;
  border-radius: 0;
  margin: 0 -24px 0 0;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS WIRES  ·  matched to flow design.png
   Convergence sampled at x630 y228 of a 1490x507 frame — the cube's
   left face, at 45% of frame height. The reference dashes are short,
   thick and clearly brighter than the wire, so they read as packets
   rather than as a texture along the line.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire {
  stroke: #3A44C9;
  stroke-width: 1.1;
  stroke-linecap: butt;
  opacity: .85;
}

.ics-wires .signal {
  stroke: #2F3BE8;
  stroke-width: 4;
  stroke-linecap: round;
  stroke-dasharray: 4 96;
  filter: drop-shadow(0 0 3px rgba(60, 76, 255, .8));
}


/* ═══════════════════════════════════════════════════════════════════
   ICS WIRES  ·  Glow line.png
   Thin, evenly weighted, rounded joins — the reference draws a bus,
   not a glowing tube. The packet is a short thick dash on the same
   blue, which is what makes it read as travelling along the line
   rather than lighting it up.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire {
  stroke: #2C34D8;
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
  opacity: .95;
}

.ics-wires .signal {
  stroke: #2F3BE8;
  stroke-width: 4.5;
  stroke-linecap: round;
  stroke-dasharray: 3 97;
  filter: drop-shadow(0 0 3px rgba(60, 76, 255, .85));
}


/* ═══════════════════════════════════════════════════════════════════
   ICS WIRES  ·  hard L corners
   linejoin miter rather than round, so each turn renders as a true
   right angle. linecap stays butt — a round cap would bulge the line
   ends past the dot and the cube face.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire {
  stroke: #2C34D8;
  stroke-width: 1.2;
  stroke-linejoin: miter;
  stroke-linecap: butt;
  fill: none;
  opacity: .95;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS WIRES  ·  rounded joins again
   The reference bends are filleted, not mitred — linejoin round.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire {
  stroke: #2C34D8;
  stroke-width: 1.2;
  stroke-linejoin: round;
  stroke-linecap: butt;
  fill: none;
  opacity: .95;
}


/* ═══════════════════════════════════════════════════════════════════
   ═══  LIGHT THEME  ══════════════════════════════════════════════════
   ═══════════════════════════════════════════════════════════════════

   The page flips to a cool off-white ground; four zones stay dark —
   How it works, ICS, Questions and the footer.

   Done by REDECLARING TOKENS, not by rewriting components. Every
   surface already reads its colour from a variable, so the dark zones
   just re-scope the same names and every card, border and label inside
   them follows automatically. One block, not a second stylesheet.
   ═══════════════════════════════════════════════════════════════════ */
:root {
  /* Blue-and-white, not off-white. The ground carries real blue so the
     white surfaces on top of it read AS white — that contrast between
     ground and card is what makes the page look mixed rather than pale.
     Cards stay #FFFFFF; the ground moved, they did not. */
  --bg-page:   #F5F6FA;   /* the hero's own bottom stop                */
  --bg-base:   #F5F6FA;
  --bg-void:   #D5DDF1;
  --bg-raise:  #FFFFFF;

  --text-hi:   #0A0F2E;   /* brand navy, primary · 15.4:1              */
  /* Every one of these moved down a step. The ground is darker than the
     off-white they were set against, so the old values lost contrast:
     #7C8299 fell to 3.14:1 and #646C87 to 4.28:1 — the first scraping
     the 3.0 large-text floor, the second failing the 4.5 small-text
     one outright. These clear both with margin. */
  --text-dim:  #6F758C;   /* de-emphasised headline half · 3.75:1      */
  --text-mid:  #4A5169;   /* body · 6.46:1                             */
  --text-low:  #5A6180;   /* small caps, captions · 5.03:1             */

  --line-soft: rgba(10, 15, 46, .10);
  --line-hard: rgba(10, 15, 46, .18);

  --glass-fill: rgba(255, 255, 255, .72);
}

body { background: var(--bg-page); color: var(--text-hi); }

/* The fixed atmosphere was built for a dark ground: at full strength on
   off-white it turns the page lilac. Same gradients, far lighter touch. */
body::before { opacity: .22; }

/* — Dark zones ————————————————————————————————————————
   Same token names, dark values. Everything nested inherits. */
.on-dark {
  /* Lifted off the old near-black #0B1020. That value was chosen when the
     whole page was dark; against a blue-white ground it read as a hole
     punched through the page rather than a band of it. This is the same
     hue, opened up — still unambiguously dark, but part of the family. */
  --bg-page:   #1B2350;
  --bg-base:   #1B2350;
  --bg-raise:  #262F63;   /* raised to keep its step above the new ground */

  --text-hi:   #FFFFFF;   /* 14.9:1 */
  --text-dim:  #8C94AC;   /* 4.88:1 */
  --text-mid:  #A0AAC8;
  /* #747CA6 measured 4.68:1 on #0B1020 and fell to 3.67:1 here — under
     the small-text floor. Lightened until it cleared 4.5 again. */
  --text-low:  #878FBB;   /* 4.77:1 */

  --line-soft: rgba(255, 255, 255, .10);
  --line-hard: rgba(255, 255, 255, .18);

  /* A 3.5% white fill was calibrated against near-black. On the lighter
     ground it disappears, so the cards need a little more to sit up. */
  --glass-fill: rgba(255, 255, 255, .055);

  background: #1B2350;
  color: var(--text-hi);
  position: relative;
  z-index: 1;
}

/* ═══ Components that need more than a token swap ═══════════════════ */

/* Glass on light is a raised white surface, not a translucent dark one:
   a 3.5% white fill over off-white is invisible. Shadow carries the
   elevation instead of a rim highlight. */
.card, .glass.card, .acc, .glass.acc, .final, .step, .glass.step {
  background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(10, 15, 46, .09);
  backdrop-filter: blur(18px) saturate(1.1);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  box-shadow: 0 10px 30px -18px rgba(10, 15, 46, .28);
}
.on-dark .card, .on-dark .glass.card,
.on-dark .acc,  .on-dark .glass.acc,
.on-dark .final, .on-dark .step, .on-dark .glass.step {
  background: rgba(255, 255, 255, .035);
  border: 1px solid rgba(255, 255, 255, .09);
  box-shadow: none;
}

/* Section label pill */
.pill {
  border-color: color-mix(in srgb, var(--indigo-500) 40%, transparent);
  background: color-mix(in srgb, var(--indigo-500) 8%, transparent);
  color: var(--indigo-500);
}
.on-dark .pill {
  border-color: color-mix(in srgb, var(--indigo-400) 55%, transparent);
  background: color-mix(in srgb, var(--indigo-900) 40%, transparent);
  color: #FFFFFF;
}

/* Nav sits over the light hero now */
.nav {
  background: rgba(245, 246, 250, .78);
  border-bottom: 1px solid rgba(10, 15, 46, .08);
}
.nav-link { color: var(--text-mid); }
.nav-link:hover { color: var(--text-hi); background: rgba(10, 15, 46, .05); }
.nav-link[aria-current] { color: var(--text-hi); background: rgba(10, 15, 46, .07); }

/* The wordmark is a white PNG — it needs inverting on the light nav,
   but not in the dark footer. */
.brand-logo { filter: invert(1) brightness(.18); }
.on-dark .footer-logo { filter: none; }

/* Integration marks were monochrome WHITE; on off-white they vanish. */
.logo { filter: grayscale(1) brightness(0); opacity: .55; }
.logo:hover { filter: none; opacity: 1; }

/* Hero headline: navy with a muted second half. */
.headline { background: none; -webkit-background-clip: border-box; background-clip: border-box; color: var(--text-hi); }
.headline .dim { color: var(--text-dim); }

/* Hero tiles read as solid shapes with shadow — a glow only exists
   against darkness, and bloom on off-white just muddies it. */
.tile-face {
  background: linear-gradient(150deg, var(--indigo-400), var(--indigo-600));
  box-shadow: 0 12px 26px -14px rgba(45, 77, 224, .55);
}
.tile.is-lit .tile-face {
  box-shadow: 0 14px 30px -12px rgba(45, 77, 224, .70);
}
.tile.is-soft .tile-face { filter: none; }

/* Buttons */
.btn-ghost { background: rgba(255,255,255,.7); border-color: rgba(10,15,46,.16); color: var(--text-hi); }
.btn-ghost:hover { background: #fff; border-color: var(--indigo-500); }
.btn-outline { background: rgba(255,255,255,.7); border-color: rgba(10,15,46,.16); color: var(--text-hi); }
.on-dark .btn-outline { background: color-mix(in srgb, var(--indigo-900) 34%, transparent); border-color: color-mix(in srgb, var(--indigo-400) 58%, transparent); }

/* Suite list */
.suite { background-image: none; background: rgba(255,255,255,.6); border-color: rgba(10,15,46,.09); }
.suite-item h3 { color: var(--text-hi); }
.suite-item:not(.is-active):hover { background: rgba(10,15,46,.04); }

/* Accordion plus-sign disc */
.acc > summary::after { background:
  linear-gradient(currentColor, currentColor) center/11px 1.6px no-repeat,
  linear-gradient(currentColor, currentColor) center/1.6px 11px no-repeat,
  rgba(10,15,46,.10); color: #fff; }
.on-dark .acc > summary::after { background:
  linear-gradient(currentColor, currentColor) center/11px 1.6px no-repeat,
  linear-gradient(currentColor, currentColor) center/1.6px 11px no-repeat,
  rgba(255,255,255,.16); color: var(--bg-void); }

/* About side-lights are far too strong on a pale ground. */
.about.is-visible::before,
.about.is-visible::after { opacity: .35; }


/* ═══════════════════════════════════════════════════════════════════
   LIGHT THEME  ·  adjustments
   ═══════════════════════════════════════════════════════════════════ */

/* Hero dots — 15s beat. The drift stays at 24s: matching them would
   make the two cycles align and the field would pulse in lockstep. */
:root { --pulse-duration: 15000ms; }

/* Problem — no ambient wash behind the illustration on light. */
#problem { background: transparent; }
#problem::before, #problem::after { content: none; }
.problem-grid { background: none; }
.orbit::before, .orbit::after { content: none; }

/* — Section rhythm ————————————————————————————————————— */
#how      { padding-top: 120px; padding-bottom: 120px; }
#ics      { padding-top: 120px; }
#about    { padding-top: 700px; padding-bottom: 700px; }
#faqs     { padding-bottom: 80px; }
#demo     { padding-bottom: 80px; }

/* ═══════════════════════════════════════════════════════════════════
   SUITE MENU  ·  selected vs unselected
   Selected reads white on the indigo fill; the rest sit at half
   strength so the active row is unambiguous at a glance.
   ═══════════════════════════════════════════════════════════════════ */
.suite-item {
  transition: opacity .2s var(--ease-out), background-color .2s var(--ease-out);
  opacity: .5;
}
.suite-item h3 { color: var(--text-hi); }
.suite-item p  { color: var(--text-mid); }

.suite-item.is-active {
  opacity: 1;
  background: linear-gradient(100deg, var(--indigo-500), var(--indigo-600));
  border-color: transparent;
  box-shadow: 0 10px 26px -14px rgba(45, 77, 224, .55);
}
.suite-item.is-active h3,
.suite-item.is-active p { color: #FFFFFF; }
.suite-item.is-active p { opacity: .82; }

.suite-item:not(.is-active):hover { opacity: .78; background: rgba(10, 15, 46, .04); }
.on-dark .suite-item:not(.is-active):hover { background: rgba(255, 255, 255, .05); }

/* ═══════════════════════════════════════════════════════════════════
   SOFTER HOVERS
   The dark-theme hovers were built to lift off a near-black ground; at
   that strength on off-white they read as heavy grey blocks.
   ═══════════════════════════════════════════════════════════════════ */
.card:hover, .acc:hover, .step:hover, .final:hover {
  background: rgba(255, 255, 255, .88);
  border-color: rgba(45, 77, 224, .22);
  box-shadow: 0 14px 34px -20px rgba(45, 77, 224, .30);
}
.card:hover::after, .acc:hover::after { opacity: .12; }
.final:hover::before { opacity: .22; }
.nav-link:hover { background: rgba(45, 77, 224, .07); }
.logo:hover { opacity: 1; }

/* ═══════════════════════════════════════════════════════════════════
   FOOTER  ·  light
   ═══════════════════════════════════════════════════════════════════ */
.footer-shell {
  --bg-page:   #E3E9F7;
  --text-hi:   #0A0F2E;
  --text-mid:  #4A5169;
  --text-low:  #5A6180;
  --line-soft: rgba(10, 15, 46, .10);
  background: transparent;
  color: var(--text-hi);
}
.footer {
  background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(10, 15, 46, .09);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}
.footer-rule { background: rgba(10, 15, 46, .12); }
.footer-col a { color: var(--text-hi); }
.footer-col a:hover { color: var(--indigo-500); }
.footer-base, .footer-base a { color: var(--text-low); }
.footer-logo { filter: invert(1) brightness(.18); }

/* The starfield bloom is a dark-ground asset — it cannot sit on
   off-white, so it comes off with the light footer. */
.footer-glow { display: none; }


/* ═══════════════════════════════════════════════════════════════════
   ADJUSTMENTS
   ═══════════════════════════════════════════════════════════════════ */

/* Hero beat at 30s. Drift stays 24s — deliberately not a multiple, so
   the two cycles never fall into phase. */
:root { --pulse-duration: 30000ms; }

/* — Problem ————————————————————————————————————————————
   No wash on hover and none on open. The disclosure should read as
   text appearing, not as the card changing state. */
#problem .card::after,
#problem .card:hover::after,
#problem .card:has(details[open])::after { opacity: 0; }
#problem .card:hover,
#problem .card:has(details[open]) {
  background: rgba(255, 255, 255, .82);
  border-color: rgba(10, 15, 46, .12);
  box-shadow: 0 10px 30px -18px rgba(10, 15, 46, .28);
}
#problem { padding-bottom: 80px; }

/* — How it works · centred ————————————————————————————— */
#how .section-head {
  display: block;
  text-align: center;
  margin-bottom: 40px;
}
#how .pill { display: inline-block; }
#how .section-title { max-width: none; margin-left: auto; margin-right: auto; }
#how .section-sub {
  margin: 20px auto 0;
  padding-top: 0;
  max-width: 60ch;
  text-align: center;
}
#how .how-strip img { margin: 0 auto; }

/* — Marketing suite ————————————————————————————————————— */
#marketing { padding-bottom: 80px; }

/* — ICS · centred, equal top and bottom ————————————————— */
#ics {
  padding-top: 120px;
  padding-bottom: 120px;
}
#ics .section-head {
  display: block;
  text-align: center;
  margin-bottom: 40px;
}
#ics .pill { display: inline-block; }
#ics .section-title { max-width: none; margin-left: auto; margin-right: auto; }
#ics .section-sub {
  margin: 20px auto 0;
  padding-top: 0;
  max-width: 60ch;
  text-align: center;
}

/* — Who built this · half the previous space ——————————— */
#about { padding-top: 350px; padding-bottom: 350px; }

/* — Nav · the clicked item becomes the current one ——————— */
.nav-link { cursor: pointer; }
.nav-link[aria-current] {
  color: var(--text-hi);
  background: rgba(45, 77, 224, .10);
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS + ICS  ·  headings back to the two-column layout
   The last pass centred the text as well; only the artwork was meant
   to be centred. Heading and pill return to the left, sub-text to the
   right, exactly as every other section on the page.
   ═══════════════════════════════════════════════════════════════════ */
#how .section-head,
#ics .section-head {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  column-gap: clamp(var(--s-6), 5vw, var(--s-9));
  align-items: start;
  text-align: left;
  margin-bottom: 40px;
}
#how .section-head > .pill,
#ics .section-head > .pill { grid-column: 1; justify-self: start; display: inline-block; }

#how .section-title,
#ics .section-title {
  grid-column: 1;
  max-width: 22ch;
  margin-left: 0;
  margin-right: 0;
  text-align: left;
}

#how .section-sub,
#ics .section-sub {
  grid-column: 2;
  grid-row: 2;
  align-self: start;
  margin: 0;
  padding-top: 30px;
  max-width: 46ch;
  text-align: left;
}

@media (max-width: 900px) {
  #how .section-head,
  #ics .section-head { grid-template-columns: 1fr; }
  #how .section-sub,
  #ics .section-sub { grid-column: 1; grid-row: auto; padding-top: 0; margin-top: 20px; max-width: none; }
}

/* The artwork stays centred in the section — that part was correct. */
.how-strip { display: flex; justify-content: center; }
.how-strip img { margin: 0 auto; }
.ics-stage  { margin-left: auto; margin-right: auto; }


/* ═══════════════════════════════════════════════════════════════════
   SECTION REVEALS
   Everything below the hero fades up as it arrives. 16px of travel,
   560ms, once — far enough to register, short enough that nobody
   watches it happen.

   transform and opacity only: both are composited, so a reveal costs
   nothing on the main thread. Animating height or margin here would
   force layout on every frame of every section.
   ═══════════════════════════════════════════════════════════════════ */
:root {
  --reveal-shift: 16px;
  --reveal-time:  730ms;   /* 30% slower than the 560ms first pass */
  --reveal-ease:  cubic-bezier(.22, .68, .28, 1);
}

.reveal {
  opacity: 0;
  transform: translate3d(0, var(--reveal-shift), 0);
  transition:
    opacity   var(--reveal-time) var(--reveal-ease) var(--reveal-delay, 0ms),
    transform var(--reveal-time) var(--reveal-ease) var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.reveal.is-in {
  opacity: 1;
  transform: none;
}

/* Dropped once the transition has run, so a few dozen elements are not
   left permanently promoted to their own compositor layer. */
.reveal.is-done { will-change: auto; transition: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS WIRES  ·  re-weighted for the lifted ground
   #2C34D8 was picked against near-black, where it held 2.34:1. On the
   new #1B2350 it falls to 1.84 and the bus goes faint — the diagram's
   whole subject. These restore the original separation on the new
   ground rather than the original hex.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires .wire   { stroke: #2C34D8; }   /* 6.1:1 on the tinted band */
.ics-wires .signal {
  stroke: #2F3BE8;
  filter: drop-shadow(0 0 3px rgba(60, 76, 255, .55));
}


/* ═══════════════════════════════════════════════════════════════════
   DARK CARDS  ·  5% fill
   Half of the 10% pass — the card is barely a surface now, just enough
   edge to hold its shape against the section. how-cards.png carries the
   same value baked in, so the painted cards and these stay in step.
   ═══════════════════════════════════════════════════════════════════ */
.on-dark { --glass-fill: rgba(255, 255, 255, .05); }

.on-dark .card, .on-dark .glass.card,
.on-dark .acc,  .on-dark .glass.acc,
.on-dark .final, .on-dark .step, .on-dark .glass.step {
  background: rgba(255, 255, 255, .05);
  border: 1px solid rgba(255, 255, 255, .11);
}



/* ═══════════════════════════════════════════════════════════════════
   TINTED BANDS  ·  how it works · ICS · FAQs
   These three were solid dark slabs. They are now the section colour
   at 5% over the page — a band you can see the edge of, not a hole
   punched through the layout.

   The class changed from .on-dark to .tint rather than being
   overridden. Thirteen rules hang off .on-dark — text tokens, card
   fills, borders, pills, chevrons — every one of them written for a
   near-black ground. Unpicking them one by one invites missing some;
   renaming the class retires all thirteen at once and lets these
   sections inherit the ordinary light theme like every other section.
   ═══════════════════════════════════════════════════════════════════ */
.tint {
  background: rgba(27, 35, 80, .05);
  position: relative;
  z-index: 1;
}


/* ═══════════════════════════════════════════════════════════════════
   NAV  ·  bare over the hero, card on scroll
   ═══════════════════════════════════════════════════════════════════ */
.nav {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
  transition:
    background-color .32s var(--ease-out),
    border-color     .32s var(--ease-out);
}
.nav.is-stuck {
  background: rgba(245, 246, 250, .82);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-bottom-color: rgba(10, 15, 46, .08);
}

/* Zero-footprint marker for the observer. Absolute so it cannot shift
   the hero by so much as a pixel. */
.nav-sentinel {
  position: absolute;
  top: 0; left: 0;
  width: 1px; height: 64px;
  pointer-events: none;
  visibility: hidden;
}
@media (prefers-reduced-motion: reduce) { .nav { transition: none; } }


/* ═══════════════════════════════════════════════════════════════════
   ACTIONS  ·  #1B2A9E, pill, roomier
   White on #1B2A9E is 11.2:1. The old padding was sized for a small
   radius; at a full pill the corners eat the horizontal end, so the
   sides grow more than the top and bottom.
   ═══════════════════════════════════════════════════════════════════ */
.btn { border-radius: 999px; padding: 13px 28px; }
.btn-lg { padding: 16px 36px; min-width: 216px; }

.btn-primary {
  background: #1B2A9E;
  color: #FFFFFF;
  border: 1px solid transparent;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .18),
    0 12px 28px -12px rgba(27, 42, 158, .70);
}
.btn-primary:hover {
  background: #2334BC;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .24),
    0 16px 34px -14px rgba(27, 42, 158, .82);
}


/* ═══════════════════════════════════════════════════════════════════
   TEXT  ·  three separable weights
   Primary, secondary and answer copy were all landing on the same two
   tokens, so the page read as one weight of grey. Each now has its own
   value, checked on BOTH grounds — the answers sit in Questions, which
   is a tinted band, and that band is darker than the page.
   ═══════════════════════════════════════════════════════════════════ */
.section-title, .role-title, .acc > summary { color: #0A0F2E; }   /* primary   · 15.5:1 */
.section-sub, .role-body                    { color: #4C5570; }   /* secondary ·  6.1:1 */
.acc-body, .why-body                        { color: #59617C; }   /* answers   ·  5.1:1 page · 4.6:1 band */

/* Eyebrows —————————————————————————————————————————————— */
#problem .role-card .eyebrow { color: #3E51FF; }   /* 5.1:1 on the card */


/* ═══════════════════════════════════════════════════════════════════
   SUITE  ·  selected row #1B2A9E
   ═══════════════════════════════════════════════════════════════════ */
.suite-item.is-active {
  background: #1B2A9E;
  border-color: transparent;
  box-shadow: 0 10px 26px -14px rgba(27, 42, 158, .60);
}
.suite-item.is-active h3,
.suite-item.is-active p { color: #FFFFFF; }
.suite-item.is-active p { opacity: .86; }


/* — ICS · the six source cards carry a dark fill, so their label has
     to be white. It was inheriting --text-hi, which is navy on this
     theme: dark type on a dark card. ————————————————————— */
.ics-source { color: #FFFFFF; }

/* — ICS · cube shadow at 30% of what it was ————————————————— */
.ics-cube { filter: drop-shadow(0 5px 13px rgba(0, 0, 0, .15)); }

/* — Who built this · 40% less air ————————————————————————— */
#about { padding-top: 210px; padding-bottom: 210px; }


/* ═══════════════════════════════════════════════════════════════════
   THE WHITE LINE ABOVE THE NAV
   Not a border. The hero is pulled up a fixed -84px to sit under the
   bar, but the bar measures about 91px — 24px of padding top and
   bottom around a 43px button. So roughly seven pixels of the page
   behind it stayed uncovered, and that strip is what reads as a line.

   Fixed by pulling further than the bar can be, rather than by tuning
   the number to today's bar height: the padding rises by the same
   amount, so nothing inside the hero moves.
   ═══════════════════════════════════════════════════════════════════ */
.hero { margin-top: -110px; padding-top: 110px; }


/* ═══════════════════════════════════════════════════════════════════
   SECTION GROUNDS
   Two alternating tones, assigned per section.
   ═══════════════════════════════════════════════════════════════════ */
body { background: #FFFAFA; }

.integrations,
#problem, #revenue, #marketing, #about, #demo,
.final, .footer-shell   { background: #FFFAFA !important; }

#how, #ics, #faqs       { background: #F7F7FD !important; }


/* ═══════════════════════════════════════════════════════════════════
   INTEGRATIONS  ·  tighter
   ═══════════════════════════════════════════════════════════════════ */
.integrations       { padding: 56px 0 0; }   /* was 80px · 30% off */
.integrations-title { margin: 0 0 20px; }    /* was 64px to the logos */


/* — ICS · the six source dots ————————————————————————————— */
.ics-dot {
  background: #3E51FF;
  box-shadow: 0 0 8px rgba(62, 81, 255, .85);
}


/* ═══════════════════════════════════════════════════════════════════
   EYEBROWS  ·  #3E51FF everywhere
   ═══════════════════════════════════════════════════════════════════ */
.eyebrow, .pill { color: #3E51FF; }
#problem .role-card .eyebrow { color: #3E51FF; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  sub ends where the card ends
   The heading grid spanned the container while the artwork is 1406px
   centred inside it, so the sub's right edge stopped short of the card
   by however much padding the viewport happened to give. Capping the
   head to the artwork's width and pushing the sub to the end of its
   column lines the two up at any size.
   ═══════════════════════════════════════════════════════════════════ */
#how .section-head {
  max-width: 1406px;
  margin-left: auto;
  margin-right: auto;
}
#how .section-sub {
  justify-self: end;
  text-align: left;
}


/* ═══════════════════════════════════════════════════════════════════
   SUITE  ·  copy above each dashboard
   ═══════════════════════════════════════════════════════════════════ */
.suite-copy {
  margin: 0 0 18px;
  max-width: 62ch;
  font-size: 14.5px;
  line-height: 1.6;
  color: #59617C;
}


/* ═══════════════════════════════════════════════════════════════════
   DEMO CARD  ·  the hero field
   ═══════════════════════════════════════════════════════════════════ */
.final {
  background:
    radial-gradient(90% 95% at 88% 6%,   #CDD3F2 0%, transparent 58%),
    radial-gradient(70% 80% at 12% 4%,   #FBEFF3 0%, transparent 55%),
    radial-gradient(105% 110% at 6% 96%, #D5DAF4 0%, transparent 62%),
    radial-gradient(85% 90% at 100% 78%, #E9E4F5 0%, transparent 50%),
    linear-gradient(118deg, #EDEFFB 0%, #F5F5FD 42%, #FFFAFA 70%, #E8EBF9 100%) !important;
}


/* — Footer card ————————————————————————————————————————— */
.footer { background: #EFF1FB !important; }


/* ═══════════════════════════════════════════════════════════════════
   HEADINGS AND EYEBROWS
   ═══════════════════════════════════════════════════════════════════ */
.headline, .section-title, .final-title, .about-title { color: #1B1F7A; }   /* 13.5:1 */
.eyebrow, .pill,
#problem .role-card .eyebrow                          { color: #1B2A9E; }   /* 11.0:1 */


/* ═══════════════════════════════════════════════════════════════════
   FAQ  ·  heading column left, cards right
   The head was a full-width flex row with the aside pushed to the far
   right, and the questions ran beneath it in two columns. Now the
   section is one two-column grid: heading, sub and CTA stack in the
   left column, every question in a single column on the right.

   .faq-head and .faq-grid are siblings, so the grid goes on the
   container and each becomes a column — no wrapper element needed.
   ═══════════════════════════════════════════════════════════════════ */
#faqs .container {
  display: grid;
  grid-template-columns: minmax(260px, 360px) 1fr;
  gap: 56px;
  align-items: start;
}

#faqs .faq-head {
  display: block;
  margin-bottom: 0;
}

/* Was right-aligned against the far edge of the section; under the
   heading it reads left like everything else in the column. */
#faqs .faq-aside {
  text-align: left;
  justify-items: start;
  margin-top: 20px;
}
#faqs .faq-aside p { max-width: none; }

#faqs .faq-grid { grid-template-columns: 1fr; }

@media (max-width: 900px) {
  #faqs .container { grid-template-columns: 1fr; gap: 32px; }
}


/* ═══════════════════════════════════════════════════════════════════
   BOOK A DEMO  ·  10px between heading, sub and button
   ═══════════════════════════════════════════════════════════════════ */
.final-title { margin-bottom: 0; }
.final-sub   { margin: 10px 0 10px; }


/* — How it works · sub aligns to the new export ————————————
   1419x507 with a 12px transparent border, so the visible artwork is
   1395 wide. The head is capped to that, not to the file's width, or
   the sub would stop 12px short of the card it is meant to line up
   with. ————————————————————————————————————————————— */
#how .section-head { max-width: 1395px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  the six source cards take the hero field
   Same five layers as the hero, so the two read as the same material.

   Two things had to move with the fill. The label was white, set when
   these carried a dark gradient — on a pale card that is invisible, so
   it takes the heading colour. And the glow was a blue bloom sized to
   lift a dark card off a dark ground; against #F7F7FD it only muddies
   the edge, so it becomes a hairline and a soft drop instead.
   ═══════════════════════════════════════════════════════════════════ */
.ics-source {
  color: #1B1F7A;                      /* 9.3:1 at the field's darkest */
  background:
    radial-gradient(90% 95% at 88% 6%,   #CDD3F2 0%, transparent 58%),
    radial-gradient(70% 80% at 12% 4%,   #FBEFF3 0%, transparent 55%),
    radial-gradient(105% 110% at 6% 96%, #D5DAF4 0%, transparent 62%),
    radial-gradient(85% 90% at 100% 78%, #E9E4F5 0%, transparent 50%),
    linear-gradient(118deg, #EDEFFB 0%, #F5F5FD 42%, #FFFAFA 70%, #E8EBF9 100%);
  border: 1px solid rgba(27, 31, 122, .12);
  box-shadow: 0 8px 20px -14px rgba(27, 31, 122, .35);
}


/* ═══════════════════════════════════════════════════════════════════
   MERGING CUBE  ·  from pulse-cube-light-exact.html
   Copied from the prototype. One alteration, described below.

   The overlay sits at z-index 0 and the content wrapper at 1, so the
   page reads over the animation. body::before, the page atmosphere,
   is at -1 and stays underneath both.
   ═══════════════════════════════════════════════════════════════════ */
#lines { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
#field { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }

/* THE ALTERATION. Two of them, both required.

   First, scope. The prototype styles a bare `.tile`, but this page
   already uses .tile and .tile-face for the hero dots. An unscoped
   rule would turn each of those into a purple rectangle.

   Second, colour. The prototype's tile is a pale lavender sampled from
   its own dot.png. This page has its own dot cube, and that is the one
   the flowing cube is built from now, so the fill is rebuilt from the
   hero tile's recipe: the same 150deg angle, the same mix of the tint
   with 8% white at the top and with --indigo-900 at 40% at the bottom,
   and a glow in the same family. The geometry is left at 129 by 159
   because the script divides by TILE_W to work out its scale. */
#field .tile {
  position: absolute; top: 0; left: 0;
  width: 129px;
  height: 159px;
  border-radius: 28px;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--indigo-700) 92%, white 8%) 0%,
    color-mix(in srgb, var(--indigo-700) 60%, var(--indigo-900) 40%) 100%);
  box-shadow: 0 0 46px rgba(27, 42, 158, .34);
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}

/* The wrapper that holds all content. No z-index on individual
   sections, per the brief. */
main { position: relative; z-index: 1; }

/* HERO SCRIM. Needed, not optional: the headline is #1B1F7A and a tile
   at full strength is #6659e2, which measures 2.69:1 where one passes
   behind the type. The prototype's rule, with --scrim set to this
   page's own hero ground so the wash is invisible against it.
   .hero::before was disabled earlier in this file, so it is re-enabled
   here rather than edited in place. */
:root { --scrim: rgba(245, 245, 253, .94); }
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 62% 44% at 50% 46%, var(--scrim) 0%, transparent 68%);
  z-index: 0;
  pointer-events: none;
}


/* ═══════════════════════════════════════════════════════════════════
   WHY THE CUBE WAS INVISIBLE
   #field is fixed at z-index 0 and the content wrapper is at z-index 1,
   which is the correct order. But every section was painted with a
   fully opaque ground, and those grounds live inside the wrapper. So
   the animation was rendering the whole time, perfectly, behind an
   unbroken sheet of #FFFAFA.

   No z-index can solve this. A section's background and its own text
   are the same element, so the cube is either behind both or in front
   of both. The grounds therefore carry alpha: same two tones, same
   alternation, now letting the field read through. Body text measures
   11:1 at the worst point, where a tile sits directly behind a card.
   ═══════════════════════════════════════════════════════════════════ */
.integrations,
#problem, #revenue, #marketing, #about, #demo,
.final, .footer-shell   { background: rgba(255, 250, 250, .82) !important; }

#how, #ics, #faqs       { background: rgba(247, 247, 253, .82) !important; }

/* The hero is where the tiles scatter, so its ground has to let them
   through too. Same five layers, same stops, alpha on each. */
.hero {
  /* A light diagonal running top-left to bottom-right, with the other
     two corners dark. The blooms are anchored AT the corners rather
     than inset, so each one owns its corner instead of drifting toward
     the middle where the headline sits.

     Still .82 on every layer: the hero ground sits over the fixed cube
     field, and at full opacity it hides it completely. Body #FFFAFA is
     what shows through the remaining 18%.

     The dark corners peak at #B5BAD9. The copy is centred and never
     reaches them: the darkest ground under it is #DFE1EF, where the
     headline holds 10.6:1 and the lede 6.0:1. */
  background:
    /* the two light corners */
    radial-gradient(72% 82% at  4%   2%, rgba(255, 252, 254, .82) 0%, transparent 58%),
    radial-gradient(82% 92% at 97% 100%, rgba(253, 250, 255, .82) 0%, transparent 60%),

    /* the two dark ones */
    radial-gradient(86% 94% at 100%  0%, rgba(165, 172, 210, .82) 0%, transparent 62%),
    radial-gradient(92% 98% at  0%  100%, rgba(169, 176, 212, .82) 0%, transparent 64%),

    /* base kept flat and pale, so the corners do the work */
    linear-gradient(118deg,
      rgba(243, 244, 252, .82) 0%,
      rgba(248, 248, 253, .82) 50%,
      rgba(244, 245, 252, .82) 100%) !important;
}

/* — Problem widget. The new gif carries real transparency, 97% of the
     frame, so it needs no blend mode and no panel behind it. —————— */
.problem-widget { display: block; width: 100%; height: auto; mix-blend-mode: normal; }


/* ═══════════════════════════════════════════════════════════════════
   SUITE SCREENS  ·  drop shadow, 0 / 3 / 18 at 12%
   The dashboard images only. Not the menu rows beside them, not the
   FAQ or problem cards, which keep the shadows they already had.

   A filter rather than a box-shadow: these exports carry transparent
   padding and rounded corners, so a box shadow would trace the file's
   rectangle instead of the artwork inside it. drop-shadow follows the
   alpha channel and hugs the real shape.
   ═══════════════════════════════════════════════════════════════════ */
.suite-screen { filter: drop-shadow(0 3px 18px rgba(10, 15, 46, .12)); }


/* The old hero dots are back and own the hero on their own. The cube
   field no longer appears there at all: its journey now starts at the
   problem section, so at the top of the page it is parked off the
   right edge. ——————————————————————————————————————— */

/* — Logos must not have tiles passing behind them, but a flat opaque
     block gave the cube a hard edge to disappear at: it was simply
     sliced off at the section boundary.

     The ground is opaque only across the band the logos occupy, and
     fades to the page alpha at both ends. The cube now dissolves as it
     passes behind the row rather than being cut, and the logos still
     have solid ground under them. ————————————————————————— */
.integrations {
  background: linear-gradient(180deg,
    rgba(255, 250, 250, .82)  0%,
    rgba(255, 250, 250, .94) 14%,
    #FFFAFA                  30%,
    #FFFAFA                  72%,
    rgba(255, 250, 250, .94) 88%,
    rgba(255, 250, 250, .82) 100%) !important;
}


/* ═══════════════════════════════════════════════════════════════════
   HERO DOTS  ·  back to the original recipe
   The light-theme pass rewrote .tile-face as a flat --indigo-400 to
   --indigo-600 ramp, #5660FF to #2B33C4. That is the pale cube in the
   hero, and it is what has been overriding the dot's real colour all
   along: the rule sits later in this file than the one in the page's
   own style block, so it wins.

   This restores the original: each tile mixed from its OWN --tint,
   with 8% white at the top and 40% --indigo-900 at the bottom, which
   is why the field had a range of depths rather than one flat blue.
   The bloom and the blur on the soft ones come back with it.
   ═══════════════════════════════════════════════════════════════════ */
.tile-face {
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--tint) 92%, white 8%) 0%,
    color-mix(in srgb, var(--tint) 60%, var(--indigo-900) 40%) 100%);
  box-shadow: none;
}

.tile.is-lit .tile-face {
  box-shadow:
    0 0 var(--bloom, 60px) color-mix(in srgb, var(--tint) 55%, transparent),
    0 0 calc(var(--bloom, 60px) * 2.2) color-mix(in srgb, var(--tint) 26%, transparent);
}

.tile.is-soft .tile-face { filter: blur(calc(var(--size) * .06)); }


/* ═══════════════════════════════════════════════════════════════════
   THE DOT  ·  the asset itself, not a copy of it
   Every attempt so far has rebuilt this tile as a CSS gradient, and
   each one has been a near miss. dot.png carries a glow that fades to
   full transparency at the corners, so the fill is not a plain corner
   to corner ramp and no set of stops reproduces it exactly.

   So the artwork is used directly. Colour, gradient, corner radius and
   glow now come from your file rather than from my reading of it, and
   they cannot drift again.

   Both fields use it: the ten in the hero and the twenty seven that
   form the flowing cube. `contain` keeps its aspect, so the slight
   difference between the file's 0.82 and the script's 0.81 letterboxes
   by under a pixel instead of stretching.
   ═══════════════════════════════════════════════════════════════════ */
#field .tile,
.tile-face {
  background: url('dot.png') center center / contain no-repeat;
  border-radius: 0;
  box-shadow: none;
  filter: none;
}

/* The lit ones keep their beat through opacity, which the asset's own
   glow already supports. No extra bloom: dot.png has one. */
.tile.is-lit .tile-face { box-shadow: none; }
.tile.is-soft .tile-face { filter: none; }


/* ═══════════════════════════════════════════════════════════════════
   HERO DOTS  ·  larger, with depth
   Size comes off each tile's own --size, so one multiplier grows the
   whole field while keeping the spread the composition was built on:
   the 20px dots stay the small ones, the 104px stay the large.

   Depth is built from three cues rather than one, because size alone
   on a flat field reads as scale, not distance:

     · a drop shadow whose offset and blur scale WITH the tile, so a
       large dot throws a long shadow and a small one barely any. A
       fixed shadow on every tile would have flattened them instead.
     · the soft ones keep their blur, also scaled by size, which puts
       them off the focal plane.
     · opacity, already per tile through --alpha, left alone.

   Shadow rather than glow: dot.png carries its own glow, and stacking
   a second one on top only smears the edge.
   ═══════════════════════════════════════════════════════════════════ */
.hero .tile {
  width: calc(var(--size) * 1.7);
  height: calc(var(--size) * 1.7 * 1.12);
}

.hero .tile-face {
  filter: drop-shadow(0 calc(var(--size) * .09) calc(var(--size) * .20) rgba(18, 24, 79, .30));
}

.hero .tile.is-soft .tile-face {
  filter:
    blur(calc(var(--size) * .07))
    drop-shadow(0 calc(var(--size) * .09) calc(var(--size) * .20) rgba(18, 24, 79, .22));
}


/* ═══════════════════════════════════════════════════════════════════
   THE HERO FIELD, PAGE-WIDE
   The same five layers now span the whole document rather than one
   section, so the light diagonal runs from the top-left of the page to
   the bottom-right and the two dark corners sit at the page's corners.

   Painted on body at 100% by 100% with scroll attachment, NOT fixed.
   Fixed would pin the corners to the viewport, so every paragraph
   would scroll through the dark ones in turn. Spanning the document
   instead means the corners are thousands of pixels apart and the
   reading column stays in the light middle.

   Full opacity is safe here because this sits below #field rather than
   above it. The .82 the hero carried existed only so the cube could
   read through it; below the cube, no alpha is needed.

   The corner radii are 14 to 17 percent VERTICALLY, not the 82 to 98
   the hero used. Those were percentages of one section; here they are
   percentages of the whole document, and at hero values each corner
   swallowed half the page. At full strength the dark corners fail body
   text at 3.3:1, so they are kept to the top and bottom bands, where
   the hero's centred headline and the footer's own card sit rather
   than any running copy.
   ═══════════════════════════════════════════════════════════════════ */
body {
  /* The reference gradient: white at the top-left and bottom-right,
     periwinkle at the top-right and bottom-left. Both blues are the
     same value and the same shape, as are both whites, so the two
     diagonals stay balanced.

     The blues are held to a 32% horizontal radius rather than the ~70%
     the reference shows. That is a readability limit, not a taste one:
     at the reference's own spread the blue reaches #B7BEFF inside the
     reading column, where body copy measures 4.15:1 and FAQ answers
     3.44:1, both under the floor. At 32% the darkest the column sees
     is #DBDEFF, and everything clears. */
  background:
    radial-gradient(62% 26% at   0%   0%, #FFFFFF 0%, transparent 58%),
    radial-gradient(70% 26% at 100% 100%, #FFFFFF 0%, transparent 60%),
    radial-gradient(32% 26% at 100%   0%, #9BA4FF 0%, transparent 66%),
    radial-gradient(32% 26% at   0% 100%, #A6ADFF 0%, transparent 64%),
    #FBFBFF;
  background-attachment: scroll;
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

/* Sections carried their own grounds, which would hide the field the
   way they hid the cube. They come off; the page ground shows through
   every one of them now. */
.hero,
.integrations,
#problem, #revenue, #marketing, #about, #trust, #demo,
#how, #ics, #faqs,
.footer-shell { background: transparent !important; }

/* The one exception. Logos still need solid ground so the cube does
   not pass behind them, and it still fades at both ends so the cube
   dissolves rather than being cut. */
.integrations {
  background: linear-gradient(180deg,
    rgba(255, 252, 254, 0)   0%,
    rgba(255, 252, 254, .72) 16%,
    rgba(255, 252, 254, .94) 32%,
    rgba(255, 252, 254, .94) 70%,
    rgba(255, 252, 254, .72) 86%,
    rgba(255, 252, 254, 0)  100%) !important;
}


/* — No dots in the hero. The field's journey already starts at the
     problem section, so the hero is left to its copy alone. —————— */

/* — The logo strip takes the page ground like every other section.
     This drops the protective band, so the cube now passes behind the
     logos rather than dissolving before them. ————————————————— */
.integrations { background: transparent !important; }

/* — Problem cards, solid white ——————————————————————————— */
#problem .role-card,
#problem .card, #problem .glass.card {
  background: #FFFFFF;
  border: 1px solid rgba(10, 15, 46, .10);
}
#problem .role-card:hover,
#problem .card:hover,
#problem .card:has(details[open]) {
  background: #FFFFFF;
  border-color: rgba(62, 81, 255, .26);
}


/* ═══════════════════════════════════════════════════════════════════
   FOOTER  ·  let the bottom-left corner through
   The corner was never light. .footer is a card filled #EFF1FB and it
   covers almost the whole bottom of the page, so what you were seeing
   there was the card, not the gradient.

   Dropping the fill to 28% white lets the dark corner read through
   while the card keeps its shape. That costs contrast, so the small
   footer text moves with it: over the corner the card resolves to
   #BEC3DF, where #5A6180 measured 2.7:1 and #4A5169 holds 4.5:1.
   ═══════════════════════════════════════════════════════════════════ */
.footer {
  background: rgba(255, 255, 255, .28) !important;
  border: 1px solid rgba(10, 15, 46, .10);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
}

.footer-shell,
.footer-base, .footer-base a,
.footer-col a, .footer-brand .sub { color: #4A5169; }
.footer-brand p, .footer-col h3   { color: #1B1F7A; }


/* ═══════════════════════════════════════════════════════════════════
   THE PROBLEM  ·  orbit graphic
   From problem-orbit-animated.html. Geometry, arc paths, timings and
   the counter-rotation are unchanged.

   Every class is prefixed po-. The source uses .orbit, .card, .stage,
   .ring, .row, .label and .bar, and five of those already mean
   something here: .card alone carries twenty four rules on this page,
   and .orbit is the sticky wrapper this graphic sits inside. Dropping
   the names in unprefixed would have restyled the problem cards, the
   FAQ accordions and the suite panels.

   Tokens are scoped to .po-stage rather than :root for the same
   reason: --card, --ring and --ink are generic enough to collide.

   HOW IT MOVES. Two rotations at one speed in opposite directions.
   .po-orbit turns clockwise about the circle's centre and carries the
   arcs and cards with it; each .po-card turns anti-clockwise about its
   own middle. The second cancels the tilt of the first but not the
   travel, so the cards ride the circle with their text upright. Both
   must stay linear and share --po-spin or the text drifts out of true.
   ═══════════════════════════════════════════════════════════════════ */
.po-stage {
  --po-spin: 60s;
  --po-accent:      #2B33C4;
  --po-accent-deep: #1B1F7A;
  --po-surface-dk:  #0A0B21;
  --po-ink:         #14162E;
  --po-skeleton:    #DCDEE6;
  --po-ring:        #C4C6D8;
  --po-card:        #FFFFFF;
  --po-card-edge:   #EEEFF5;

  position: absolute;
  top: 0; left: 0;
  width: 760px;
  height: 760px;
  transform-origin: top left;
  transform: scale(var(--po-k, 1));
}

/* The stage is authored at a fixed 760px so the arc maths stays exact.
   This box gives it a fluid footprint and JS sets --po-k to the ratio,
   so it scales without any of the geometry being re-expressed. */
.po-fit {
  position: relative;
  width: 100%;
  max-width: 760px;
  aspect-ratio: 1;
  margin: 0 auto;
}

.po-glow {
  position: absolute; left: 245px; top: 245px;
  width: 270px; height: 270px; border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--po-accent) 52%, transparent) 0%,
    color-mix(in srgb, var(--po-accent) 26%, transparent) 42%,
    color-mix(in srgb, var(--po-accent) 8%,  transparent) 66%,
    transparent 78%);
  filter: blur(18px);
}

.po-disc {
  position: absolute; left: 265px; top: 265px;
  width: 230px; height: 230px; border-radius: 50%;
  background: #FFFFFF;
  box-shadow: 0 20px 50px rgba(20, 22, 48, .10);
}

.po-orbit {
  position: absolute; inset: 0;
  transform-origin: 380px 380px;
  animation: po-spin var(--po-spin) linear infinite;
  will-change: transform;
}

@keyframes po-spin    { from { transform: rotate(0deg); }  to { transform: rotate(360deg); } }
@keyframes po-counter { from { transform: rotate(0deg); }  to { transform: rotate(-360deg); } }

@media (prefers-reduced-motion: reduce) {
  .po-orbit, .po-card { animation: none !important; }
}

.po-ring { position: absolute; inset: 0; width: 760px; height: 760px; pointer-events: none; }
.po-ring path { fill: none; stroke: var(--po-ring); stroke-width: 1.3; stroke-linecap: round; }

.po-window {
  position: absolute;
  left: 310px; top: 331px;
  width: 140px; height: 98px;
  box-sizing: border-box;
  background: var(--po-surface-dk);
  border-radius: 7px;
  box-shadow: 0 14px 32px rgba(10, 11, 33, .28);
  padding: 8px;
}
.po-dots { display: flex; gap: 3.5px; margin-bottom: 7px; }
.po-dots i { width: 4px; height: 4px; border-radius: 50%; background: #8B90A4; }
.po-win-body { display: flex; gap: 7px; }
.po-win-side { display: flex; flex-direction: column; gap: 4px; width: 39px; }
.po-win-side b { height: 5px; border-radius: 2.5px; background: #6D7288; display: block; }
.po-win-side b:nth-child(2) { width: 78%; }
.po-win-side b:nth-child(4) { width: 64%; }
.po-win-panel { flex: 1; height: 66px; background: #31364D; border-radius: 4px; position: relative; }
.po-win-panel svg { position: absolute; inset: 0; width: 100%; height: 100%; }

.po-card {
  position: absolute;
  width: 200px; height: 89px;
  box-sizing: border-box;
  background: var(--po-card);
  border: 1px solid var(--po-card-edge);
  border-radius: 14px;
  padding: 15px 18px;
  box-shadow: 0 12px 32px rgba(20, 22, 48, .10);
  transform-origin: center center;
  animation: po-counter var(--po-spin) linear infinite;
  will-change: transform;
}

.po-mkt { left: 113px; top: 150px; }
.po-gm  { left: 113px; top: 521px; }
.po-rev { left: 529px; top: 353px; }

.po-row { display: flex; align-items: center; gap: 10px; }
.po-ico { display: inline-flex; width: 20px; height: 20px; flex: none; }
.po-ico svg {
  width: 20px; height: 20px;
  stroke: var(--po-accent-deep); stroke-width: 1.7;
  fill: none; stroke-linecap: round; stroke-linejoin: round;
}
.po-label { font-size: 15px; font-weight: 500; color: var(--po-ink); white-space: nowrap; }
.po-bar { height: 8px; border-radius: 4px; background: var(--po-skeleton); }
.po-bar.w1 { width: 100%; margin-top: 13px; }
.po-bar.w2 { width: 62%;  margin-top: 7px; }


/* ═══════════════════════════════════════════════════════════════════
   ORBIT  ·  radius 250 -> 300, cards 89 -> 58
   The ring grew by 50, so the stage grew by 100 and its centre moved
   from 380 to 430. Without that the Revenue card, which sits furthest
   right, would have ended at 779px on a 760px stage and hung outside
   the graphic. Every coordinate below is re-derived from the same
   formula the source uses, not nudged by eye:

       x = 430 + 300 * cos(angle)      y = 430 - 300 * sin(angle)

   The card boxes are each centred on their arc's end point, so the
   58px height changes their top offset as well as their size.

   The interior is re-proportioned to fit 58px. At the original
   padding and bar spacing the contents measured 86px, so they would
   have overflowed a 58px box by half its height.
   ═══════════════════════════════════════════════════════════════════ */
.po-stage  { width: 860px; height: 860px; }
.po-ring   { width: 860px; height: 860px; }
.po-orbit  { transform-origin: 430px 430px; }

.po-glow   { left: 295px; top: 295px; }
.po-disc   { left: 315px; top: 315px; }
.po-window { left: 360px; top: 381px; }

.po-card {
  height: 58px;
  padding: 9px 16px;
}
.po-mkt { left: 129.3px; top: 178.1px; }
.po-gm  { left: 129.3px; top: 623.9px; }
.po-rev { left: 629.3px; top: 421.9px; }

.po-row     { gap: 9px; }
.po-ico     { width: 18px; height: 18px; }
.po-ico svg { width: 18px; height: 18px; }
.po-label   { font-size: 13.5px; }

.po-bar     { height: 5px; border-radius: 2.5px; }
.po-bar.w1  { margin-top: 5px; }
.po-bar.w2  { margin-top: 4px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  scrolling metric cards
   From scroll-metric-cards.html. Prefixed sm-: .track is already in
   the markup here and .chip and .spark carry six rules between them.

   The six cards are written twice. The track slides up by exactly half
   its own height and jumps back, and because the halves are identical
   the jump is invisible. EDIT BOTH COPIES or it will jump each cycle.
   ═══════════════════════════════════════════════════════════════════ */
.sm-metrics {
  --sm-scroll: 26s;
  --sm-ink:       #14162E;
  --sm-ink-soft:  #6B6F80;
  --sm-card:      #FFFFFF;
  --sm-card-edge: #E7E8F2;
  --sm-up:        #12946A;
  --sm-up-bg:     #E6F4EE;
  --sm-down:      #C2413A;
  --sm-down-bg:   #FBECEB;

  width: 330px;
  height: 380px;
  margin: 0 auto;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 46px, #000 334px, transparent 380px);
          mask-image: linear-gradient(to bottom, transparent 0, #000 46px, #000 334px, transparent 380px);
}

.sm-track {
  display: flex; flex-direction: column; gap: 14px;
  animation: sm-scroll-up var(--sm-scroll) linear infinite;
  will-change: transform;
}
.sm-metrics:hover .sm-track { animation-play-state: paused; }

@keyframes sm-scroll-up { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@media (prefers-reduced-motion: reduce) { .sm-track { animation: none; } }

.sm-card {
  flex: none;
  height: 84px; box-sizing: border-box;
  background: var(--sm-card);
  border: 1px solid var(--sm-card-edge);
  border-radius: 14px;
  padding: 14px 16px;
  box-shadow: 0 6px 20px rgba(20, 22, 48, .06);
  display: flex; align-items: center; gap: 14px;
}
.sm-main  { flex: 1; min-width: 0; }
.sm-label {
  font-size: 12px; font-weight: 500; color: var(--sm-ink-soft);
  letter-spacing: .04em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sm-value { font-size: 22px; font-weight: 600; color: var(--sm-ink); letter-spacing: -.01em; margin-top: 5px; }
.sm-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.sm-chip  { font-size: 12px; font-weight: 600; padding: 4px 9px; border-radius: 999px; white-space: nowrap; }
.sm-chip.up   { color: var(--sm-up);   background: var(--sm-up-bg); }
.sm-chip.down { color: var(--sm-down); background: var(--sm-down-bg); }
.sm-spark { display: block; width: 64px; height: 22px; }
.sm-spark polyline { fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.sm-spark.up   polyline { stroke: var(--sm-up); }
.sm-spark.down polyline { stroke: var(--sm-down); }


/* — Orbit cards, 20% larger. Width, height, padding, type and bars all
     scale together; growing the box alone would just add padding. The
     top and left offsets move too, because each card is centred on its
     arc's end point. ————————————————————————————————— */
.po-card {
  width: 240px;
  height: 70px;
  padding: 11px 19px;
  border-radius: 17px;
}
.po-mkt { left: 109.3px; top: 172.1px; }
.po-gm  { left: 109.3px; top: 617.9px; }
.po-rev { left: 609.3px; top: 415.9px; }

.po-row     { gap: 11px; }
.po-ico     { width: 22px; height: 22px; }
.po-ico svg { width: 22px; height: 22px; }
.po-label   { font-size: 16px; }
.po-bar     { height: 6px; border-radius: 3px; }
.po-bar.w1  { margin-top: 6px; }
.po-bar.w2  { margin-top: 5px; }


/* ═══════════════════════════════════════════════════════════════════
   BACKGROUND  ·  one fixed field, content scrolls over it
   Moved off body and onto a fixed layer. On body it was painted across
   the whole document, so it stretched and drifted as the page grew.
   Fixed to the viewport it is drawn once, at viewport size, and never
   repeats or seams: the content scrolls, the field does not.

   z-index -1 keeps it under #field, so the cube still reads over it.
   ═══════════════════════════════════════════════════════════════════ */
body {
  /* #F5F6FA is the last stop of the hero gradient and the colour
     .hero::after fades to, so the page now continues in exactly the
     colour the hero ends on and the join has nothing to show.
     Was #F7F8FC — two units lighter, which is not a colour difference
     you can name but is enough to draw a line where two flat fields
     meet. */
  background: #F5F6FA !important;
}

body::before {
  content: '';
  display: block;
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  filter: none;
  opacity: 1;
  background:
    radial-gradient(62% 26% at   0%   0%, #FFFFFF 0%, transparent 58%),
    radial-gradient(70% 26% at 100% 100%, #FFFFFF 0%, transparent 60%),
    radial-gradient(32% 26% at 100%   0%, #9BA4FF 0%, transparent 66%),
    radial-gradient(32% 26% at   0% 100%, #A6ADFF 0%, transparent 64%),
    #FBFBFF;
}


/* — ICS source labels in bold ————————————————————————————— */
.ics-source { font-weight: 700; }

/* — The metrics column now sits in the problem section, where the
     orbit was. That slot is the sticky left column, so it keeps its
     own centring rather than the orbit's fluid square. ——————— */
#problem .sm-metrics { margin: 0 auto; }


/* — The ICS orb's artwork comes off, its box stays. The wire layout
     measures .ics-orb to find where the six wires converge, so the
     element has to keep its dimensions. visibility hides it and leaves
     the box; display:none would collapse it and break the diagram. —— */
.ics-orb {
  visibility: hidden;
  filter: none;
}

/* The metrics take the orb's place, centred on the same point. */
.ics-orb-wrap .sm-metrics {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
}


/* — ICS metrics: 10% smaller, 30px lower ————————————————————
     Scaled rather than re-dimensioned, so the mask stops, card heights
     and type all come down together. Re-typing 330 as 297 would have
     shrunk the frame while leaving the cards inside it their old size. */
.ics-orb-wrap .sm-metrics {
  transform: translate(-50%, -50%) scale(.9);
  margin-top: 30px;
}


/* — Hero dots, fewer of them ————————————————————————————
     They were hidden outright a few passes back. Back on, showing five
     of the ten: the first five are the smaller, scattered ones, so the
     field reads lighter rather than just sparser. ————————————— */


/* ═══════════════════════════════════════════════════════════════════
   BACKGROUND  ·  the reference spread
   The blues now run to a 70% horizontal radius, which is what the
   image shows, instead of the 32% I had held them to.

   That costs contrast and two tiers move to pay for it. At this spread
   the blue reaches #B7BEFF inside the reading column, where the old
   body colour measured 4.15:1 and answers 3.44:1. #454B61 clears both
   at 4.87:1. The gap between body and answers closes as a result:
   there is not room for three separable greys on this ground.
   ═══════════════════════════════════════════════════════════════════ */
body::before {
  background:
    radial-gradient(62% 26% at   0%   0%, #FFFFFF 0%, transparent 58%),
    radial-gradient(70% 26% at 100% 100%, #FFFFFF 0%, transparent 60%),
    radial-gradient(70% 30% at 100%   0%, #9BA4FF 0%, transparent 68%),
    radial-gradient(70% 30% at   0% 100%, #A6ADFF 0%, transparent 66%),
    #FBFBFF;
}

.section-sub, .role-body,
.acc-body, .why-body { color: #454B61; }


/* ═══════════════════════════════════════════════════════════════════
   BACKGROUND  ·  the actual file
   hero background.png, 1728x1117. Every previous pass rebuilt this as
   CSS radial gradients and each was a near miss, in the same way the
   dot was before dot.png went in directly. The file is the source now.

   Fixed and covering: drawn once at viewport size, so it never repeats
   or seams, and the content scrolls over it. z-index -1 keeps it under
   the cube field.

   Sampled corners: #FFFFFF top-left, #A5B1FF top-right,
   #A3AEFF bottom-left, #FFFFFF bottom-right.
   ═══════════════════════════════════════════════════════════════════ */
body::before {
  /* The washes, in place of the export. Two soft blues over the
     near-white ground: one off the top-right corner, one low on the
     left. page-bg.png is left on disk, just no longer referenced here.

     Longhands, not the shorthand it replaces, so position / inset /
     z-index / pointer-events / opacity all stay exactly where the
     rule above set them. !important because the declaration being
     replaced carried it, and size / repeat / position are each reset
     by name because the shorthand set all three for the png. */
  background-image:
    radial-gradient(620px 560px at calc(100% + 170px) 120px, rgba(45, 77, 224, .18), transparent 70%),
    radial-gradient(480px 480px at 40px 500px, rgba(123, 140, 255, .14), transparent 70%) !important;
  background-color: transparent !important;
  background-size: auto !important;
  background-repeat: no-repeat !important;
  background-position: 0 0 !important;
}

/* The real image runs its blue further in than my reproduction did.
   Measured across the reading column it reaches #A0ACFF, where the
   previous body colour fell to 4.05:1. This clears it at 4.62:1. */
.section-sub, .role-body,
.acc-body, .why-body { color: #3C4257; }

/* — Hero dots off again ————————————————————————————————— */


/* ═══════════════════════════════════════════════════════════════════
   HERO DOTS  ·  back on
   All ten, with their drift, their beat and the arrival sequence that
   gathers them on load. The earlier display:none rules are removed
   rather than overridden, so there is one statement about these
   elements in the file instead of three contradicting ones.

   The swarm that used to cross the hero was never these: it was the
   cube field's load animation, and that is held back separately now.
   ═══════════════════════════════════════════════════════════════════ */
.hero .tiles { display: block; }


/* — Orbit graphic, 20% larger ————————————————————————————
     Grown by widening its box rather than by scaling past it. The
     stage is sized by JS from fit.clientWidth, so a wider fit scales
     everything inside it in one step and the ResizeObserver already
     picks it up.

     The extra 20% is split evenly into both gutters with the negative
     margin, so the graphic stays centred on its column instead of
     spilling only to the right, where the role cards are. ————— */
#problem .po-fit {
  width: 120%;
  margin-left: -10%;
  margin-top: -30px;
  max-width: none;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRICS  ·  static panel, not a loop
   The scroll is off and the duplicate half of the list is gone from
   the markup. That second copy only existed so the track could slide
   by exactly half its height and jump back invisibly; with nothing
   moving it was six repeated cards.

   The panel takes the suite screens' treatment: white ground, the same
   hairline border, and the same 0 3px 18px shadow at 12%, so the two
   read as the same kind of surface.

   Card height and gap come down from 84/14 to 72/10. Six cards at the
   old figures measured 574px before padding, against a 500px stage.
   At these they come to 482, and the panel lands at 518 including its
   own padding.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-metrics {
  width: 330px;
  height: auto;
  overflow: visible;
  -webkit-mask-image: none;
          mask-image: none;

  padding: 18px;
  background: #FFFFFF;
  border: 1px solid rgba(10, 15, 46, .10);
  border-radius: 18px;
  box-shadow: 0 3px 18px rgba(10, 15, 46, .12);

  transform: translate(-50%, -50%) scale(1);
}

.ics-orb-wrap .sm-track {
  animation: none;
  gap: 10px;
}
.ics-orb-wrap .sm-metrics:hover .sm-track { animation: none; }

.ics-orb-wrap .sm-card {
  height: 72px;
  padding: 12px 14px;
  box-shadow: none;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRICS  ·  the four rows from the reference
   Name on the left behind a small marker, figure on the right with an
   up chevron. Four rows, not six, because that is what the reference
   holds.

   The previous structure carried a small caps label, a large value, a
   percentage chip AND a sparkline. The reference has one figure per
   row, so the value and the sparkline are gone rather than left empty:
   the figure moved into the position the chip occupied.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  display: flex;
  align-items: center;
  gap: 11px;
  height: 62px;
  padding: 0 16px;
}

.sm-dot {
  flex: none;
  width: 9px; height: 9px;
  border-radius: 50%;
  background: #12946A;
  box-shadow: 0 0 0 3px rgba(18, 148, 106, .16);
}

.ics-orb-wrap .sm-label {
  flex: 1; min-width: 0;
  font-size: 14.5px; font-weight: 600;
  letter-spacing: 0; text-transform: none;
  color: #14162E;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.sm-trend { display: flex; align-items: center; gap: 6px; flex: none; }
.sm-arrow { width: 12px; height: 12px; }
.sm-arrow polyline {
  fill: none; stroke: #12946A; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.sm-figure { font-size: 14.5px; font-weight: 600; color: #12946A; white-space: nowrap; }








/* — Cube stops 1px up ————————————————————————————————————— */

/* — The hero's second line stays on one line ————————————————— */
.headline .nowrap { white-space: nowrap; }


/* ═══════════════════════════════════════════════════════════════════
   THE 3D LATTICE  ·  from pulse-cube-3d-dot.html
   Prefixed c3-: the source uses .cube, .stage, .scene, .tilt, .spin
   and .f, and .cube, .stage and .f are all taken on this page.

   Two instances. One is fixed and travels with the scroll, taking over
   from the flat tile field the moment the dots finish merging. The
   other is static in the ICS section, where the gif used to be.

   The measured gradient, the 13.3% radius and the per-face darkening
   are the source's, unchanged. Sizes are down from 54/84 to 48/76 so
   the lattice spans exactly the 200px the journey is built around:
   two gaps plus one cube.
   ═══════════════════════════════════════════════════════════════════ */
.c3-rig {
  --c3-spin: 18s;
  --c3-breathe: 7s;
  --c3-cube: 48px;
  --c3-gap: 76px;
  --c3-dot: linear-gradient(51deg, #9D99ED 0%, #7D78E3 28%, #6963DE 52%, #625CDF 100%);
  --c3-sh-front: .10;
  --c3-sh-right: .15;
  --c3-sh-back:  .23;
  --c3-sh-left:  .28;

  position: fixed;
  top: 0; left: 0;
  width: 200px; height: 200px;
  z-index: 0;
  pointer-events: none;
  transform-origin: top left;
  opacity: 0;
}

.c3-rig.is-static {
  position: relative;
  width: 200px; height: 200px;
  opacity: 1;
  margin: 0 auto;
}

.c3-scene { perspective: 1500px; width: 100%; height: 100%; display: grid; place-items: center; }
.c3-tilt  { transform-style: preserve-3d; transform: rotateX(-35.264deg);
            animation: c3-breathe var(--c3-breathe) ease-in-out infinite alternate; }
.c3-spin  { transform-style: preserve-3d; width: 0; height: 0;
            animation: c3-spin var(--c3-spin) linear infinite; }

@keyframes c3-spin    { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } }
@keyframes c3-breathe { from { transform: rotateX(-35.264deg) scale(1); }
                        to   { transform: rotateX(-35.264deg) scale(1.06); } }
@media (prefers-reduced-motion: reduce) { .c3-spin, .c3-tilt { animation: none; } }

.c3-cube {
  position: absolute;
  width: var(--c3-cube); height: var(--c3-cube);
  --c3-h: calc(var(--c3-cube) / 2);
  margin: calc(var(--c3-cube) / -2) 0 0 calc(var(--c3-cube) / -2);
  transform-style: preserve-3d;
  transform: translate3d(calc(var(--x) * var(--c3-gap)),
                         calc(var(--y) * var(--c3-gap)),
                         calc(var(--z) * var(--c3-gap)));
}

.c3-f {
  position: absolute; inset: 0;
  border-radius: 13.3%;
  background-image: var(--c3-dot);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14);
}
.c3-f.front { background-image: linear-gradient(rgba(24,21,86,var(--c3-sh-front)),rgba(24,21,86,var(--c3-sh-front))), var(--c3-dot); }
.c3-f.right { background-image: linear-gradient(rgba(24,21,86,var(--c3-sh-right)),rgba(24,21,86,var(--c3-sh-right))), var(--c3-dot); }
.c3-f.back  { background-image: linear-gradient(rgba(24,21,86,var(--c3-sh-back)), rgba(24,21,86,var(--c3-sh-back))),  var(--c3-dot); }
.c3-f.left  { background-image: linear-gradient(rgba(24,21,86,var(--c3-sh-left)), rgba(24,21,86,var(--c3-sh-left))),  var(--c3-dot); }

.c3-f.top   { transform: rotateX(90deg)  translateZ(var(--c3-h)); }
.c3-f.front { transform: translateZ(var(--c3-h)); }
.c3-f.back  { transform: rotateY(180deg) translateZ(var(--c3-h)); }
.c3-f.right { transform: rotateY(90deg)  translateZ(var(--c3-h)); }
.c3-f.left  { transform: rotateY(-90deg) translateZ(var(--c3-h)); }

/* The ICS instance keeps the box the wire layout measures. */
.ics-cube.c3-rig { display: block; }


/* — No edge lines on the lattice faces ————————————————————————
     The source draws a 1px inset highlight on every face. On 27 cubes
     that reads as a wireframe over the whole object rather than as an
     edge on each one. ————————————————————————————————— */
.c3-f { box-shadow: none; }

/* — The ICS instance at the size the old cube actually read ————
     The video box was 260px, but its artwork sat inset about 17% each
     side, so the cube on screen was roughly 170. Matching the box
     rather than the artwork would have made it noticeably bigger than
     what it replaced. ——————————————————————————————————— */
.ics-cube.c3-rig.is-static {
  width: 170px;
  height: 170px;
}
.ics-cube.c3-rig .c3-cube { --c3-cube: 41px; }
.ics-cube.c3-rig          { --c3-gap: 64px; }


/* — The ICS cube starts hidden; the render loop fades it up as the
     travelling cube lands on it. Without this both would be on screen
     together for the whole approach. ————————————————————— */
.ics-cube.c3-rig.is-static { opacity: 1; }

/* — The ICS caption, 10px lower ————————————————————————————— */


/* — 1. Cube stops off, ordinary full stops back —————————————
     The marks are hidden and the real stop, which was held off-screen
     for screen readers, is returned to the flow. The script that
     splits the headings stays: it is inert once these two rules are
     in, and reversing it in CSS keeps the change to one place. ——— */

/* — 2. ICS caption at 25px ————————————————————————————————— */

/* — 3. The ICS lattice, 10px right ————————————————————————
     A margin rather than a transform: the wire layout measures this
     element to find where the six wires converge, and a transform
     would move the artwork while leaving the measured box behind. — */
.ics-cube.c3-rig.is-static { margin-left: 10px; }


/* — 4. One card, turning through the data ————————————————— */
.ics-orb-wrap .sm-card {
  position: relative;
  transform-style: preserve-3d;
  will-change: transform;
}
.ics-orb-wrap .sm-card.is-turning { animation: sm-turn .62s ease-in-out both; }
@keyframes sm-turn {
  0%   { transform: rotateX(0deg); }
  50%  { transform: rotateX(90deg); }
  100% { transform: rotateX(0deg); }
}

.sm-face {
  display: none;
  align-items: center;
  gap: 11px;
  width: 100%;
}
.sm-face.is-on { display: flex; }

/* — 6. The demo card's second line at half strength ——————————— */
.final-dim { opacity: .5; }


/* ═══════════════════════════════════════════════════════════════════
   ICS LATTICE  ·  turning like the gif it replaced
   The gif rotated the whole block as one solid, steadily, on a single
   axis. Each cube was fixed within it.

   So the per-cube tumble from the last pass comes off. Twenty seven
   cubes each turning on two axes inside a block that is also turning
   reads as noise, and it is not what the reference did. What is left
   is one slow rotation of the lattice, at 24s rather than 18 to match
   the gif's pace, with the tilt and the breathe kept.
   ═══════════════════════════════════════════════════════════════════ */



/* — The ICS caption, 50px clear of the cube ————————————————
     Back to absolute. The rules before this set position: relative,
     which pulled the label out of the slot it was anchored in and
     dropped it into the grid flow, so every `top` I gave it measured
     from the wrong origin and it appeared not to move at all.

     Anchored to the cube's bottom edge again, the gap is the offset
     itself. ——————————————————————————————————————————— */
.ics-cube-label {
  position: absolute;
  left: 50%;
  top: calc(100% + 50px);
  transform: translateX(-50%);
}


/* — The ICS cube defaults to VISIBLE ————————————————————————
   It was opacity 0, relying on the render loop to fade it up as the
   travelling cube landed. That makes the section's whole subject
   dependent on one animation frame running: any error earlier in the
   loop, and the cube is simply not there.

   Visible by default, with the loop only DIMMING it during the
   approach, fails the safe way round. If the script never runs, the
   ICS section still has its cube. ——————————————————————— */




/* — The metrics panel on the wire's own line ————————————————
     Both wrappers are anchored at 42.4%, so the panel and the cube
     already shared a centre. The 30px offset added a few passes back
     was the whole discrepancy: the wire leaves the cube at that shared
     centre, and the panel sat 30px under it. ————————————— */
.ics-orb-wrap .sm-metrics { margin-top: 0; }


/* — The collapse is back ————————————————————————————————————
   The hero dots hold their drift while the hero is on screen, and run
   to the centre and shrink out as you scroll into the problem section,
   which is the behaviour they had before I neutralised it last pass. */

/* — The ICS caption, 80px clear of the cube ————————————————— */
.ics-cube-label { top: calc(100% + 80px); }






/* ═══════════════════════════════════════════════════════════════════
   THE LATTICE  ·  pulse-cube-3d-dot-spin.html
   The spin behaviour is replaced; the size and the journey are not.
   Both rigs keep their own --c3-cube and --c3-gap, so the travelling
   one still spans 200px and the ICS one 170.

   Two changes come from the new file:

   · The lattice turn is 5s with the GIF's own keyframes rather than a
     constant rotation. Timing stays linear so those stops control the
     speed: 75 deg/s in, 104 through the middle, 84 out, with a hold at
     each end. --breathe is 2.5s, which with `alternate` is a 5s round
     trip and stays in sync with it.

   · Each cube now turns on its own, on a .c3-spinner between the cube
     and its faces. That split matters: .c3-cube holds the lattice
     translate and .c3-spinner holds the rotation, so the two never
     fight over the transform property. My own attempt at this a few
     passes back animated the cube directly, which is why it had to
     carry the translate inside every keyframe.

   The per-cube durations, delays and directions in that file never
   reach the cubes: a misplaced quote in the style attribute leaves
   --dur, --delay and --dir outside it, so every cube falls back to the
   same 7s base. That is what the file actually plays, so that is what
   is reproduced here. Applying the intended values would have looked
   better and been a different animation from the one you asked for.
   ═══════════════════════════════════════════════════════════════════ */
.c3-rig {
  --c3-spin: 5s;
  --c3-breathe: 2.5s;
}

@keyframes c3-spin {
  0%, 13.33%   { transform: rotateY(0deg);   }
  25.33%       { transform: rotateY(45deg);  }
  42.67%       { transform: rotateY(135deg); }
  60.00%       { transform: rotateY(225deg); }
  78.67%       { transform: rotateY(315deg); }
  89.33%, 100% { transform: rotateY(360deg); }
}

.c3-spinner {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  animation-name: c3-cubespin;
  animation-duration: var(--c3-dur, 7s);
  animation-timing-function: linear;
  animation-delay: var(--c3-delay, 0s);
  animation-iteration-count: infinite;
  animation-direction: var(--c3-dir, normal);
}
@keyframes c3-cubespin { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } }

@media (prefers-reduced-motion: reduce) { .c3-spinner { animation: none; } }


/* Every cube on the same 7s base, direction normal, no offset, which
   is what the source renders. */
.c3-rig { --c3-cube-spin: 7s; }
.c3-spinner { animation-duration: var(--c3-cube-spin); animation-delay: 0s; animation-direction: normal; }


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARD  ·  the reference layout
   Name and a grey qualifier on the top row with the info mark pushed
   right, the figure large beneath it, and the change in a green pill
   at the foot. The card takes the suite screens' surface: white, the
   same hairline border, the same 0 3px 18px shadow at 12%.

   Your four readings are one figure each, where the reference carries
   two: a level (72.4%) and a change (3.2%). So the figure appears
   large AND in the pill rather than a level being invented to fill the
   space. Send me level values and the pill becomes the change on its
   own, as in your image.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-metrics {
  width: 314px;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
}

.ics-orb-wrap .sm-card {
  display: block;
  height: auto;
  padding: 18px 20px 20px;
  background: #FFFFFF;
  border: 1px solid rgba(10, 15, 46, .10);
  border-radius: 16px;
  box-shadow: 0 3px 18px rgba(10, 15, 46, .12);
}

.sm-face { display: none; }
.sm-face.is-on { display: block; }

.sm-head { display: flex; align-items: center; gap: 10px; }
.sm-name { font-size: 15px; font-weight: 700; color: #0A0F2E; white-space: nowrap; }
.sm-sub  { font-size: 13.5px; font-weight: 400; color: #7A8199; white-space: nowrap; }
.sm-info {
  margin-left: auto; flex: none;
  width: 17px; height: 17px;
  border: 1px solid rgba(10, 15, 46, .22);
  border-radius: 50%;
  font-size: 10.5px; font-style: italic; line-height: 15px;
  text-align: center; color: #7A8199;
}

.sm-big {
  margin: 14px 0 12px;
  font-size: 34px; font-weight: 700;
  letter-spacing: -.02em; color: #0A0F2E;
}

.sm-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 10px;
  border-radius: 999px;
  background: #E6F4EE;
  font-size: 13px; font-weight: 600; color: #12946A;
}
.sm-chip .sm-arrow { width: 11px; height: 11px; }
.sm-chip .sm-arrow polyline { stroke: #12946A; }


/* — The metric card on the page's glass surface ————————————————
     Copied from the .card rule the suites and every other panel use,
     rather than approximated: 72% white, an 18px backdrop blur at 1.1
     saturation, a 9% navy hairline and the same shadow. It was an
     opaque white card with a different border and shadow, which is why
     it did not sit with the rest. ————————————————————————— */
.ics-orb-wrap .sm-card {
  background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(10, 15, 46, .09);
  backdrop-filter: blur(18px) saturate(1.1);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  box-shadow: 0 10px 30px -18px rgba(10, 15, 46, .28);
  border-radius: 18px;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARD  ·  the four exports
   Each face is one image and the turn is unchanged: the card still
   rotates on its X axis and swaps face at the halfway point, while it
   is edge on.

   No glass frame behind them. The exports carry their own card, with
   rounded corners and transparency outside it, so a surface underneath
   would show as a second card sitting a few pixels proud of the first.
   The panel is the image.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.ics-orb-wrap .sm-face img {
  display: block;
  width: 100%;
  height: auto;
}

/* 634x364 at 314 wide comes to 180 tall. */
.ics-orb-wrap .sm-metrics { width: 314px; }


/* — The ICS cube and its caption, 25px right ————————————————
     Moved on the wrapper, so the cube and the label travel together.
     The 10px that was on the cube alone comes off: it shifted the cube
     out from under its own caption. The wires re-measure at runtime,
     so they follow. ——————————————————————————————————— */
.ics-cube.c3-rig.is-static { margin-left: 0; }



/* — ICS cube and caption, 80px right ————————————————————————
   Moved with a transform rather than a margin. The wrapper is
   absolutely positioned with `left: 48%` and already carries
   `translate(-50%, -50%)`; a margin on a box positioned that way is
   the kind of thing that resolves differently depending on how the
   rest of the box is constrained, and it was not shifting reliably.
   Appending translateX to the transform that is already there cannot
   be ignored, and the wire layout measures the transformed box, so
   the six wires follow it. ————————————————————————————— */
.ics-cube-wrap {
  margin-left: 0;
  transform: translate(-50%, -50%) translateX(80px);
}


/* — How it works · the stages, readable but not visible ——————————
     The clip-path technique rather than display:none or visibility:
     both of those take the text out of the accessibility tree, which
     would defeat the point. This keeps it in the tree and in the page
     for a crawler, and off the screen for everyone else. ————— */
.how-text {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* — Integrations column: names, not destinations ————————————
     These seven were links to "#". They are a list of what pulse.
     connects to, not places to go, and there is nowhere on this site
     to send someone for "RateGain". Linking each to its vendor would
     have been worse: a footer full of outbound links to other
     companies' marketing pages.

     They are spans now, styled as the column's other items but
     without the hover and the pointer that promise a destination. — */
.footer-col li span {
  font-size: 13.5px;
  color: var(--text-low);
  display: block;
  padding: 3px 0;
}


/* ═══════════════════════════════════════════════════════════════════
   FIXES  ·  reduced motion and dead weight
   ═══════════════════════════════════════════════════════════════════ */

/* sm-turn was the one continuous animation not switched off under
   reduced motion. The JS skips scheduling the turn, but the class and
   the keyframe still existed, so anything that added it would still
   animate. Covered now for certain. */
@media (prefers-reduced-motion: reduce) {
  .ics-orb-wrap .sm-card.is-turning { animation: none; }
}



/* — FAQ · no wash behind an open question ————————————————————
     .acc::after is a blurred three-point radial in #1B1F7A and #2B33C4
     that fades to .35 on hover and to full on open. That is the
     highlight: it sits behind the card, blurred at 46px, so it reads
     as the section lighting up rather than as part of the card.

     Off in all three states. The card keeps its glass fill and its
     border, so the open question still reads as open — the disclosure
     is the text appearing, not the background changing.

     Scoped to .acc. The same wash exists on .card, where the problem
     cards use it; left alone unless you want it gone there too. ——— */
.acc::after,
.acc:hover::after,
.acc[open]::after { opacity: 0; }


/* ═══════════════════════════════════════════════════════════════════
   ICS CARDS  ·  a scroll, not a flip
   The four cards run in a strip that slides up by exactly half its own
   height and restarts. The strip holds them twice, so the restart
   lands on an identical frame and the seam cannot be seen. Edit one
   copy and you must edit the other.

   This replaces the flip and its timer: the turn needed JavaScript to
   swap the face mid-rotation, a scroll needs none. Fewer moving parts,
   and it pauses on hover.

   The panel takes the revenue suite's card surface rather than a flat
   white one: 72% white over the page with an 18px backdrop blur, so
   the ground reads through it.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  --sm-scroll: 22s;

  width: 314px;
  height: 300px;
  padding: 16px;
  overflow: hidden;

  background: rgba(255, 255, 255, .72);
  border: 1px solid rgba(10, 15, 46, .09);
  backdrop-filter: blur(18px) saturate(1.1);
  -webkit-backdrop-filter: blur(18px) saturate(1.1);
  box-shadow: 0 10px 30px -18px rgba(10, 15, 46, .28);
  border-radius: 18px;

  /* the strip fades at both edges instead of being cut by the box */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 34px, #000 266px, transparent 300px);
          mask-image: linear-gradient(to bottom, transparent 0, #000 34px, #000 266px, transparent 300px);

  display: block;
  animation: none;
}

.sm-strip {
  display: flex;
  flex-direction: column;
  gap: 12px;
  animation: sm-scroll var(--sm-scroll) linear infinite;
  will-change: transform;
}
.ics-orb-wrap .sm-card:hover .sm-strip { animation-play-state: paused; }

@keyframes sm-scroll { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@media (prefers-reduced-motion: reduce) { .sm-strip { animation: none; } }

.sm-strip img { display: block; width: 100%; height: auto; flex: none; }


/* ═══════════════════════════════════════════════════════════════════
   ICS CARDS  ·  glass, and separated
   The panel was 72% white, which on a light page is close to opaque:
   the blur behind it had nothing to show through. Down to 22%, with
   the blur raised to 26px and a little saturation, so what reads is
   the ground softened rather than a white box.

   The cards are split apart too. At a 12px gap on a 300px window they
   ran as one column of artwork; 26px lets each one sit as its own
   card, which is what the exports are.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  background: rgba(255, 255, 255, .22);
  border: 1px solid rgba(255, 255, 255, .55);
  backdrop-filter: blur(26px) saturate(1.6);
  -webkit-backdrop-filter: blur(26px) saturate(1.6);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .60),
    0 14px 38px -22px rgba(10, 15, 46, .32);
  padding: 18px;
}

.sm-strip { gap: 26px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS CARDS  ·  four cards, not one panel
   The latest exports are content only: 29% ink on a transparent
   canvas, where the earlier ones carried their own filled card. So
   each needs a surface of its own rather than sharing the panel's.

   The panel therefore gives up its glass and keeps only the job of
   clipping the scroll and fading its edges. Otherwise there would be
   glass inside glass, and the blur would be doing its work twice.

   16px corners, and the same glassmorphism the panel had: 22% white,
   26px blur, a lit top edge.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  background: none;
  border: 0;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  padding: 0;
}

.sm-tile {
  flex: none;
  padding: 18px 20px;
  border-radius: 16px;
  background: rgba(255, 255, 255, .22);
  border: 1px solid rgba(255, 255, 255, .55);
  backdrop-filter: blur(26px) saturate(1.6);
  -webkit-backdrop-filter: blur(26px) saturate(1.6);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .60),
    0 14px 38px -22px rgba(10, 15, 46, .32);
}
.sm-tile img { display: block; width: 100%; height: auto; }

/* The exports vary by a few pixels in height, so the strip cannot
   assume a fixed card. Gap holds them apart instead. */
.sm-strip { gap: 18px; }


/* — Suite copy, two lines ——————————————————————————————————
     The second line sits at the body tier so the pair reads as a
     statement and a qualifier rather than two equal sentences. ——— */
.suite-copy .sc-b { color: #5A6178; }


/* ═══════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  centred, evenly spaced
   The gaps were 10px from the pill to the heading, 10px to the body,
   then 40px to the button — three different values set by three
   separate margin rules that had accumulated over several passes.

   A flex column with one gap replaces all of them. Every space is the
   same by construction rather than by three numbers agreeing, and
   align-items centres the button as a box rather than relying on
   text-align, which only centres it because it happens to be inline.
   ═══════════════════════════════════════════════════════════════════ */
.about > .container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  text-align: center;
}

/* The margins those elements carried are now the container's job. */
.about .pill,
.about-title,
.about-body {
  margin: 0;
}

/* Measure is kept: the heading stays short, the body stays readable. */
.about-title { max-width: 24ch; }
.about-body  { max-width: 62ch; }


/* — Dashboard copy, 20px left ————————————————————————————— */
.suite-copy { margin-left: -20px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS CARDS  ·  a slider, not a scroll
   Each card rises into place, holds, then the next follows. The strip
   holds the four twice, so the fourth step lands on an identical frame
   and the loop restarts unseen.

   Two things had to change before discrete steps were possible:

   · The cards were four different heights, 159 to 170px, because the
     exports differ. A slider steps by a fixed distance, so an uneven
     card would drift a few pixels further out of line on every step
     until the loop no longer matched. Each card is a fixed 200px with
     its artwork centred inside.

   · Spacing moved from `gap` to a margin on each card. With gap there
     is no space after the last card, so half the strip is not exactly
     half its content and -50% never lands where the loop needs it. A
     margin on every card makes the two halves identical.

   Step distance is therefore 200 + 18 = 218px, the same every time.
   ═══════════════════════════════════════════════════════════════════ */
.sm-strip {
  gap: 0;
  animation: sm-slide 16s cubic-bezier(.65, 0, .35, 1) infinite;
}

.sm-tile {
  height: 200px;
  margin-bottom: 18px;
  display: flex;
  align-items: center;
  box-sizing: border-box;
}

@keyframes sm-slide {
  0%,   20%  { transform: translateY(0); }
  25%,  45%  { transform: translateY(-12.5%); }
  50%,  70%  { transform: translateY(-25%); }
  75%,  95%  { transform: translateY(-37.5%); }
  100%       { transform: translateY(-50%); }
}

@media (prefers-reduced-motion: reduce) { .sm-strip { animation: none; } }


/* — Dashboard copy: 50px right of where it was ————————————————
     Was -20px. Moving it 50 to the right lands it at +30. ————— */
.suite-copy { margin-left: 30px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS SLIDER  ·  one card at a time, back to back
   The window is exactly one card tall and the cards sit flush against
   each other, so each slide swaps one card for the next with nothing
   of the neighbours showing. That is what makes the difference between
   them readable: before, a 300px window over 200px cards left part of
   two on screen at once and neither was the subject.

   It fixes the alignment as well. The panel centres on the orb, which
   sits on the same 42.4% line as the cube, so the wire meets it dead
   centre — but only once the window and the card are the same height.
   A 300px window showing a 200px card put the visible card off the
   line the wire arrives on.

   210px, not 200: the tallest export is 170px at this width and the
   card carries 36px of padding, so 200 would have clipped it.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  height: 210px;
  -webkit-mask-image: none;
          mask-image: none;
}

.sm-tile {
  height: 210px;
  margin-bottom: 0;
}

/* 8 cards x 210 = 1680. Each step is 210px, which is 12.5% of the
   strip, and -50% is exactly four cards, so the loop is seamless. */
@keyframes sm-slide {
  0%,   20%  { transform: translateY(0); }
  25%,  45%  { transform: translateY(-12.5%); }
  50%,  70%  { transform: translateY(-25%); }
  75%,  95%  { transform: translateY(-37.5%); }
  100%       { transform: translateY(-50%); }
}


/* — Dashboard copy, 5px left of where it was ————————————————— */
.suite-copy { margin-left: 25px; }


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  stacked card carousel
   Four cards on one spot rather than a strip. Each holds at the front,
   then steps back through three depths, fades out and returns. One
   keyframe drives all four; they differ only by a negative delay of a
   quarter cycle each, so at any moment there is one at every depth.

   The swap from back to front happens while the card is at zero
   opacity, which is what stops it reading as a jump: the position
   changes where nobody can see it, then it fades in at the front.

   The stack grows UPWARD, so the front card stays on the container's
   centre line. That matters here: the panel centres on the orb, which
   sits on the same 42.4% line as the cube, so the wire meets the front
   card dead centre rather than meeting the middle of a taller box.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card {
  height: 210px;
  overflow: visible;
}

.sm-stack {
  position: relative;
  height: 210px;
  animation: none;
}

.sm-stack .sm-tile {
  position: absolute;
  inset: 0;
  height: 210px;
  margin: 0;
  transform-origin: 50% 100%;
  animation: sm-carousel 16s cubic-bezier(.4, 0, .2, 1) infinite;
  animation-delay: calc(var(--i) * -4s);
  will-change: transform, opacity;
}

@keyframes sm-carousel {
  0%        { transform: translateY(0)     scale(1);    opacity: 0; z-index: 4; }
  4%,  20%  { transform: translateY(0)     scale(1);    opacity: 1; z-index: 4; }
  25%, 45%  { transform: translateY(-16px) scale(.945); opacity: .72; z-index: 3; }
  50%, 70%  { transform: translateY(-32px) scale(.89);  opacity: .42; z-index: 2; }
  75%, 92%  { transform: translateY(-48px) scale(.835); opacity: .18; z-index: 1; }
  96%,100%  { transform: translateY(-48px) scale(.835); opacity: 0;   z-index: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .sm-stack .sm-tile { animation: none; }
  .sm-stack .sm-tile:not(:first-child) { display: none; }
}


/* — ICS cards: blur at 300px ————————————————————————————————
     Up from 26. At this radius the backdrop is no longer a softened
     view of the page, it is an even wash of the colours behind it, so
     the card reads as frosted glass rather than as a lens.

     The white is dropped to 14% to suit it: at 22% over a backdrop
     this flat the two just add up to a pale panel, and the blur stops
     showing. Saturation carries the tint instead.
     ————————————————————————————————————————————————— */
.sm-tile {
  backdrop-filter: blur(300px) saturate(1.8);
  -webkit-backdrop-filter: blur(300px) saturate(1.8);
  background: rgba(255, 255, 255, .14);
}


/* — ICS cards: blur 500, and the fill raised with it ——————————
     The blur is set to 500 as asked. On its own it will not fix the
     show-through, and it is worth knowing why: Chrome and Safari clamp
     the backdrop sample well below this, so 500 and 150 render the
     same while 500 costs more to compute. Past that point a larger
     radius stops removing detail.

     What actually stops the page reading through the glass is the
     card's own fill. Blur smears the shapes behind it but keeps their
     colour and brightness, and behind these cards sits the wire
     diagram, which is high-contrast line work. 14% white left almost
     all of that variation intact.

     42% is the point where the backdrop settles into an even ground
     and the card's own text carries. Still glass, still tinted by what
     is behind it, no longer competing with it.
     ————————————————————————————————————————————————— */
.sm-tile {
  backdrop-filter: blur(500px) saturate(1.9);
  -webkit-backdrop-filter: blur(500px) saturate(1.9);
  background: rgba(255, 255, 255, .42);
  border-color: rgba(255, 255, 255, .70);
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  only the front card carries content
   The bleed-through is coming from inside the stack, not from the page
   behind it. The three cards waiting their turn sit directly under the
   front one, offset by only 16 to 48px, so almost all of each is
   covered — and the front card is glass, so their artwork reads
   straight through it. No amount of blur fixes that: backdrop-filter
   blurs what is behind, and blurred text at 72% opacity is still text.

   So the artwork fades with the card's turn. Each image is visible
   only while its card is at the front, on the same keyframe and the
   same delay as the card itself. The three behind stay as bare glass
   slabs peeking above the front one, which is what a stacked carousel
   should look like anyway.

   With nothing bleeding through, the fill comes back down to 30% and
   the cards read as glass again rather than as panels.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile img {
  animation: sm-face 16s cubic-bezier(.4, 0, .2, 1) infinite;
  animation-delay: calc(var(--i) * -4s);
}

@keyframes sm-face {
  0%          { opacity: 0; }
  4%,   20%   { opacity: 1; }
  24%,  100%  { opacity: 0; }
}

.sm-tile {
  background: rgba(255, 255, 255, .30);
  backdrop-filter: blur(500px) saturate(1.9);
  -webkit-backdrop-filter: blur(500px) saturate(1.9);
}

@media (prefers-reduced-motion: reduce) {
  .sm-stack .sm-tile img { animation: none; opacity: 1; }
}


/* — ICS stack: the move is quicker, the hold is not ————————————
     Each transition was 5% of a 16s cycle, 0.8s, which on an ease that
     starts and ends slowly reads as drifting rather than placing. It
     is 2.5% now, 0.4s, on a sharper curve that leaves quickly and
     settles hard.

     The cycle length is unchanged, so the time saved goes into the
     hold: each card now rests at the front for 3.2s instead of 2.56s.
     The cards place faster and then sit still, rather than the whole
     carousel simply spinning quicker.
     ————————————————————————————————————————————————— */
.sm-stack .sm-tile {
  animation-timing-function: cubic-bezier(.22, .9, .25, 1);
}

@keyframes sm-carousel {
  0%           { transform: translateY(0)     scale(1);    opacity: 0;   z-index: 4; }
  2.5%,  22.5% { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4; }
  25%,   47.5% { transform: translateY(-16px) scale(.945); opacity: .72; z-index: 3; }
  50%,   72.5% { transform: translateY(-32px) scale(.89);  opacity: .42; z-index: 2; }
  75%,   94%   { transform: translateY(-48px) scale(.835); opacity: .18; z-index: 1; }
  97%,   100%  { transform: translateY(-48px) scale(.835); opacity: 0;   z-index: 1; }
}

/* the artwork follows the same window */
.sm-stack .sm-tile img { animation-timing-function: cubic-bezier(.22, .9, .25, 1); }
@keyframes sm-face {
  0%            { opacity: 0; }
  2.5%,  22.5%  { opacity: 1; }
  24.5%, 100%   { opacity: 0; }
}


/* — ICS stack: 300ms moves, and no blank card behind ————————
     Two changes.

     The move is 1.9% of the 16s cycle, which is 304ms.

     And the white card you were seeing was the one at the first depth
     behind the front. It had already dropped its artwork but was still
     sitting at 72% opacity with a 70% white border, 16px above the
     front card — so what showed was a bright, blank glass edge. The
     depths now fall away much faster, 34 / 16 / 6 rather than
     72 / 42 / 18, and the border fades with them. They read as edges
     hinting at a stack instead of as cards in their own right.
     ————————————————————————————————————————————————— */
@keyframes sm-carousel {
  0%            { transform: translateY(0)     scale(1);    opacity: 0;   z-index: 4; border-color: rgba(255,255,255,0); }
  1.9%,  23.1%  { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4; border-color: rgba(255,255,255,.70); }
  25%,   48.1%  { transform: translateY(-16px) scale(.945); opacity: .34; z-index: 3; border-color: rgba(255,255,255,.30); }
  50%,   73.1%  { transform: translateY(-32px) scale(.89);  opacity: .16; z-index: 2; border-color: rgba(255,255,255,.18); }
  75%,   95%    { transform: translateY(-48px) scale(.835); opacity: .06; z-index: 1; border-color: rgba(255,255,255,.10); }
  96.9%, 100%   { transform: translateY(-48px) scale(.835); opacity: 0;   z-index: 1; border-color: rgba(255,255,255,0); }
}

@keyframes sm-face {
  0%            { opacity: 0; }
  1.9%,  23.1%  { opacity: 1; }
  24.6%, 100%   { opacity: 0; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  the six source cards' colour
   The same fill the six carry, copied off .ics-source rather than
   matched by eye, so the two cannot drift apart.

   It also suits the artwork better than the glass did: the exports are
   inked in #1B2A9E, near-black and a green, all of which need a light
   ground. The glass was letting the wire diagram through underneath
   them.

   The backdrop blur comes off with it. That fill ends in an opaque
   linear gradient, so there is nothing behind the card left to blur
   and the filter would only cost frames.
   ═══════════════════════════════════════════════════════════════════ */
.sm-tile {
  background: radial-gradient(90% 95% at 88% 6%, #CDD3F2 0%, transparent 58%), radial-gradient(70% 80% at 12% 4%, #FBEFF3 0%, transparent 55%), radial-gradient(105% 110% at 6% 96%, #D5DAF4 0%, transparent 62%), radial-gradient(85% 90% at 100% 78%, #E9E4F5 0%, transparent 50%), linear-gradient(118deg, #EDEFFB 0%, #F5F5FD 42%, #FFFAFA 70%, #E8EBF9 100%);
  border: 1px solid rgba(27, 31, 122, .12);
  box-shadow: 0 8px 20px -14px rgba(27, 31, 122, .35);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  no gap, slide up, 4s a card
   THE BLANK. The front position was occupied for 21.2% of the cycle
   while the cards are spaced 25% apart, so for the remaining 3.8%,
   roughly 600ms, nothing was at the front at all. That gap is what you
   were seeing, not a slow transition.

   The front window is now the full quarter. One card leaves as the
   next arrives, so the hand-off has no space in it.

   THE ENTRANCE. A card returns to the front by sliding up from 22px
   below while fading in, over the last 4% of its cycle, which lands it
   in place exactly as the previous one steps back. The jump from the
   back of the stack to below the front happens at 96%, at zero
   opacity, so it is never seen.

   16s over four cards is 4s each: 3.7s at the front, 300ms arriving.
   ═══════════════════════════════════════════════════════════════════ */
@keyframes sm-carousel {
  0%,   23%  { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4; border-color: rgba(255,255,255,.70); }
  25%,  48%  { transform: translateY(-16px) scale(.945); opacity: .34; z-index: 3; border-color: rgba(255,255,255,.30); }
  50%,  73%  { transform: translateY(-32px) scale(.89);  opacity: .16; z-index: 2; border-color: rgba(255,255,255,.18); }
  75%,  92%  { transform: translateY(-48px) scale(.835); opacity: .06; z-index: 1; border-color: rgba(255,255,255,.10); }
  94%        { transform: translateY(-48px) scale(.835); opacity: 0;   z-index: 1; border-color: rgba(255,255,255,0); }
  96%        { transform: translateY(22px)  scale(1);    opacity: 0;   z-index: 4; border-color: rgba(255,255,255,0); }
  100%       { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4; border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,   23%  { opacity: 1; }
  25%,  96%  { opacity: 0; }
  100%       { opacity: 1; }
}

.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 16s;
  animation-timing-function: cubic-bezier(.22, .9, .25, 1);
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  3s a card, eased in and out
   12s over four cards. The front hold is 2.76s and each move is under
   300ms, so the change is quick without being a cut.

   Easing is set per keyframe rather than once on the animation, which
   is the part that makes it read as smooth rather than mechanical:

     · leaving the front  ease-in   — it accelerates away
     · stepping back      ease-out  — each depth settles
     · arriving           a soft overshoot-free curve, so the card
                                      slides up and stops rather than
                                      easing to a vague halt

   One timing function across the whole animation would apply the same
   shape to a card leaving and a card arriving, and those two want
   opposite curves.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 12s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

@keyframes sm-carousel {
  0%,   23%  { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
               border-color: rgba(255,255,255,.70); animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  25%,  48%  { transform: translateY(-16px) scale(.945); opacity: .34; z-index: 3;
               border-color: rgba(255,255,255,.30); animation-timing-function: cubic-bezier(0, .55, .45, 1); }
  50%,  73%  { transform: translateY(-32px) scale(.89);  opacity: .16; z-index: 2;
               border-color: rgba(255,255,255,.18); animation-timing-function: cubic-bezier(0, .55, .45, 1); }
  75%,  93%  { transform: translateY(-48px) scale(.835); opacity: .06; z-index: 1;
               border-color: rgba(255,255,255,.10); animation-timing-function: linear; }
  95%        { transform: translateY(-48px) scale(.835); opacity: 0;   z-index: 1;
               border-color: rgba(255,255,255,0); animation-timing-function: step-end; }
  97%        { transform: translateY(22px)  scale(1);    opacity: 0;   z-index: 4;
               border-color: rgba(255,255,255,0); animation-timing-function: cubic-bezier(.16, .84, .32, 1); }
  100%       { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
               border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,   23%  { opacity: 1; animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  25%,  97%  { opacity: 0; animation-timing-function: cubic-bezier(.16, .84, .32, 1); }
  100%       { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  2s hold, 90% overlap, a slow fade behind
   Three changes.

   HOLD. 8s over four cards. The front card sits for 1.76s and takes
   240ms to step away, which reads as the two seconds you asked for.

   OVERLAP. The step is 21px on a 210px card, so an arriving card
   covers 90% of the one it lands on and the card behind shows as a
   10% band. It was 16px, about 7.6%, which read as a shadow rather
   than as a card waiting its turn.

   THE SLOW HIDE. The depths no longer hold at fixed values. Once a
   card leaves the front it drifts back and fades continuously over
   5.4s, so it recedes rather than stepping through three positions.
   That is the difference between a stack that ticks and one that
   breathes: the plateaus were what made the old one feel mechanical.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 8s;
  animation-timing-function: linear;
}

@keyframes sm-carousel {
  /* front, held */
  0%,  22%  { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
              border-color: rgba(255,255,255,.70);
              animation-timing-function: cubic-bezier(.55, 0, 1, .45); }

  /* one card back, then a continuous recede */
  25%       { transform: translateY(-21px) scale(.955); opacity: .58; z-index: 3;
              border-color: rgba(255,255,255,.34);
              animation-timing-function: cubic-bezier(.33, 0, .5, 1); }
  50%       { transform: translateY(-42px) scale(.912); opacity: .30; z-index: 2;
              border-color: rgba(255,255,255,.20); }
  75%       { transform: translateY(-63px) scale(.870); opacity: .11; z-index: 1;
              border-color: rgba(255,255,255,.10); }
  92%       { transform: translateY(-78px) scale(.840); opacity: 0;   z-index: 1;
              border-color: rgba(255,255,255,0);
              animation-timing-function: step-end; }

  /* reset below the front while invisible, then rise into place */
  95%       { transform: translateY(22px)  scale(1);    opacity: 0;   z-index: 4;
              border-color: rgba(255,255,255,0);
              animation-timing-function: cubic-bezier(.16, .84, .32, 1); }
  100%      { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
              border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,  22%  { opacity: 1; animation-timing-function: cubic-bezier(.55, 0, 1, .45); }
  30%, 95%  { opacity: 0; animation-timing-function: cubic-bezier(.16, .84, .32, 1); }
  100%      { opacity: 1; }
}


/* — ICS stack: both moves at 150ms —————————————————————————
     The step away and the rise into place are 1.9% of the 8s cycle,
     which is 152ms. Everything between them is unchanged: the card
     still recedes and fades over 5.4s once it has left.

     Two things worth knowing at this speed. The card holds the front
     for 1.85s rather than 1.76, since the time comes out of the move.
     And the arrival now finishes in about nine frames at 60fps, which
     is close to the floor — much faster and it reads as a cut rather
     than a movement, however the curve is shaped.
     ————————————————————————————————————————————————— */
@keyframes sm-carousel {
  0%,   23.1%  { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
                 border-color: rgba(255,255,255,.70);
                 animation-timing-function: cubic-bezier(.5, 0, .9, .4); }

  25%          { transform: translateY(-21px) scale(.955); opacity: .58; z-index: 3;
                 border-color: rgba(255,255,255,.34);
                 animation-timing-function: cubic-bezier(.33, 0, .5, 1); }
  50%          { transform: translateY(-42px) scale(.912); opacity: .30; z-index: 2;
                 border-color: rgba(255,255,255,.20); }
  75%          { transform: translateY(-63px) scale(.870); opacity: .11; z-index: 1;
                 border-color: rgba(255,255,255,.10); }
  92%          { transform: translateY(-78px) scale(.840); opacity: 0;   z-index: 1;
                 border-color: rgba(255,255,255,0); }

  96%          { transform: translateY(-78px) scale(.840); opacity: 0;   z-index: 1;
                 border-color: rgba(255,255,255,0);
                 animation-timing-function: step-end; }
  98.1%        { transform: translateY(22px)  scale(1);    opacity: 0;   z-index: 4;
                 border-color: rgba(255,255,255,0);
                 animation-timing-function: cubic-bezier(.1, .9, .3, 1); }
  100%         { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
                 border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,   23.1%  { opacity: 1; animation-timing-function: cubic-bezier(.5, 0, .9, .4); }
  25%,  98.1%  { opacity: 0; animation-timing-function: cubic-bezier(.1, .9, .3, 1); }
  100%         { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  the timings, exactly
     hold at front    300ms
     step away        152ms
     recede and fade  360ms
     rise into place  152ms

   A card therefore leaves the front every 452ms, and with four cards
   sharing one front position the cycle is 4 x 452 = 1808ms. That is
   what sets the cycle length: it is not a free choice once the hold
   and the step are fixed, or two cards would be at the front at once.

   The 360ms recede still passes through all three depths, just
   quickly. Between finishing that and rising again each card waits
   out the rest of the cycle at zero opacity, which is where the reset
   below the front happens.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 1808ms;
}

@keyframes sm-carousel {
  0%,   16.59% { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
                 border-color: rgba(255,255,255,.70);
                 animation-timing-function: cubic-bezier(.5, 0, .9, .4); }

  25%          { transform: translateY(-21px) scale(.955); opacity: .58; z-index: 3;
                 border-color: rgba(255,255,255,.34);
                 animation-timing-function: cubic-bezier(.33, 0, .5, 1); }
  31.6%        { transform: translateY(-42px) scale(.912); opacity: .30; z-index: 2;
                 border-color: rgba(255,255,255,.20); }
  38.3%        { transform: translateY(-63px) scale(.870); opacity: .11; z-index: 1;
                 border-color: rgba(255,255,255,.10); }
  44.91%       { transform: translateY(-78px) scale(.840); opacity: 0;   z-index: 1;
                 border-color: rgba(255,255,255,0);
                 animation-timing-function: step-end; }

  91.59%       { transform: translateY(22px)  scale(1);    opacity: 0;   z-index: 4;
                 border-color: rgba(255,255,255,0);
                 animation-timing-function: cubic-bezier(.1, .9, .3, 1); }
  100%         { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
                 border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,   16.59% { opacity: 1; animation-timing-function: cubic-bezier(.5, 0, .9, .4); }
  25%,  91.59% { opacity: 0; animation-timing-function: cubic-bezier(.1, .9, .3, 1); }
  100%         { opacity: 1; }
}

/* The delay must be a quarter of the cycle, not a fixed 4s. It was
   left over from the 16s version, so with a 1808ms cycle the four
   cards would have bunched instead of spacing evenly. */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-delay: calc(var(--i) * -452ms);
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  new timings, cards 30% bigger
     hold at front    400ms
     step away        200ms
     recede and fade  360ms
     rise into place  300ms

   A card leaves every 600ms, so the cycle is 4 x 600 = 2400ms and the
   delay is a quarter of that. The cycle is not a free choice: it
   follows from the hold and the step, or two cards share the front.

   Everything geometric scales by the same 1.3 — the panel, the card,
   the padding and the four depth offsets. Scaling the card alone would
   have left the stack peeking by 21px on a 273px card, which is 7.7%
   rather than the 10% overlap the stack is built around.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 2400ms;
  animation-delay: calc(var(--i) * -600ms);
}

.ics-orb-wrap .sm-metrics { width: 408px; }
.ics-orb-wrap .sm-card    { height: 273px; }
.sm-stack                 { height: 273px; }
.sm-stack .sm-tile        { height: 273px; padding: 23px 26px; }

@keyframes sm-carousel {
  0%,   16.667% { transform: translateY(0)      scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70);
                  animation-timing-function: cubic-bezier(.5, 0, .9, .4); }

  25%           { transform: translateY(-27px)  scale(.955); opacity: .58; z-index: 3;
                  border-color: rgba(255,255,255,.34);
                  animation-timing-function: cubic-bezier(.33, 0, .5, 1); }
  30%           { transform: translateY(-55px)  scale(.912); opacity: .30; z-index: 2;
                  border-color: rgba(255,255,255,.20); }
  35%           { transform: translateY(-82px)  scale(.870); opacity: .11; z-index: 1;
                  border-color: rgba(255,255,255,.10); }
  40%           { transform: translateY(-101px) scale(.840); opacity: 0;   z-index: 1;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: step-end; }

  87.5%         { transform: translateY(29px)   scale(1);    opacity: 0;   z-index: 4;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: cubic-bezier(.1, .9, .3, 1); }
  100%          { transform: translateY(0)      scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,   16.667% { opacity: 1; animation-timing-function: cubic-bezier(.5, 0, .9, .4); }
  25%,  87.5%   { opacity: 0; animation-timing-function: cubic-bezier(.1, .9, .3, 1); }
  100%          { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  smooth, and taller for the new exports
     hold at front    600ms
     step away        300ms
     recede and fade  360ms
     rise into place  600ms

   900ms between arrivals, so the cycle is 3600ms.

   THE CARD GREW. The new exports are 401px tall against 345 to 366
   before, which renders 242px at this width — 15px more than the old
   card interior could hold, so the artwork would have been clipped.
   The card is 290px now and the depth offsets go with it, staying at
   10% of the card so the overlap is unchanged.

   All four exports are the same height this time, which they were not
   before. The stack no longer has to be sized for its tallest member.

   SMOOTHER CURVES. With 600ms to arrive there is room for a gentle
   ease rather than the sharp one those 150ms moves needed; a snappy
   curve over 600ms reads as a lurch.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 3600ms;
  animation-delay: calc(var(--i) * -900ms);
}

.ics-orb-wrap .sm-card { height: 290px; }
.sm-stack              { height: 290px; }
.sm-stack .sm-tile     { height: 290px; }

@keyframes sm-carousel {
  0%,   16.667% { transform: translateY(0)      scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }

  25%           { transform: translateY(-29px)  scale(.955); opacity: .58; z-index: 3;
                  border-color: rgba(255,255,255,.34);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  28.3%         { transform: translateY(-58px)  scale(.912); opacity: .30; z-index: 2;
                  border-color: rgba(255,255,255,.20); }
  31.7%         { transform: translateY(-87px)  scale(.870); opacity: .11; z-index: 1;
                  border-color: rgba(255,255,255,.10); }
  35%           { transform: translateY(-107px) scale(.840); opacity: 0;   z-index: 1;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: step-end; }

  83.333%       { transform: translateY(31px)   scale(1);    opacity: 0;   z-index: 4;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: cubic-bezier(.25, .8, .35, 1); }
  100%          { transform: translateY(0)      scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70); }
}

@keyframes sm-face {
  0%,   16.667% { opacity: 1; animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  25%,  83.333% { opacity: 0; animation-timing-function: cubic-bezier(.25, .8, .35, 1); }
  100%          { opacity: 1; }
}


/* — ICS cards: 15px top and bottom ————————————————————————
     The card shrinks to the artwork plus that padding, 272px rather
     than 290. Horizontal padding stays at 26 so the artwork keeps its
     width and does not need re-measuring.

     Depth offsets follow at 10% of the new height. Leaving them at the
     old 29px would have made the stack peek by 10.7% on a shorter
     card, which is the small drift that has crept in every time the
     card has been resized.
     ————————————————————————————————————————————————— */
.ics-orb-wrap .sm-card { height: 272px; }
.sm-stack              { height: 272px; }
.sm-stack .sm-tile     { height: 272px; padding: 15px 26px; }

@keyframes sm-carousel {
  0%,   16.667% { transform: translateY(0)      scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }

  25%           { transform: translateY(-27px)  scale(.955); opacity: .58; z-index: 3;
                  border-color: rgba(255,255,255,.34);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  28.3%         { transform: translateY(-54px)  scale(.912); opacity: .30; z-index: 2;
                  border-color: rgba(255,255,255,.20); }
  31.7%         { transform: translateY(-82px)  scale(.870); opacity: .11; z-index: 1;
                  border-color: rgba(255,255,255,.10); }
  35%           { transform: translateY(-101px) scale(.840); opacity: 0;   z-index: 1;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: step-end; }

  83.333%       { transform: translateY(29px)   scale(1);    opacity: 0;   z-index: 4;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: cubic-bezier(.25, .8, .35, 1); }
  100%          { transform: translateY(0)      scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70); }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS STACK  ·  every card holds at every depth
   Only one card looked like it was transitioning because only one was
   on screen. A card reached the back and faded to nothing after 360ms,
   then waited out the remaining two thirds of the cycle invisible — so
   for most of the time there was a single card and an empty space
   behind it.

   The four positions now share the cycle evenly: 900ms each, made of a
   600ms hold and a 300ms move, which is the hold and step you asked
   for applied at every depth rather than only at the front. Three
   cards are therefore always visible behind the front one, and each
   one visibly steps back.

   The fade to nothing and the return happen inside the last position,
   where the card is already at the back and nearly transparent.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card { height: 262px; }
.sm-stack              { height: 262px; }
.sm-stack .sm-tile     { height: 262px; padding: 10px 26px; }

@keyframes sm-carousel {
  /* front */
  0%,   16.667% { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  /* first behind */
  25%,  41.667% { transform: translateY(-26px) scale(.958); opacity: .60; z-index: 3;
                  border-color: rgba(255,255,255,.38);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  /* second */
  50%,  66.667% { transform: translateY(-52px) scale(.916); opacity: .34; z-index: 2;
                  border-color: rgba(255,255,255,.24);
                  animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  /* third, then out */
  75%           { transform: translateY(-79px) scale(.874); opacity: .16; z-index: 1;
                  border-color: rgba(255,255,255,.14);
                  animation-timing-function: cubic-bezier(.4, 0, .8, .5); }
  83.333%       { transform: translateY(-96px) scale(.845); opacity: 0;   z-index: 1;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: step-end; }
  /* reset below the front while invisible, then rise */
  83.334%       { transform: translateY(28px)  scale(1);    opacity: 0;   z-index: 4;
                  border-color: rgba(255,255,255,0);
                  animation-timing-function: cubic-bezier(.25, .8, .35, 1); }
  100%          { transform: translateY(0)     scale(1);    opacity: 1;   z-index: 4;
                  border-color: rgba(255,255,255,.70); }
}

/* artwork on the front card only, so the three behind stay as edges
   and nothing reads through the glass */
@keyframes sm-face {
  0%,   16.667% { opacity: 1; animation-timing-function: cubic-bezier(.37, 0, .63, 1); }
  25%,  83.334% { opacity: 0; animation-timing-function: cubic-bezier(.25, .8, .35, 1); }
  100%          { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════
   ═══  MOBILE  ═══════════════════════════════════════════════════
   The page was built at desktop width and several pieces are pinned
   in absolute pixels, so they cannot simply reflow. What follows
   handles each of those by hand.

   Guard first: nothing on a landing page justifies a horizontal
   scrollbar, and a single overflowing element causes one for the whole
   document. This catches anything the rules below miss.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  /* clip, not hidden. `overflow: hidden` makes main a scroll
     container, so focusing anything inside it lets the browser scroll
     that container to reveal the focused element — which is what threw
     the page back to the top when a suite button was tapped. `clip`
     crops the overflow without creating a scrollport, so nothing can
     be scrolled. */
  main { overflow-x: clip; }

  /* The hero's second line carries white-space: nowrap so it holds one
     line on desktop. On a phone that one declaration is enough to push
     the whole document sideways. */
  .headline .nowrap { white-space: normal; }

  .headline { font-size: clamp(30px, 8.5vw, 46px); }
  .lede     { font-size: 15px; }
  .cta-row  { flex-direction: column; align-items: stretch; gap: 10px; }
  .btn-lg   { min-width: 0; width: 100%; }

  /* — The problem orbit ————————————————————————————————
     It sizes itself from its container, so it only needs its bleed
     taking off; the 120% width was there to overhang the gutters on a
     wide screen. */
  #problem .po-fit {
    width: 100%;
    margin-left: 0;
    margin-top: 0;
  }

  /* — Suites and footer ————————————————————————————— */
  .suite-grid  { grid-template-columns: 1fr; gap: 24px; }
  .suite-copy  { margin-left: 0; }
  .footer-grid { grid-template-columns: 1fr 1fr; gap: 28px; }

  /* — The logo rail keeps marching. Stopping it and leaving a
       hand-scrollable strip meant nothing appeared to happen, and a
       row of logos that does not move reads as broken rather than as
       static. ————————————————————————————————————————— */
  .logo-marquee { overflow: hidden; }

  /* — Section rhythm. 210px of air above and below About is a third
       of a phone screen. ——————————————————————————————— */
  #about { padding-top: 90px; padding-bottom: 90px; }
  .section { padding: 56px 0 0; }
}

@media (max-width: 620px) {
  .footer-grid { grid-template-columns: 1fr; }
  .sm-stack .sm-tile { padding: 10px 14px; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE  ·  the two animations that cannot follow
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  /* THE TRAVELLING CUBE comes off. It is a fixed 200px steered to 6%
     and 92% of the viewport, which on a 375px screen puts it straight
     over the reading column. The path could be re-pointed, but a cube
     crossing the text on a phone is a cost with no matching benefit —
     the journey reads as a journey because there is margin to travel
     through, and on mobile there is none. */
  #field, #lines, #cube3d { display: none; }

}


/* — The baseline every responsive page needs ————————————————
     An image with width and height attributes renders at those pixel
     dimensions unless told otherwise, so a 1419px export on a 375px
     screen pushes the document sideways on its own. Several images
     here are capped individually; this covers the rest, and anything
     added later.

     height: auto with it, or a capped width would squash the image
     against its unchanged height attribute. ————————————— */
img, video { max-width: 100%; height: auto; }


/* ═══════════════════════════════════════════════════════════════════
   ═══  MOBILE, REVISED  ═══════════════════════════════════════════
   Three changes, none of which apply above 900px.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  /* 1 — THE TRAVELLING CUBE IS BACK ON.
     It was hidden because a fixed 200px cube crossed the reading
     column. Its size now follows the viewport instead — 12.5% of the
     width, so about 100px on a phone against 200 on a desktop — and it
     keeps to the margins the way it does on a wide screen. */
  #field, #lines, #cube3d { display: block; }

  /* 2 — SUITE: SMALLER MENU, BIGGER SCREEN.
     The menu rows carry desktop padding and type, which on a phone
     takes more height than the dashboard they introduce. Trimmed, and
     the screen bleeds to the full viewport width so the artwork is not
     paying for the container's gutters. */
  .suite-item      { padding: 11px 14px; border-radius: 12px; }
  .suite-item h3   { font-size: 14.5px; }
  .suite-item p    { font-size: 12.5px; line-height: 1.45; }
  .suite-list      { gap: 8px; }

  .suite-views {
    width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
  }
  .suite-screen { border-radius: 0; }
  .suite-copy   { padding: 0 clamp(20px, 4vw, 64px); }

  /* 3 — ICS KEEPS ITS DESKTOP LAYOUT.
     The stacking from the last pass is undone. The diagram holds its
     design width and is scaled to fit by the script above, so every
     wire, dot and card stays exactly where it is on a wide screen,
     only smaller. Reflowing it would have meant redrawing the wires
     against positions that no longer describe the same flow. */
}



/* ═══════════════════════════════════════════════════════════════════
   MOBILE  ·  suite screens back inside the frame
   The full-bleed I used was `width: 100vw; margin-left: calc(50% -
   50vw)`. That trick only lands correctly when the element's parent
   spans the viewport. Here the parent is a padded container inside a
   grid, so the 50% resolved against the wrong box and the screens
   pushed out past the frame on one side.

   The screen gets its extra width by narrowing the container instead,
   which cannot overflow because nothing leaves the flow. The copy
   above it then needs no compensating padding, so it lines up with the
   dashboard beneath it again.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #revenue .container,
  #marketing .container { padding-left: 16px; padding-right: 16px; }

  .suite-views {
    width: 100%;
    margin-left: 0;
    margin-right: 0;
  }
  .suite-screen { border-radius: 12px; }
  .suite-copy   { padding: 0; margin-left: 0; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE  ·  the ICS diagram, scaled and nothing else
   Every other override on this section is gone. Earlier passes reset
   left, top, transform and margin on its children to stack them, and a
   later pass put `position: absolute` back without restoring those, so
   all three children resolved to the top-left corner. That is what put
   the whole diagram in a thumbnail in the corner of an empty box.

   The rule now is only: hold the design width, scale to fit. The
   layout, the wires and the alignment are the desktop ones untouched.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .ics-fit {
    position: relative;
    width: 100%;
    overflow: hidden;
  }
  .ics-stage {
    width: 1120px;
    transform-origin: top left;
    transform: scale(var(--ics-k, 1));
  }
}


/* The diagram is scaled as one piece now, so the old responsive rules
   for its parts would fight that: reflowing the source cards inside a
   stage that is already being shrunk changes the alignment the scale
   exists to preserve. */
@media (max-width: 1100px) { .ics-grid, .ics-sources { grid-template-columns: unset; } }
@media (max-width: 620px)  { .ics-sources { grid-template-columns: unset; } }


/* ═══════════════════════════════════════════════════════════════════
   MOBILE  ·  section rhythm
   The page is spaced for a wide screen, where 120px between sections
   is a comfortable pause. On a 390px phone that same gap is a third of
   the viewport, so you scroll through empty space between every
   section. These are roughly halved.

   Everything here is inside the media query. Desktop spacing is
   untouched.

   The one exception is the gap between the Revenue and Marketing
   suites, which keeps its full 80px. They are the same component shown
   twice, so the space between them is what separates one suite from
   the next rather than one topic from another; closing it would run
   the two together.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .section       { padding-top: 36px; }

  .integrations  { padding-top: 32px; }
  #problem       { padding-bottom: 36px; }

  #how           { padding-top: 48px; padding-bottom: 48px; }
  #ics           { padding-top: 48px; padding-bottom: 48px; }

  /* left alone on purpose */
  #marketing     { padding-top: 80px; }

  #about         { padding-top: 56px;  padding-bottom: 56px; }
  #faqs          { padding-top: 36px;  padding-bottom: 40px; }
  #demo          { padding-top: 36px;  padding-bottom: 40px; }

  .section-head  { margin-bottom: 28px; }
  .faq-head      { margin-bottom: 24px; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE MENU
   The button and the drawer do not exist above 900px. They are
   display:none there rather than merely off-screen, so the desktop bar
   is exactly what it was.
   ═══════════════════════════════════════════════════════════════════ */
.nav-burger, .nav-drawer, .nav-scrim { display: none; }

/* The middle column is capped by the wordmark and the two actions, and
   below 1366 that cap is about 800px. Eight links at a readable size
   with room to breathe need 767, so this is as far down as the row
   goes. The drawer takes over below it and carries the same eight. */
@media (max-width: 1365px) {

  nav.nav .nav-links { display: none; }

  /* — the button ————————————————————————————————————— */
  .nav-burger {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 5px;
    width: 40px; height: 40px;
    padding: 0 9px;
    border: 0; border-radius: 10px;
    background: transparent;
    cursor: pointer;
    flex: none;
  }
  .nav-burger span {
    display: block; height: 2px; border-radius: 2px;
    background: #1B1F7A;
    transition: transform .24s var(--ease-out), opacity .18s var(--ease-out);
  }
  /* the three bars become a cross while it is open */
  body.nav-open .nav-burger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
  body.nav-open .nav-burger span:nth-child(2) { opacity: 0; }
  body.nav-open .nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

  /* the burger sits first, so the wordmark moves off the left edge */
  .nav { gap: 10px; }
  .brand { margin-right: auto; }

  /* — the sheet behind it ————————————————————————————— */
  .nav-scrim {
    display: block;
    position: fixed; inset: 0; z-index: 60;
    background: rgba(10, 15, 46, .38);
    opacity: 0;
    transition: opacity .24s var(--ease-out);
  }
  .nav-scrim.is-open { opacity: 1; }

  /* — the drawer, in from the left ————————————————————— */
  .nav-drawer {
    display: flex;
    flex-direction: column;
    gap: 6px;
    position: fixed;
    top: 0; left: 0; bottom: 0; z-index: 61;
    width: min(78vw, 320px);
    padding: 84px 22px 28px;
    background: #FFFAFA;
    box-shadow: 0 0 60px -16px rgba(10, 15, 46, .45);
    transform: translateX(-100%);
    transition: transform .26s cubic-bezier(.22, .8, .28, 1);
    overflow-y: auto;
  }
  .nav-drawer.is-open { transform: translateX(0); }

  .nav-drawer ul { list-style: none; margin: 0 0 20px; padding: 0; }
  .nav-drawer li + li { margin-top: 2px; }
  .nav-drawer a:not(.btn) {
    display: block;
    padding: 13px 12px;
    border-radius: 10px;
    font-size: 16px; font-weight: 500;
    color: #1B1F7A;
    text-decoration: none;
  }
  .nav-drawer a:not(.btn):hover { background: rgba(27, 31, 122, .06); }
  .nav-drawer .btn { width: 100%; }

  .nav-close {
    position: absolute; top: 18px; right: 16px;
    width: 38px; height: 38px;
    border: 0; border-radius: 10px;
    background: transparent;
    font-size: 26px; line-height: 1; color: #1B1F7A;
    cursor: pointer;
  }

  /* the page must not scroll behind an open drawer */
  body.nav-open { overflow: hidden; }
}

@media (prefers-reduced-motion: reduce) {
  .nav-drawer, .nav-scrim, .nav-burger span { transition: none; }
}


/* — The drawer sits above everything, including the sticky bar ————
     It is a child of body now rather than of main. main carries
     z-index 1, which boxes everything inside it into that stacking
     context however high its own z-index climbs — so a drawer at 61
     inside main still cannot rise above a sibling of main. ——— */
@media (max-width: 900px) {
  .nav-scrim  { z-index: 998; }
  .nav-drawer { z-index: 999; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE  ·  suite menu as a row
   The five rows stack on a phone and take more height than the
   dashboard they introduce. They become a horizontal rail of pills
   that scrolls sideways inside its own track.

   The descriptions come off with the stack. At pill width they would
   wrap to three lines each and the rail would be taller than the list
   it replaced; the h4 already names the screen, and the two lines
   above the dashboard describe it.

   scroll-snap so a swipe lands on a pill rather than between two.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .suite-grid { gap: 14px; }

  .suite-list {
    display: flex;
    flex-direction: row;
    gap: 8px;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
    scrollbar-width: none;
  }
  .suite-list::-webkit-scrollbar { display: none; }

  .suite-item {
    flex: 0 0 auto;
    scroll-snap-align: start;
    padding: 9px 14px;
    border-radius: 999px;
    opacity: 1;
  }
  .suite-item p { display: none; }
  .suite-item h3 {
    font-size: 13.5px;
    white-space: nowrap;
    margin: 0;
  }
}


/* — Mobile: all five suite pills reachable ————————————————
     The rail scrolls, but with no visual cue that it does. A fade at
     the right edge shows there is more, and the first pill is inset so
     the row does not start flush against the screen. —————— */
@media (max-width: 900px) {
  .suite-list {
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 26px), transparent 100%);
            mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 26px), transparent 100%);
    padding-right: 26px;
  }
  .logo-track { animation: logo-marquee 26s linear infinite; }
}


/* ═══════════════════════════════════════════════════════════════════
   SUITE PILLS  ·  mobile, made visible
   Two faults, both inherited rather than introduced by the rail.

   The inactive pills had no background of their own. On the white
   panel behind them that is nothing at all — no fill, no border, so
   they read as plain text and gave no sign of being tappable.

   The active pill carried `color: transparent`, left over from a
   gradient-text treatment whose background-clip no longer applies. On
   a solid #1B2A9E fill that makes the label invisible, so the selected
   item looked emptier than the rest rather than picked out.

   Both are set explicitly here, with the section id in the selector so
   nothing earlier can win on specificity.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #revenue .suite-item,
  #marketing .suite-item {
    background: rgba(27, 31, 122, .05);
    border: 1px solid rgba(27, 31, 122, .12);
    -webkit-background-clip: border-box;
            background-clip: border-box;
    opacity: 1;
  }
  #revenue .suite-item h3,
  #marketing .suite-item h3 {
    color: #1B1F7A;
    -webkit-text-fill-color: #1B1F7A;
  }

  #revenue .suite-item.is-active,
  #marketing .suite-item.is-active {
    background: #1B2A9E;
    border-color: #1B2A9E;
    box-shadow: 0 6px 16px -8px rgba(27, 42, 158, .6);
  }
  #revenue .suite-item.is-active h3,
  #marketing .suite-item.is-active h3 {
    color: #FFFFFF;
    -webkit-text-fill-color: #FFFFFF;
  }
}


/* — Mobile: all five pills on screen ————————————————————————
     The rail scrolled, so two of the five were always off the edge.
     They wrap onto two rows instead: five short labels fit a phone
     comfortably, and a menu you can see all of beats one you have to
     discover by swiping. ——————————————————————————————— */
@media (max-width: 900px) {
  .suite-list {
    flex-wrap: wrap;
    overflow-x: visible;
    -webkit-mask-image: none;
            mask-image: none;
    padding-right: 0;
    gap: 7px;
  }
  .suite-item   { flex: 0 0 auto; }
  .suite-item h3 { font-size: 12.5px; }
}


/* ═══════════════════════════════════════════════════════════════════
   THE INVISIBLE SHEET OVER THE WHOLE PAGE
   `.nav-scrim { display: block }` inside the mobile query beat the
   `hidden` attribute on the element. Both selectors weigh 0,1,0, and
   an author rule beats the browser's own [hidden] rule, so the closed
   scrim was still laid out: a fixed overlay across the viewport at
   z-index 998, invisible at opacity 0, catching every tap on the page.

   That is why nothing on mobile responded — the suite pills, and the
   hamburger itself. It was never the scripts.

   Two guards, because one is not enough on its own:

     · [hidden] wins, with !important, so the element genuinely leaves
       the layout when closed.
     · pointer-events: none until it is open, so even a mid-transition
       frame cannot intercept a tap.
   ═══════════════════════════════════════════════════════════════════ */
.nav-scrim[hidden],
.nav-drawer[hidden] { display: none !important; }

.nav-scrim  { pointer-events: none; }
.nav-scrim.is-open { pointer-events: auto; }


/* ═══════════════════════════════════════════════════════════════════
   MOBILE  ·  the ICS diagram, rebuilt as a column
   Replaces the scale-to-fit from the earlier pass. Scaling kept the
   desktop alignment but at 35% the labels were unreadable; this trades
   that alignment for legibility, which on a phone is the better deal.

   Reading order, top to bottom:
       the six sources, in a grid
       a rule beneath them
       the cube
       the metric cards

   The wires come off. They describe a left-to-right convergence that
   the column no longer has, and the layout script measures positions
   that would now mean nothing. The rule under the sources does the
   same job the wires did: everything above it feeds what is below.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  .ics-fit   { overflow: visible; height: auto !important; }
  .ics-stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 22px;
    width: 100%;
    min-height: 0;
    transform: none;
  }

  .ics-wires { display: none; }
  .ics-dot   { display: none; }        /* they anchored the wires */

  /* 1 — the six, across */
  .ics-sources {
    position: static;
    transform: none;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
    width: 100%;
    order: 1;
  }
  .ics-source { padding: 10px 8px; font-size: 12.5px; border-radius: 9px; text-align: center; }

  /* 2 — the rule, standing in for the wires */
  .ics-stage::after {
    content: '';
    order: 2;
    width: 100%;
    height: 1px;
    background: linear-gradient(90deg,
      rgba(27, 31, 122, 0) 0%,
      rgba(27, 31, 122, .22) 18%,
      rgba(27, 31, 122, .22) 82%,
      rgba(27, 31, 122, 0) 100%);
  }

  /* 3 — the cube */
  .ics-cube-wrap {
    position: static;
    transform: none;
    margin: 0;
    order: 3;
    display: block;
    text-align: center;
  }
  .ics-cube.c3-rig.is-static { margin: 0 auto; }
  .ics-cube-label {
    position: static;
    transform: none;
    display: block;
    margin-top: 14px;
    text-align: center;
  }

  /* 4 — the metric cards */
  .ics-orb-wrap {
    position: static;
    transform: none;
    order: 4;
    width: 100%;
    display: flex;
    justify-content: center;
  }
  .ics-orb { display: none; }
  .ics-orb-wrap .sm-metrics {
    position: static;
    transform: none;
    width: 100%;
    max-width: 408px;
  }
}

@media (max-width: 560px) {
  .ics-sources { grid-template-columns: repeat(2, 1fr); }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE ICS  ·  one row, a visible line, centred
   Three corrections.

   THE LINE was there but invisible: 1px of a gradient topping out at
   22% opacity. It is 2px and twice the strength now, and carries
   `flex: none` so the column cannot shrink it away — a 1px flex item
   with default shrink is one rounding error from nothing.

   ONE ROW. The six sit in a single row of equal columns rather than
   wrapping to two or three. At phone width that is about 53px each, so
   the type comes down and the labels wrap inside their own card. They
   stretch to a common height so the row reads as one band.

   CENTRED throughout: the row, the rule, the cube and the cards all on
   the same axis, and the text inside each card centred too.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  .ics-stage { gap: 26px; align-items: center; text-align: center; }

  /* the six, in one row */
  .ics-sources {
    display: flex;
    flex-direction: row;
    align-items: stretch;
    justify-content: center;
    gap: 6px;
    width: 100%;
  }
  .ics-source {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 9px 4px;
    font-size: 9.5px;
    line-height: 1.25;
    letter-spacing: 0;
    text-align: center;
    border-radius: 8px;
    hyphens: auto;
  }

  /* the rule, actually visible */
  .ics-stage::after {
    flex: none;
    height: 2px;
    border-radius: 2px;
    background: linear-gradient(90deg,
      rgba(27, 31, 122, 0)   0%,
      rgba(27, 31, 122, .45) 20%,
      rgba(27, 31, 122, .45) 80%,
      rgba(27, 31, 122, 0)   100%);
  }

  .ics-cube-wrap,
  .ics-orb-wrap { align-self: center; }
  .ics-cube-label { text-align: center; }
}

/* the row stays a row all the way down */
@media (max-width: 560px) {
  .ics-sources { flex-direction: row; }
  .ics-source  { font-size: 8.5px; padding: 8px 3px; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE ICS  ·  the wires, wider gaps, one axis
   The faint rule is replaced by six real lines fanning from the card
   row into the cube, in the same blue and weight as the desktop bus.

   They are a separate SVG. The desktop wires are drawn at runtime from
   the measured positions of the cards, the cube and the orb — in a
   column those positions describe nothing, so a second set is authored
   for the column rather than the first being re-pointed.

   preserveAspectRatio is none so the fan stretches to whatever width
   the row is, keeping each line under its own card at any size.
   ═══════════════════════════════════════════════════════════════════ */
.ics-wires-m { display: none; }

@media (max-width: 900px) {
  .ics-stage::after { display: none; }       /* the rule it replaces */

  .ics-wires-m {
    display: block;
    order: 2;
    width: 100%;
    height: 84px;
    overflow: visible;
  }
  .ics-wires-m path {
    fill: none;
    stroke: #2C34D8;
    stroke-width: 1.4;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }

  /* — more air between the four stages ——————————————————— */
  .ics-stage { gap: 34px; }
  .ics-sources { margin-bottom: 0; }

  /* — the three on one axis ——————————————————————————————
     The cube, the label and the card stack all centre on the same
     line. The label goes back to static flow: it is absolutely
     positioned on desktop against the cube's box, which in a column
     put it over the artwork. */
  .ics-cube-wrap {
    order: 3;
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .ics-cube.c3-rig.is-static { margin: 0 auto; }
  .ics-cube-label {
    position: static !important;
    left: auto; top: auto;
    transform: none !important;
    margin: 16px 0 0;
    text-align: center;
  }

  .ics-orb-wrap { order: 4; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE ICS  ·  desktop wire idiom, centred, 80px to the card
   THE LINES now run the way the desktop bus does: straight out of each
   card, a rounded right-angle, a flat run, another turn, then down.
   The three lanes are staggered 34 / 58 / 82 so the corners nest
   instead of stacking on one column, which is the same reason the
   desktop diagram staggers its turn columns.

   THE CENTRING was off because the cube kept a desktop offset. The
   wrapper carries translateX(80px) to sit right of the wire on a wide
   screen; in a column that pushed it 80px off the axis everything else
   is centred on.

   THE MISSING RUN from the cube to the card is drawn as a rule on the
   card's own wrapper, so it cannot drift from either end, and the
   80px gap IS that run.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  /* the cube back on the axis */
  .ics-cube-wrap {
    margin-left: 0 !important;
    margin-right: 0 !important;
    transform: none !important;
    left: auto !important;
    right: auto !important;
  }

  /* clearance under the rig: the cubes are transformed in 3D and paint
     outside their box, so the label needs more than a flow gap or it
     sits behind the lowest row */
  .ics-cube-label { margin-top: 34px !important; }

  /* the cube to the cards: an 80px run, with the line in it */
  .ics-orb-wrap {
    position: relative;
    padding-top: 80px;
  }
  .ics-orb-wrap::before {
    content: '';
    position: absolute;
    top: 0; left: 50%;
    width: 1.4px; height: 80px;
    margin-left: -.7px;
    background: #2C34D8;
    border-radius: 2px;
  }

  /* the stage gap is now carried by that padding, so it does not
     double up */
  .ics-stage { gap: 30px; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE ICS  ·  one axis, an undistorted fan, both runs drawn
   Mobile only. Nothing here exists above 900px.

   THE FAN NO LONGER STRETCHES. It carried preserveAspectRatio="none"
   against a fixed 84px height, so a 600x130 drawing was squashed to
   0.65 vertically: the twelve-unit corners came out as ellipses and
   the three lanes closed up on each other. It now scales uniformly
   (height:auto, default meet), so a corner is a corner at any width.

   IT STILL LANDS UNDER THE CARDS because the six starts are computed
   from the row's own flex maths rather than eyeballed. With a 1.2%
   gap each card is (100 - 5x1.2)/6 = 15.6667% wide, so centre k sits
   at k x 16.8667 + 7.8333 percent — 47, 148.2, 249.4, 350.6, 451.8
   and 553 in a 600-unit box. Percentage gap is the point: a px gap
   would drift off the line as the screen changes width.

   BOTH RUNS ARE DRAWN. The fan stops at its own bottom edge and the
   flex gap left a 30px break before the cube, so the run into the
   cube is bridged by a rule on the cube's wrapper. The run out of the
   cube into the cards is the same idea on the other wrapper, and the
   80px of padding there IS that run.

   THE CARDS ARE CENTRED with !important because six earlier blocks
   place this panel absolutely against the desktop orb — left:50% and
   a translate that a plain reset kept losing.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  #ics .ics-stage { align-items: center; }

  /* the six, on the fractions the fan is drawn to */
  #ics .ics-sources { gap: 1.2%; }

  /* the fan, at its own aspect */
  .ics-wires-m {
    width: 100%;
    height: auto;
    margin: 0;
    overflow: visible;
  }

  /* the run into the cube: bridges the flex gap and reaches a little
     way into the rig, which carries empty space above the top row */
  .ics-cube-wrap {
    position: relative !important;
    margin: 0 !important;
    left: auto !important; right: auto !important;
    transform: none !important;
    width: 100%;
  }
  .ics-cube-wrap::before {
    content: '';
    position: absolute;
    top: -30px; left: 50%;
    width: 1.4px; height: 42px;
    margin-left: -.7px;
    background: #2C34D8;
    border-radius: 2px;
  }

  /* the run out of the cube, and the cards under it */
  .ics-orb-wrap {
    position: relative !important;
    width: 100%;
    padding-top: 80px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    margin: 0 !important;
    left: auto !important; right: auto !important;
    transform: none !important;
  }
  .ics-orb-wrap::before {
    content: '';
    position: absolute;
    top: 0; left: 50%;
    width: 1.4px; height: 80px;
    margin-left: -.7px;
    background: #2C34D8;
    border-radius: 2px;
  }

  .ics-orb-wrap .sm-metrics {
    position: static !important;
    left: auto !important; top: auto !important;
    transform: none !important;
    width: min(100%, 340px) !important;
    max-width: none !important;
    margin: 0 auto !important;
  }
  .ics-orb-wrap .sm-card { width: 100% !important; margin: 0 auto; }
  .ics-orb-wrap .sm-track { align-items: center; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE ICS  ·  the fan stops crossing itself, 30 to the label
   THE CROSSINGS came from the lane order. The outermost pair turned
   on the highest lane, so its long run passed straight through the
   vertical drop of every card inside it — three crossings a side.

   Reversed: the INNER pair turns first (lane 40), the middle pair
   next (72) and the outer pair last (104). Each lane now ends inside
   the drop of the pair above it, so no run ever meets a stub. The
   only shared ink is the trunk at x=300, where the six are meant to
   merge — same colour, same width, so it reads as one line.

   The paths themselves are in the markup; this note is here because
   the lane order is the part that is easy to undo by accident.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .ics-cube-label { margin-top: 70px !important; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARD  ·  rise, settle, hold, fade back
   One card at a time now, and one continuous move each.

   What it did before was four discrete stops: hold at the front, step
   back, hold, step back, hold, step back, then vanish. Every step was
   a separate move with a hold either side, which is what made it read
   as stuttering rather than travelling.

   The lifecycle is now the one you described, and nothing else:

     0 - 600ms      rises from 34px below, decelerating hard into
                    place. The curve is weighted .12/.82 so most of
                    the distance is covered early and the last few
                    pixels take the longest — that is the "slows down
                    and sticks" part; a symmetric ease arrives at
                    speed and stops dead.
     600 - 1200ms   holds. Nothing moves.
     1200 - 1824ms  eases IN as it goes: accelerating away, 30px back
                    and down to nothing.
     1824 - 4800ms  invisible. It resets below the front on a
                    step-end frame, so the jump can never be seen.

   Four cards at a 1200ms stagger against a 4800ms cycle: the next one
   begins its rise on the exact frame the last one begins to leave, so
   the two cross over rather than the panel going empty between them.

   The artwork no longer fades on its own. It was masked off every
   card but the front one, which mattered when three sat stacked
   behind; with one card carrying the whole cycle it only ever
   subtracted from the fade the card is already doing.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 4800ms;
  animation-delay: calc(var(--i) * -1200ms);
}

@keyframes sm-carousel {
  /* rising — fast away from the start, slow into the stop */
  0%      { transform: translateY(34px) scale(.965); opacity: 0; z-index: 4;
            animation-timing-function: cubic-bezier(.12, .82, .24, 1); }
  /* arrived */
  12.5%   { transform: translateY(0)    scale(1);    opacity: 1; z-index: 4;
            animation-timing-function: linear; }
  /* stuck */
  25%     { transform: translateY(0)    scale(1);    opacity: 1; z-index: 2;
            animation-timing-function: cubic-bezier(.55, 0, .85, .45); }
  /* fading back */
  38%     { transform: translateY(-30px) scale(.94); opacity: 0; z-index: 2;
            animation-timing-function: step-end; }
  /* below again, unseen, and waiting */
  38.01%,
  100%    { transform: translateY(34px) scale(.965); opacity: 0; z-index: 4; }
}

@keyframes sm-face {
  0%, 100% { opacity: 1; }
}


/* ═══════════════════════════════════════════════════════════════════
   MOBILE ICS  ·  4px from the label to the run
   The gap under the ICS label is the stage's own 30px flex gap, and
   the run to the metric cards starts at the top of that next block.
   The gap itself cannot come down to 4 without closing every other
   gap in the column — sources to fan, fan to cube — so it is pulled
   back on this one item instead: 30 - 26 = 4.

   The bridge above the cube is unaffected: it is anchored at -30px
   against the gap it spans, which has not moved.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .ics-orb-wrap { margin-top: -26px !important; }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  the mobile pin
   Mobile only. The desktop strip is one wide export of three cards
   sitting side by side, which at phone width leaves each of them
   about a hundred pixels across. So on mobile the section holds still
   and the cards travel through it one at a time, full size.

   THE THREE FILES are cut from that same export — bodies at x 18, 488
   and 955, each 448 wide, taken with 11px of shadow either side so
   nothing is clipped mid-blur. The wide one is switched off here
   rather than removed: above 900px it is still the section.

   THE HEIGHT is what makes the pin work. 260svh of wrapper against a
   100svh stage leaves 160svh of scroll, and the track is moved across
   its two remaining card-widths over exactly that distance. Longer
   and it feels stuck; shorter and the cards snap past.

   svh, not vh: vh on a phone is the tallest the viewport ever gets,
   so a 100vh stage is taller than what you can actually see while the
   address bar is showing, and the card would sit low behind it.

   `overflow: clip` rather than hidden. hidden would make the stage a
   scroll container that the browser could scroll on its own — the
   same thing that used to throw the page back to the hero when a
   suite button took focus.
   ═══════════════════════════════════════════════════════════════════ */
.how-pin { display: none; }

@media (max-width: 900px) {

  #how .how-strip > img { display: none; }     /* the wide export */

  .how-pin {
    display: block;
    position: relative;
    height: 260vh;
    height: 260svh;
  }

  .how-pin-stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100svh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: clip;
  }

  .how-track {
    display: flex;
    width: 300vw;
    will-change: transform;
  }

  /* 10px either side of the card, and nothing else in the slide */
  .how-slide {
    flex: none;
    width: 100vw;
    box-sizing: border-box;
    padding: 0 10px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .how-slide img {
    display: block;
    width: 100%;
    height: auto;
  }

  /* three cards with no edge showing is three cards you cannot count */
  .how-dots {
    display: flex;
    gap: 8px;
    margin-top: 22px;
  }
  .how-dots i {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: rgba(27, 31, 122, .20);
    transition: background .25s ease, transform .25s ease;
  }
  .how-dots i.is-on {
    background: #2C34D8;
    transform: scale(1.28);
  }
}

/* No pin without motion: the wrapper collapses to its content and the
   three stack down the page, which is the same information. */
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .how-pin       { height: auto; }
  .how-pin-stage { position: static; height: auto; overflow: visible; }
  .how-track     { display: block; width: auto; transform: none !important; }
  .how-slide     { width: auto; padding: 0 10px; margin-bottom: 18px; }
  .how-dots      { display: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  20px from the head to the card
   The gap was not spacing — it was the stage. A 100svh frame with the
   card centred in it put roughly 200px of empty stage above the card,
   and no amount of margin on the section head could reach that.

   So the stage is only as tall as the card now, and sticks under the
   nav rather than filling the screen. The 20px is then a real 20px:
   the section head's own margin, with .how-strip collapsed to nothing
   between them (it holds only the screen-reader list, which is
   absolutely positioned).

   --how-top is written by the pin script from the nav's measured
   height, because the bar is sticky at top:0 with z-index 50 — a
   stage pinned to 0 would slide its card under the blur. Measured
   rather than typed: the bar is 42px of logo plus padding that
   changes at 560px.

   Travel is unaffected: the script measures the stage, so a shorter
   stage simply means more of the wrapper is scroll.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  #how .section-head { margin-bottom: 20px; }
  #how .how-strip    { margin: 0; }

  .how-pin-stage {
    top: var(--how-top, 76px);
    height: auto;
    justify-content: flex-start;
  }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  the whole section pins, not just the cards
   The head used to scroll away and leave the card alone on screen.
   The container now sits INSIDE the sticky stage, so the eyebrow,
   the headline, the line under it and the card all hold together and
   the three travel through a section that is complete on screen.

   ABOVE 900px the two wrappers are display:contents. They vanish from
   the box tree entirely rather than being switched off, so the
   desktop layout is the one it always was — section, container, head,
   strip — and the wide export is still the section. The track and the
   dots are display:none there; they are the only parts that are new.

   THE STAGE IS THE SCREEN, less the bar above it. Inside, the head
   takes what it needs and the track takes the rest: `flex: 1` with
   `min-height: 0`, which is the part flexbox needs spelled out or the
   image refuses to shrink below its own height and pushes the dots
   off the bottom.

   THE CARD FITS ITSELF to whatever that leaves. `object-fit: contain`
   against a box that is 100% both ways means full width when there is
   room for it and shorter when there is not, on a 5.4in phone as
   much as a 6.7in one, without a second breakpoint guessing at it.
   ═══════════════════════════════════════════════════════════════════ */
.how-pin, .how-pin-stage { display: contents; }
.how-track, .how-dots    { display: none; }

@media (max-width: 900px) {

  .how-pin {
    display: block;
    position: relative;
    height: 260vh;
    height: 260svh;
  }

  .how-pin-stage {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    position: sticky;
    top: var(--how-top, 76px);
    height: calc(100vh - var(--how-top, 76px) - 16px);
    height: calc(100svh - var(--how-top, 76px) - 16px);
    overflow: clip;
  }

  .how-pin-stage > .container { flex: none; width: 100%; }

  .how-track {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;              /* or the image will not shrink */
    width: 300vw;
    will-change: transform;
  }

  .how-slide {
    flex: none;
    width: 100vw;
    box-sizing: border-box;
    padding: 0 10px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .how-slide img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  .how-dots { display: flex; flex: none; margin-top: 16px; }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .how-pin       { height: auto; }
  .how-pin-stage { position: static; height: auto; overflow: visible; }
  .how-track     { display: block; width: auto; transform: none !important; }
  .how-slide     { width: auto; padding: 0 10px; margin-bottom: 18px; }
  .how-slide img { height: auto; }
  .how-dots      { display: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   SUITE SCREENS  ·  a swap you can follow
   The panels were shown and hidden with display, which is a hard cut.
   That was fine when a cut was the answer to a click you had just
   made; unattended it reads as a flicker. A short fade in on arrival
   is enough to carry the eye — no move, no scale, nothing that would
   make the panel feel like it is sliding somewhere.

   Only the arriving panel animates. Cross-fading both would need them
   stacked and absolutely positioned, and the panels are different
   heights.
   ═══════════════════════════════════════════════════════════════════ */
@keyframes suite-arrive {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.suite-view.is-shown { animation: suite-arrive .34s var(--ease-out, ease-out) both; }

@media (prefers-reduced-motion: reduce) {
  .suite-view.is-shown { animation: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  nothing after the third card
   Two separate sources of dead space, both after the pin releases.

   THE 120px. #how sets its own padding as an ID rule, so the mobile
   `.section { padding-top: 36px }` never reached it — the section kept
   a desktop 120 top and bottom. The bottom 120 is what you scrolled
   into after the third card: a band of section tint with nothing in
   it. Matched to every other section here instead.

   THE SLACK ROUND THE CARD. The track was `flex: 1` — it took the
   whole stage and centred a card that is shorter than that, which
   put empty stage above and below it and left the dots stranded near
   the bottom of the screen. It is now the smaller of what the screen
   can give and what the card actually needs: 470x507 at (100vw-20)
   wide is (100vw-20) x 1.0787. So on any normal phone the card sets
   the height and there is no slack at all; on a short one the screen
   clamps it and the card letterboxes narrower instead of overflowing.

   --how-media is written by the pin script, because what is left for
   the card is the screen minus the bar, the head and the dots, and
   the head's height depends on how the headline wraps.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  #how { padding-top: 36px; padding-bottom: 0; }

  .how-pin-stage { height: auto; }

  .how-track {
    flex: none;
    height: min(var(--how-media, 380px), calc((100vw - 20px) * 1.0787));
  }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .how-track { height: auto; }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  the track was centred, so it started a card early
   The stage is a flex COLUMN, so its `align-items: center` governs
   the horizontal axis — and it was centring a track three times the
   width of the screen. A 300vw box centred in 100vw begins at -100vw,
   which put card two under the frame at rest and card three a whole
   screen past it by the end. That is the empty space: the track had
   run out of cards before the scroll had run out of travel.

   Two changes. The track starts at the stage's own left edge, and it
   is sized as a percentage of that stage rather than in vw — 300% of
   the frame with three 33.333% slides — so the maths is against the
   box it actually sits in. vw is the viewport INCLUDING any scrollbar,
   which is the other way this drifts.

   The transform moves in percent too, and percent on a transform is
   of the element's own border box: -66.667% of a 300% track is
   exactly two slides, whatever that measures.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .how-track { width: 300%; align-self: flex-start; }
  .how-slide { width: 33.3333%; }
}

@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .how-track { width: auto; }
  .how-slide { width: auto; }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  the wrapper is measured, not guessed
   260svh was a guess at how much scroll the three cards needed, and a
   guess is exactly what leaves a tail: whatever the guess exceeded
   the stage by, minus the two card steps, was scrolling with nothing
   left to move. The script sizes the wrapper from the stage instead —
   the stage, plus one step per card after the first — so the run ends
   on the frame the pin releases and the next section follows straight
   on. A step is 60% of the screen, capped at 520px.

   `height: auto` here is the state before the script has measured,
   and the state if it never does: the section simply renders as its
   head and the first card, with nothing pinned. A leftover 260svh
   would have been 2.5 screens of dead scroll in that case.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .how-pin { height: auto; }
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT WORKS  ·  no pin, still a scroll
   The sticky stage is gone. The section scrolls past like any other,
   and the three cards live in a strip that scrolls sideways on the
   browser's own overflow with snap points on it.

   That is the whole reason to do it this way: a swipe is a swipe, the
   momentum and the rubber band are the platform's own, and no part of
   the page's vertical scroll is being held back to drive it. Nothing
   in here is animated by script — the script only advances it on a
   timer and keeps the dots in step.

   Every property the pin needed is undone by name rather than left to
   be overridden by luck: the stage stops being sticky and gets its
   height back, the track stops being 300% of anything and stops
   carrying a transform, and the slides go back to a full frame each.

   `scroll-snap-align: center` with full-width slides lands each card
   square in the frame from either direction. The scrollbar is hidden
   both ways — WebKit's pseudo-element and the standard property —
   because on a desktop-class browser at this width it would sit
   across the bottom of the card.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  .how-pin {
    display: block;
    position: static;
    height: auto;
  }

  .how-pin-stage {
    display: block;
    position: static;
    top: auto;
    height: auto;
    overflow: visible;
  }

  .how-track {
    display: flex;
    position: static;
    width: auto;
    height: auto;
    flex: none;
    align-self: auto;
    transform: none !important;

    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }
  .how-track::-webkit-scrollbar { display: none; }

  .how-slide {
    flex: none;
    width: 100%;
    box-sizing: border-box;
    padding: 0 10px;
    scroll-snap-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .how-slide img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  .how-dots { display: flex; margin-top: 16px; justify-content: center; }
}

/* Reduced motion keeps the strip — it is a scroll, not an animation —
   and only the timer is dropped, which the script handles. */
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .how-track { display: flex; width: auto; }
  .how-slide { width: 100%; margin-bottom: 0; }
  .how-dots  { display: flex; }
}


/* ════════════════════════════════════════════════════════════════════
   DEMO CARD  ·  dark, white type, a glow on the CTA
   The card is .glass.final, the only child of #demo's container.
   Every rule is scoped to #demo because .btn-primary appears four
   more times on the page — nav, drawer, hero, about — and none of
   those are on dark.

   The fill it replaces is !important (`.final, .footer-shell` is set
   to rgba(255,250,250,.82) earlier in this file), so this is too.
   Nothing else about the card moves: radius, padding, the coloured
   discs on hover and the glass blur are all left where they were.

   The title is named as well as inherited, because .final-title
   carries its own colour (#1B1F7A) and would otherwise stay navy on
   a near-black card.
   ════════════════════════════════════════════════════════════════════ */
#demo .glass.final { background: #1D1D1D !important; }

#demo .glass.final,
#demo .glass.final .final-title,
#demo .glass.final .final-sub,
#demo .glass.final .final-note,
#demo .glass.final .final-dim,
#demo .glass.final p,
#demo .glass.final h2,
#demo .glass.final span { color: #FFFFFF; }

/* The CTA keeps its indigo and its white label — 11.2:1, unchanged.
   What is new is the glow, which is also what separates the button
   from the card: #1B2A9E on #1D1D1D is only 1.33:1, so without this
   the button would have no visible edge.

   Two shadows. A 12% hairline that holds that edge where the bloom is
   widest, and the bloom itself. Both stay at 12% on hover — it
   spreads rather than brightens, so the button never looks like it
   changed colour. The inset highlight along the top is the one the
   button already had. */
#demo .glass.final .btn-primary {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .18),
    0 0 0 1px rgba(255, 255, 255, .12),
    0 0 28px 6px rgba(255, 255, 255, .12);
  transition: box-shadow 300ms ease, background-color .3s var(--ease-out);
}
#demo .glass.final .btn-primary:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .24),
    0 0 0 1px rgba(255, 255, 255, .12),
    0 0 36px 10px rgba(255, 255, 255, .12);
}

/* The glow IS a box-shadow, so a focus ring built from one would be
   swallowed by it. outline is a separate channel and cannot be. */
#demo .glass.final .btn-primary:focus-visible {
  outline: 2px solid #FFFFFF;
  outline-offset: 3px;
}


/* ── DEMO CARD · the glow comes off the card ──────────────────
   Two separate glows were on the card itself, both written for a
   glass surface on a light page and both wrong on near-black:

   · the inset rim on .final — white at .55, plus a blue and a pink
     inset — which on #1D1D1D read as a lit edge round the inside of
     the card rather than the faint glass highlight it is on white.
   · the three blurred discs on .final::before, which bloomed navy,
     white and blue inside the card on hover.

   Both are replaced with a flat surface. The drop shadow under the
   card is kept, so it still sits above the page rather than being
   pasted onto it.

   The CTA glow is NOT touched — that one was asked for and stays. */
#demo .glass.final {
  box-shadow: 0 16px 40px -22px rgba(0, 0, 0, .55);
}
#demo .glass.final::before,
#demo .glass.final:hover::before { opacity: 0; }
#demo .glass.final:hover {
  box-shadow: 0 16px 40px -22px rgba(0, 0, 0, .55);
}


/* ── CLICK SPARK · the canvas ────────────────────────────
   Fixed and over everything. pointer-events:none is what makes that
   safe — the layer is on top of the nav, the drawer and every button,
   and none of them can tell. The highest z-index otherwise on the
   page is 999, the menu drawer.

   It is appended to <body> by the script, not written into the
   markup, so the page still renders exactly as it does with the
   script removed. */
.click-spark {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: block;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}


/* ════════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  a grid of dot cubes
   The section ground is the dot cube tiled, not a drawn grid: it is
   the same asset the full stops are cut from, so the lattice is made
   of the mark the brand already uses rather than of lines.

   THE PITCH does the spacing. dot.png is a 280px canvas whose solid
   body is about 126px — 45% — the rest being its own glow. At a 60px
   tile that comes to a 27px cube with 33px of air round it, which
   reads as a grid at arm's length and as cubes up close.

   THE FADE is a mask, not a gradient overlay. An overlay would have
   to match whatever is behind the section, and the section is
   transparent over the page wash — there is no single colour to
   match. A mask takes the grid itself to nothing, so it works over
   anything.

   IT SITS BEHIND THE COPY. .section is already position:relative, so
   inset:0 fills it; the container is lifted to z-index 1 because a
   z-index 0 sibling would otherwise paint over static content.

   The two side-lights on .about::before / ::after are untouched.
   ════════════════════════════════════════════════════════════════════ */
.about-grid {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;

  background-image: url('dot.png');
  background-repeat: repeat;
  /* 7% of the asset's own 280px canvas = 20px a tile, which puts the
     solid body of the cube at about 9px. Was 42. */
  background-size: 20px 20px;
  background-position: center center;
  opacity: .06;

  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, #000 18%, #000 82%, transparent 100%);
          mask-image: linear-gradient(to bottom,
    transparent 0%, #000 18%, #000 82%, transparent 100%);
}

.about > .container { position: relative; z-index: 1; }

/* Tighter on a phone, where 60px would put barely three cubes across
   and read as spots rather than a grid. */
@media (max-width: 900px) {
  .about-grid { background-size: 15px 15px; opacity: .06; }
}


/* ════════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  the grid at 4%, and a wave through it
   A ring of brighter cubes travelling from outside the section in to
   the centre, once every 1600ms.

   TWO LAYERS, NOT ONE. opacity on an element applies to everything
   inside it, pseudo-elements included — so a crest drawn on a 4%
   parent could never be brighter than 4%. The pattern therefore moves
   off .about-grid onto ::before, and the wrapper goes back to full
   opacity carrying nothing but the top-and-bottom fade. ::before is
   the grid at 4%; ::after is the same grid at 10%, showing only where
   the wave is.

   THE WAVE IS A CONTRACTING MASK. A ring-shaped radial-gradient sits
   on ::after at 300% of the section, so it starts wholly outside it,
   and mask-size runs to 0 over 1600ms. Shrinking the mask is what
   pulls the ring inward — the cubes themselves never move or resize,
   which is the point: it should read as a wave passing THROUGH a
   fixed grid, not as the grid animating.

   IT FADES AT BOTH ENDS of the cycle, so there is no frame where a
   ring vanishes at the centre and reappears at the edge.

   IT IS PAUSED UNTIL THE SECTION IS ON SCREEN. mask-size is not a
   composited property, so this repaints every frame it runs; there is
   no reason to pay that while you are reading the hero.
   ════════════════════════════════════════════════════════════════════ */
.about-grid {
  background-image: none;      /* it lives on ::before now */
  opacity: 1;
}

.about-grid::before,
.about-grid::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: url('dot.png');
  background-repeat: repeat;
  background-size: 20px 20px;
  background-position: center center;
}

/* the grid itself */
.about-grid::before { opacity: .04; }

/* the crest */
.about-grid::after {
  opacity: 0;
  -webkit-mask-image: radial-gradient(closest-side,
    transparent 54%, #000 70%, #000 84%, transparent 98%);
          mask-image: radial-gradient(closest-side,
    transparent 54%, #000 70%, #000 84%, transparent 98%);
  -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
  -webkit-mask-position: center;   mask-position: center;
  -webkit-mask-size: 300% 300%;    mask-size: 300% 300%;

  animation: about-wave 1600ms linear infinite;
  animation-play-state: paused;
}
.about.is-waving .about-grid::after { animation-play-state: running; }

@keyframes about-wave {
  0%   { -webkit-mask-size: 300% 300%; mask-size: 300% 300%; opacity: 0; }
  14%  { opacity: .10; }
  86%  { opacity: .10; }
  100% { -webkit-mask-size: 0% 0%;     mask-size: 0% 0%;     opacity: 0; }
}

@media (max-width: 900px) {
  .about-grid::before,
  .about-grid::after { background-size: 15px 15px; }
}

/* No wave without motion. The grid stays; only the crest stops. */
@media (prefers-reduced-motion: reduce) {
  .about-grid::after { animation: none; opacity: 0; }
}


/* ════════════════════════════════════════════════════════════════════
   HERO  ·  the navy dome
   On .hero itself, as specified. Three notes on what this displaces,
   none of which change the rules below:

   · the background wins over `.hero, .integrations, … { background:
     transparent !important }` earlier in this file by being later at
     equal weight. That group rule is untouched, so the other eleven
     sections in it stay transparent.
   · .hero::before was the hero contrast scrim, a pale wash that held
     the headline clear of a passing tile. It is replaced here.
   · .hero::after was disabled at `display: none`; `display: block`
     brings it back for the handoff.

   .hero is inside <main> (z-index 1) and #field / #lines / #cube3d are
   fixed at z-index 0, so this ground paints over all three for the
   height of the hero. That is the known trade of putting it here.
   ════════════════════════════════════════════════════════════════════ */
.hero {
  position: relative;
  background: radial-gradient(170% 116% at 50% 0%,
    #070A1F 0%, #0A0F2E 26%, #0D1440 38%, #121C5C 46%,
    #19297C 52%, #22389F 57%, #2A46C8 61%, #2D4DE0 64%,
    #4863E5 68%, #6E84EB 72%, #98A8F1 76.5%, #BEC9F7 81%,
    #DAE1FA 86%, #EEF1FD 91%, #F5F6FA 100%) !important;
}

.hero::before {
  content: "";
  position: absolute;
  inset: auto;
  left: 50%;
  top: -30%;
  width: 210%;
  height: 130%;
  margin-left: -105%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    transparent 62%, rgba(45, 77, 224, .35) 74%, transparent 90%);
  filter: blur(30px);
  pointer-events: none;
  animation: pulse-ring 10s ease-in-out infinite;
}

.hero::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  height: 40%;
  background: linear-gradient(180deg,
    rgba(245, 246, 250, 0), rgba(245, 246, 250, .55) 70%, #F5F6FA);
  mask-image: none;
  -webkit-mask-image: none;
  pointer-events: none;
}

@keyframes pulse-ring {
  0%, 100% { opacity: .55; }
  50% { opacity: .8; }
}

/* Added, not in the supplied block: the ring pulses forever, so it
   needs an off switch for anyone who has asked the OS for less
   motion. The ring itself stays — only the pulse stops. */
@media (prefers-reduced-motion: reduce) {
  .hero::before { animation: none; }
}


/* ════════════════════════════════════════════════════════════════════
   NAV OVER THE HERO  ·  white while it is dark, itself once it is not
   Everything here hangs off :not(.is-stuck). That class is already
   toggled by the sentinel observer at the top of the page, so there
   is no new script and no new state — and the revert is free: the
   moment .is-stuck lands, every one of these selectors stops
   matching and the existing light-bar rules take back over.

   THE WORDMARK IS ALREADY WHITE. The asset is a white PNG that gets
   `filter: invert(1) brightness(.18)` to sit on a light bar. Over the
   dome the fix is to stop inverting it, not to paint it.

   The transitions are what stop the handover reading as a flicker.
   The bar already cross-fades its own background over .32s; the
   contents now travel with it instead of snapping a frame later.
   ════════════════════════════════════════════════════════════════════ */
.brand-logo    { transition: filter .32s var(--ease-out); }
.nav-link      { transition: color .32s var(--ease-out), background-color .32s var(--ease-out); }
.nav-burger span { transition: background-color .32s var(--ease-out),
                               transform .24s var(--ease-out),
                               opacity .18s var(--ease-out); }

.nav:not(.is-stuck) .brand-logo { filter: none; }

.nav:not(.is-stuck) .nav-link {
  color: rgba(255, 255, 255, .5);
  background: transparent;
}
.nav:not(.is-stuck) .nav-link:hover {
  color: #FFFFFF;
  background: rgba(255, 255, 255, .10);
}
.nav:not(.is-stuck) .nav-link[aria-current] {
  color: #FFFFFF;
  background: rgba(255, 255, 255, .12);
}

.nav:not(.is-stuck) .nav-burger span { background: #FFFFFF; }


/* ── HERO TYPE ON THE DOME ────────────────────────────────
   Scoped to .hero, so the same classes anywhere else on the page are
   untouched. (0,2,0) beats the page-wide `.headline, .section-title,
   … { color: #1B1F7A }`, which stays as it is for every other
   heading.

   The second half of the headline goes to #ECECEC rather than white
   with it: it is the de-emphasised half by design, and #FFFFFF for
   both would flatten the line into one weight. #ECECEC on the dome at
   that height is 7.0:1, so nothing is lost by holding it back. */
.hero .headline      { color: #FFFFFF; }
.hero .headline .dim { color: #ECECEC; }
.hero .lede          { color: #ECECEC; }


/* ════════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  a drop, not a closing ring
   The wave now runs the other way: out from the centre, 5000ms.

   THREE RINGS, ONE MASK. A drop is not one ring — it is a leading
   crest with smaller ones trailing behind it. Rather than three
   elements, the mask itself carries three bands, and because the
   whole mask scales together the gaps between them open out as the
   ripple travels, which is what water does.

   THE LEADING RING IS THE STRONGEST — 1.0 at the outside, .62 behind
   it, .30 behind that. Reverse that order and it reads as something
   being sucked in rather than spreading.

   EASE-OUT, not linear: it leaves the centre quickly and slows as it
   widens. Linear is the one curve water never moves on.

   mask-size runs 0 → 340%, so it begins as a point at the centre and
   ends past the corners of the section.
   ════════════════════════════════════════════════════════════════════ */
.about-grid::after {
  -webkit-mask-image: radial-gradient(closest-side,
    transparent 24%, rgba(0, 0, 0, .30) 32%, transparent 40%,
    transparent 52%, rgba(0, 0, 0, .62) 60%, transparent 68%,
    transparent 78%, rgba(0, 0, 0,  1 ) 86%, transparent 94%);
          mask-image: radial-gradient(closest-side,
    transparent 24%, rgba(0, 0, 0, .30) 32%, transparent 40%,
    transparent 52%, rgba(0, 0, 0, .62) 60%, transparent 68%,
    transparent 78%, rgba(0, 0, 0,  1 ) 86%, transparent 94%);

  -webkit-mask-size: 0% 0%;
          mask-size: 0% 0%;

  animation-duration: 5000ms;
  animation-timing-function: cubic-bezier(.22, .61, .36, 1);
}

@keyframes about-wave {
  0%   { -webkit-mask-size: 0% 0%;     mask-size: 0% 0%;     opacity: 0; }
  8%   { opacity: .12; }
  62%  { opacity: .10; }
  100% { -webkit-mask-size: 340% 340%; mask-size: 340% 340%; opacity: 0; }
}


/* ── HERO · the second line at half ─────────────────────────
   White at 50% rather than a fixed grey, so the held-back half tracks
   the dome underneath it instead of sitting at one value over a
   gradient that moves through five of them. It resolves to about
   #8C94BE where the line sits: 3.6:1, which clears the 3:1 floor
   display type is held to. */
.hero .headline .dim { color: #FFFFFF; opacity: .5; }


/* ════════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  the drop, inward, and actually round
   Four corrections.

   IT WAS AN ELLIPSE. `radial-gradient(closest-side, …)` with no shape
   keyword defaults to an ELLIPSE on a non-square box, and this
   section is about three times wider than it is tall — so the "ring"
   was a flattened oval the whole time. `circle` fixes it.

   AND THE SIZE IS IN PIXELS NOW, not percent. A percentage mask-size
   on a 3:1 box gives a 3:1 mask box, which would bend the circle back
   into an oval however the gradient is declared. A square box in px
   cannot. 2400px covers the section's corners at any width.

   IT TRAVELS INWARD. Out to in, so mask-size runs large to small.
   The leading edge of an inward wave is its INNERMOST ring, so the
   band strengths are reversed too — 1.0 on the inside, then .60,
   .34, .18 trailing behind it. Four bands rather than three, which is
   what gives it the detail of a drop rather than a hoop.

   AND IT SLOWS AS IT CLOSES. 3500ms to cross most of the section,
   then 3000ms for the last stretch into the centre — 6500ms in all.
   The split is a keyframe at 53.85% (3500 of 6500) with an ease-out
   on the segment after it, so the ring crawls the closer it gets.
   ════════════════════════════════════════════════════════════════════ */
.about-grid::after {
  -webkit-mask-image: radial-gradient(circle closest-side,
    transparent 26%, rgba(0, 0, 0,  1  ) 35%, transparent 44%,
    transparent 52%, rgba(0, 0, 0, .60) 58%, transparent 65%,
    transparent 72%, rgba(0, 0, 0, .34) 77%, transparent 83%,
    transparent 88%, rgba(0, 0, 0, .18) 92%, transparent 97%);
          mask-image: radial-gradient(circle closest-side,
    transparent 26%, rgba(0, 0, 0,  1  ) 35%, transparent 44%,
    transparent 52%, rgba(0, 0, 0, .60) 58%, transparent 65%,
    transparent 72%, rgba(0, 0, 0, .34) 77%, transparent 83%,
    transparent 88%, rgba(0, 0, 0, .18) 92%, transparent 97%);

  -webkit-mask-size: 2400px 2400px;
          mask-size: 2400px 2400px;

  animation-duration: 6500ms;
  animation-timing-function: linear;   /* the shaping is per keyframe */
}

@keyframes about-wave {
  /* out at the edge, arriving */
  0%       { -webkit-mask-size: 2400px 2400px; mask-size: 2400px 2400px;
             opacity: 0;   animation-timing-function: linear; }
  7%       { opacity: .14; animation-timing-function: linear; }

  /* 3500ms of 6500 gone, and most of the distance with it */
  53.85%   { -webkit-mask-size: 620px 620px;   mask-size: 620px 620px;
             opacity: .14;
             animation-timing-function: cubic-bezier(.14, .86, .28, 1); }

  /* the last stretch, crawling */
  92%      { opacity: .09; }
  100%     { -webkit-mask-size: 0px 0px;       mask-size: 0px 0px;
             opacity: 0; }
}


/* ── HERO · the lede, and the second CTA as a stroke ───────────
   THE LEDE at 12px. Scoped to .hero rather than set on --fs-lede,
   because that token also sizes .about-body and .final-sub and
   neither of those was asked to move. (0,2,0) also puts it over the
   flat `.lede { font-size: 15px }` in the 900px block, so the size
   holds on a phone too.

   GET EARLY ACCESS becomes a stroke. It was a 70%-white fill with a
   navy label — a light chip designed for a light page, which on the
   dome read as the louder of the two buttons even though it is the
   secondary one. Empty with a white edge puts the order back.

   The border is 55% white rather than solid: a full-strength stroke
   next to the filled primary competes with it. It firms to 85% on
   hover, which is the whole hover — no fill, nothing that would make
   it look like the primary for a moment. */
/* The lede: 14px, and set to fall on two lines.

   TWO LINES IS A MEASURE, not a line count you can declare — there is
   no CSS property for "wrap here". The copy is 194 characters, so two
   lines means about 97 each, and at 14px in DM Sans a character
   averages a shade under 7px. 760px carries 102 of them, which holds
   at two lines across the range that average could plausibly be
   (6.6 to 7.4px), rather than sitting one word away from a third.

   62ch — what .lede carries — would have been about 480px and four
   lines at this size, so the width has to move with the type.

   text-wrap: balance evens the pair. Without it the first line runs
   full and the second is left with a few words, which on centred
   text reads as a mistake. It replaces text-wrap: pretty, whose job
   (avoiding a one-word last line) balance does better over two.

   min(…, 100%) so a phone narrower than 760px wraps to more lines
   rather than pushing the page sideways. */
.hero .lede {
  font-size: 14px;
  max-width: min(760px, 100%);
  text-wrap: balance;
}

.hero .btn-ghost {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, .55);
  color: #FFFFFF;
  box-shadow: none;
  transition: border-color .28s var(--ease-out), color .28s var(--ease-out);
}
.hero .btn-ghost:hover {
  background: transparent;
  border-color: rgba(255, 255, 255, .85);
  color: #FFFFFF;
}


/* ════════════════════════════════════════════════════════════════════
   INTEGRATIONS  ·  the two sides, on the hero's own colour
   The strip is transparent, so what shows at its left and right is
   whatever body::before is doing at that height — and at that height
   it is doing quite a lot. The two washes are anchored at
   `40px 500px` and `calc(100% + 170px) 120px`, which puts one of them
   directly on the left edge of this row and the tail of the other on
   the right. That blue is the colour difference: the middle of the
   strip reads as the page, the sides read as the wash.

   So both sides are painted #F5F6FA — the hero gradient's last stop,
   the same value the page ground now carries — and fade out before
   they reach the logos.

   IT IS A HORIZONTAL BAND, NOT A FILL. Filling the section would put
   opaque ground behind the whole row, and an earlier decision here
   deliberately removed exactly that so the travelling cube could pass
   BEHIND the logos rather than dissolving in front of them. Clear
   through the middle 40% leaves that alone.

   The stops line up with the marquee's own edge mask, which takes the
   logos to nothing over the outer 10% — so the logos fade out into
   flat colour rather than fading out over a gradient.
   ════════════════════════════════════════════════════════════════════ */
.integrations {
  background: linear-gradient(90deg,
    #F5F6FA                   0%,
    #F5F6FA                  14%,
    rgba(245, 246, 250, .55) 24%,
    rgba(245, 246, 250, 0)   32%,
    rgba(245, 246, 250, 0)   68%,
    rgba(245, 246, 250, .55) 76%,
    #F5F6FA                  86%,
    #F5F6FA                 100%) !important;
}


/* ════════════════════════════════════════════════════════════════════
   THE TWO WASHES COME OFF
   body::before carried a blue disc off the top-right corner and a
   second one low on the left. With the page ground now matched to the
   hero's own last stop, they were the only thing left putting colour
   into the middle of the page — which is what showed as a dark circle
   at those two corners.

   The element stays (fixed, inset 0, z-index -1, and still
   !important, because the declaration it overrides is). Only the
   paint goes, so nothing about the stacking moves.
   ════════════════════════════════════════════════════════════════════ */
body::before {
  background-image: none !important;
  background-color: transparent !important;
}


/* ════════════════════════════════════════════════════════════════════
   MARKETING SUITE  ·  the supplied arc
   The block as given, with .hero swapped for #marketing throughout —
   that is the only edit. Same geometry, same sixteen stops, same
   ring, same ramp.

   It reads the opposite way round to the hero without moving the
   anchor: both are 170% x 116% at 50% 0%, but the stop list here runs
   light to dark instead of dark to light. So the light radiates from
   the top centre and the dark is what the light does not reach — the
   floor of the section and both outer edges, with the boundary
   curving down through the middle.

   background carries !important because `… #marketing … { background:
   transparent !important }` earlier in this file would otherwise win.

   .container is lifted to z-index 1: ::after is a generated last
   child, so without it the ramp would paint over the suite panel.
   ════════════════════════════════════════════════════════════════════ */
#marketing {
  position: relative;
  background: radial-gradient(170% 116% at 50% 0%,
    #F5F6FA 0%, #F5F6FA 30%, #EEF1FD 38%, #DAE1FA 43%,
    #BEC9F7 48%, #98A8F1 52.5%, #6E84EB 57%, #4863E5 61%,
    #2D4DE0 64%, #2A46C8 67%, #22389F 71%, #19297C 76%,
    #121C5C 82%, #0D1440 88%, #0A0F2E 94%, #070A1F 100%) !important;
}

#marketing::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(170% 116% at 50% 0%,
    transparent 54%, rgba(45, 77, 224, .35) 64%, transparent 76%);
  filter: blur(30px);
  pointer-events: none;
  animation: pulse-ring 10s ease-in-out infinite;
}

#marketing::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  height: 40%;
  background: linear-gradient(180deg,
    rgba(10, 15, 46, 0), rgba(10, 15, 46, .55) 70%, #0A0F2E);
  mask-image: none;
  -webkit-mask-image: none;
  pointer-events: none;
}

@keyframes pulse-ring {
  0%, 100% { opacity: .55; }
  50% { opacity: .8; }
}

/* the copy and the suite panel back over both pseudos */
#marketing > .container { position: relative; z-index: 1; }

/* the ring loops forever, so it needs an off switch */
@media (prefers-reduced-motion: reduce) {
  #marketing::before { animation: none; }
}


/* ════════════════════════════════════════════════════════════════════
   WHO BUILT THIS  ·  dark ground, white cubes
   #070A1F is the first stop of both domes on the page, so the section
   lands on a colour the design already uses rather than a new near-
   black. !important because `… #about … { background: transparent
   !important }` earlier in this file would otherwise win.

   THE CUBES GO WHITE BY FILTER, not by a second asset. dot.png is a
   blue-purple cube with its own glow, and `brightness(0) invert(1)`
   drives every channel to white while leaving the alpha — and the
   glow's soft edge with it — exactly as it was. A recolour that kept
   the shape but flattened the falloff would have lost what makes it
   a cube rather than a square.

   Filter, not mask: mask-image is already doing two jobs on these
   elements — the top-and-bottom fade on the wrapper, the travelling
   ring on ::after — and a third would have to be composited with
   them.

   6% is the grid. The crest keeps its own, higher value so the wave
   still reads: white on #070A1F at 6% is faint by design, and a wave
   that arrives at the same 6% would not be a wave.
   ════════════════════════════════════════════════════════════════════ */
#about { background: #070A1F !important; }

.about-grid::before,
.about-grid::after {
  filter: brightness(0) invert(1);
}

.about-grid::before { opacity: .06; }

@keyframes about-wave {
  0%       { -webkit-mask-size: 2400px 2400px; mask-size: 2400px 2400px;
             opacity: 0;   animation-timing-function: linear; }
  7%       { opacity: .18; animation-timing-function: linear; }
  53.85%   { -webkit-mask-size: 620px 620px;   mask-size: 620px 620px;
             opacity: .18;
             animation-timing-function: cubic-bezier(.14, .86, .28, 1); }
  92%      { opacity: .12; }
  100%     { -webkit-mask-size: 0px 0px;       mask-size: 0px 0px;
             opacity: 0; }
}


/* ── WHO BUILT THIS · type on the dark ground ────────────────
   All three scoped to #about, so the same classes everywhere else on
   the page keep the colours they have. Each selector is (1,1,0),
   which is what it takes to beat the page-wide rules that own them:
   `.headline, .section-title, .final-title, .about-title { color:
   #1B1F7A }` for the heading, `.about-body { color: var(--text-mid) }`
   for the paragraph.

   The paragraph is white at 50% rather than a fixed grey — it lands
   on about #838383 over this ground, 5.4:1, and stays correct if the
   ground ever moves. The heading and the eyebrow are 19.6:1. */
#about .about-title { color: #FFFFFF; }
#about .about-body  { color: #FFFFFF; opacity: .5; }
#about .pill        { color: #FFFFFF; }


/* ════════════════════════════════════════════════════════════════════
   MARKETING SUITE  ·  the ground goes behind the cube
   Same gradient, same ring, same ramp — moved off #marketing and onto
   a layer outside <main>.

   WHY IT HAD TO MOVE. #cube3d is fixed at z-index 0; <main> is
   position:relative z-index 1. Every section is a descendant of main,
   so a background on #marketing is inside a stacking context that
   sits entirely above the cube — and no z-index on a descendant can
   climb out of it. Painting the section at all hid the cube for the
   length of it. At -1 the order is: page, ground, cube, content.

   The section goes back to transparent, which is what it was.

   TOP AND HEIGHT ARE WRITTEN BY SCRIPT, because a body-level layer
   has no idea where the section it belongs to starts. Everything
   else — the arc, the glow, the ramp — is geometry relative to this
   box, so it follows the section wherever the script puts it.
   ════════════════════════════════════════════════════════════════════ */
#marketing            { background: transparent !important; }
#marketing::before,
#marketing::after     { content: none; }

.mk-bg {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;            /* both written by the placement script */
  height: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;

  /* The same ramp, with its flat lead-in removed. It used to hold
     #F5F6FA for the first 30%, which on this taller box pushed the
     first hint of colour about 350px below the card. The remaining
     stops are rescaled from 30..100 onto 0..100, so the shape of the
     ramp is unchanged and only the dead head is gone. */
  background:
    /* The dome is anchored at top CENTRE, so at the layer's top edge
       its left and right ends are already several stops along and
       arrive as a step. This closes that edge on the page colour, so
       the ground begins at the card as a fade rather than a line. */
    linear-gradient(180deg,
      rgb(var(--page-rgb))        0%,
      rgba(var(--page-rgb), .78)  5%,
      rgba(var(--page-rgb), .34) 10%,
      rgba(var(--page-rgb), 0)   16%),

    radial-gradient(170% 116% at 50% 0%,
    #F5F6FA 0%, #EEF1FD 11.4%, #DAE1FA 18.6%, #BEC9F7 25.7%,
    #98A8F1 32.1%, #6E84EB 38.6%, #4863E5 44.3%, #2D4DE0 48.6%,
    #2A46C8 52.9%, #22389F 58.6%, #19297C 65.7%, #121C5C 74.3%,
    #0D1440 82.9%, #0A0F2E 91.4%, #070A1F 100%);
}

.mk-bg::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(170% 116% at 50% 0%,
    transparent 54%, rgba(45, 77, 224, .35) 64%, transparent 76%);
  filter: blur(30px);
  pointer-events: none;
  animation: pulse-ring 10s ease-in-out infinite;
}

.mk-bg::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  height: 40%;
  background: linear-gradient(180deg,
    rgba(10, 15, 46, 0), rgba(10, 15, 46, .55) 70%, #0A0F2E);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .mk-bg::before { animation: none; }
}


/* ── DEMO CARD · on the page's own near-black ────────────────
   #070A1F is the first stop of both domes and the ground of the
   about section, so the card now matches the darkest colour the page
   already uses instead of being the only neutral black on it.
   Still !important — `.final, .footer-shell` is set with it earlier. */
#demo .glass.final { background: #070A1F !important; }


/* ════════════════════════════════════════════════════════════════════
   FOOTER  ·  the dot-cube grid, cut off on an arc
   The same tile as the about section, at the same 20px pitch, so the
   two read as one pattern the page returns to rather than two
   different textures.

   THE FADE IS AN ARC, not a line. A linear mask would cut the grid on
   a horizontal edge straight across the page, which is the one shape
   that announces itself as a mask. The mask here is a radial anchored
   at the bottom centre, so the grid is at full strength along the
   floor and thins as it climbs — and because the gradient is wider
   than it is tall (140% x 112%) the boundary arrives as a shallow
   dome rather than a semicircle.

   Its colour is the asset's own. The about grid is filtered white for
   a near-black ground; this one sits on the page's near-white, where
   the cube's own blue is what makes it legible at 7%.

   Behind the footer card, which is a translucent glass panel — so
   the grid reads through it, slightly softened, which is the point of
   putting it under rather than beside.
   ════════════════════════════════════════════════════════════════════ */
.footer-shell { position: relative; }

.footer-grid-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;

  background-image: url('dot.png');
  background-repeat: repeat;
  background-size: 20px 20px;
  background-position: center center;
  opacity: .07;

  -webkit-mask-image: radial-gradient(140% 112% at 50% 100%,
    #000 0%, #000 44%, rgba(0, 0, 0, .55) 68%, rgba(0, 0, 0, .18) 86%, transparent 100%);
          mask-image: radial-gradient(140% 112% at 50% 100%,
    #000 0%, #000 44%, rgba(0, 0, 0, .55) 68%, rgba(0, 0, 0, .18) 86%, transparent 100%);
}

/* the card, the glow and the base row back over it */
.footer-shell > .container,
.footer-glow { position: relative; z-index: 1; }

@media (max-width: 900px) {
  .footer-grid-art { background-size: 15px 15px; }
}


/* ── FOOTER GRID · half again as much fade ───────────────────
   The ramp ran 44% → 100% of the radius, 56% of it. It runs 16% →
   100% now: 84%, which is that 56 increased by half. The two middle
   stops keep their places WITHIN the ramp — .55 was 43% of the way
   along it, .18 was 75% — so the curve of the falloff is unchanged
   and only its length has moved.

   What that does: the grid is at full strength across the bottom
   sixth instead of the bottom half, and spends the rest of the
   footer thinning out. Same arc, much more of it given over to the
   fade, so the top edge of the pattern is further from anything you
   could call an edge. */
.footer-grid-art {
  -webkit-mask-image: radial-gradient(140% 112% at 50% 100%,
    #000 0%, #000 16%, rgba(0, 0, 0, .55) 52%, rgba(0, 0, 0, .18) 79%, transparent 100%);
          mask-image: radial-gradient(140% 112% at 50% 100%,
    #000 0%, #000 16%, rgba(0, 0, 0, .55) 52%, rgba(0, 0, 0, .18) 79%, transparent 100%);
}


/* ════════════════════════════════════════════════════════════════════
   SHINY TEXT  ·  the CTA labels
   The component, as CSS. Its whole mechanism is already declarative
   — a two-width gradient clipped to the glyphs, slid from 150% to
   -50% — so the rAF loop, the motion value and the useTransform all
   collapse into one keyframe. What React was buying there was the
   ability to pause and reverse at runtime; nothing here does either.

   IT IS ON AN INNER SPAN, and it has to be. background-clip: text
   clips THE ELEMENT'S OWN BACKGROUND to its glyphs — put it on .btn
   and the button's fill becomes the word "Book a demo" and the button
   disappears. The markup now wraps each label in <span class="shine">,
   which is exactly what the component renders.

   THE COLOURS COME FROM THE BUTTON. Rather than the component's fixed
   #b5b5b5 / #ffffff pair, the base is currentColor held back to 72%
   and the shine is currentColor at full. So one rule covers a white
   label on navy, a white label on a stroke, and a navy label on
   white, and each one shines in its own colour. A fixed pair would
   have needed a rule per button and would have washed the dark
   labels out.

   2s of sweep, then it waits. The component's default (delay 0) is a
   sweep every two seconds forever, which on seven buttons at once is
   a page that will not sit still. The keyframe covers the travel in
   the first 40% of a 5s cycle and holds off-screen for the rest —
   the same thing the component's `delay` prop does.

   @supports, because the fallback for background-clip: text is
   invisible text: -webkit-text-fill-color: transparent applies
   whether or not the clip does.
   ════════════════════════════════════════════════════════════════════ */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {

  .btn .shine,
  .btn-outline .shine {
    /* 120deg and 200% auto are the component's spread and size */
    background-image: linear-gradient(120deg,
      color-mix(in srgb, currentColor 72%, transparent)  0%,
      color-mix(in srgb, currentColor 72%, transparent) 35%,
      currentColor                                      50%,
      color-mix(in srgb, currentColor 72%, transparent) 65%,
      color-mix(in srgb, currentColor 72%, transparent) 100%);
    background-size: 200% auto;
    background-position: 150% center;
    background-repeat: no-repeat;

    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;

    animation: shine-sweep 5s linear infinite;
  }

  /* 150% -> -50% is the component's transform, end to end */
  @keyframes shine-sweep {
    0%   { background-position: 150% center; }
    40%  { background-position: -50% center; }
    100% { background-position: -50% center; }
  }
}

/* No sweep without motion, and the fill comes back with it — leaving
   text-fill transparent with nothing moving would just dim the label. */
@media (prefers-reduced-motion: reduce) {
  .btn .shine,
  .btn-outline .shine {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: currentColor;
  }
}


/* ════════════════════════════════════════════════════════════════════
   SHINY TEXT  ·  the sweep, with the label left alone
   The clip comes off the text entirely.

   WHY IT HAD TO. A gradient clipped to glyphs can only shine by
   making the rest of the word DARKER than its own colour — that is
   what the component's #b5b5b5 base is doing under its white shine.
   There is no way to run that on a label which is already #FFFFFF
   except by holding the label back to something less than white,
   which is the fade you are seeing. The technique and "the text must
   not dim" are the same knob turned opposite ways.

   SO THE SWEEP MOVES OFF THE GLYPHS AND ONTO THE BUTTON. Same angle,
   same 200% width, same 150% → -50% travel, same 5s cycle — a band
   of light crossing the whole pill instead of the letters in it. The
   label sits above it at full strength and is never repainted, so it
   cannot fade, thin or disappear, in any browser, at any moment of
   the cycle.

   TWO BANDS. White at 28% over the filled and stroked buttons, which
   are all on dark now. Navy at 10% for .btn-outline, which is the one
   light-on-light button on the page — a white band there would be
   white on white and show nothing at all.

   overflow: hidden is what keeps the band inside the 999px radius;
   border-radius: inherit on the pseudo keeps its own corners matching
   in browsers that clip the ends differently.
   ════════════════════════════════════════════════════════════════════ */

/* the label, untouched */
.btn .shine,
.btn-outline .shine {
  background-image: none;
  -webkit-background-clip: border-box;
          background-clip: border-box;
  -webkit-text-fill-color: currentColor;
  animation: none;
  position: relative;
  z-index: 1;
}

.btn,
.btn-outline {
  position: relative;
  overflow: hidden;
}

.btn::after,
.btn-outline::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 0;

  background-image: linear-gradient(120deg,
    transparent 0%, transparent 40%,
    rgba(255, 255, 255, .28) 50%,
    transparent 60%, transparent 100%);
  background-size: 200% auto;
  background-position: 150% center;
  background-repeat: no-repeat;

  animation: shine-sweep 5s linear infinite;
}

/* the one light-on-light button */
.btn-outline::after {
  background-image: linear-gradient(120deg,
    transparent 0%, transparent 40%,
    rgba(10, 15, 46, .10) 50%,
    transparent 60%, transparent 100%);
}

/* at top level now, not inside the @supports the clip needed */
@keyframes shine-sweep {
  0%   { background-position: 150% center; }
  40%  { background-position: -50% center; }
  100% { background-position: -50% center; }
}

@media (prefers-reduced-motion: reduce) {
  .btn::after,
  .btn-outline::after { animation: none; opacity: 0; }
}


/* ════════════════════════════════════════════════════════════════════
   SHINY TEXT  ·  on the letters, and additive
   Back on the text, off the card — and built so it can only ever ADD
   light, never take any away.

   THE PROBLEM IT SOLVES. Clipping a gradient to the glyphs, the way
   the component does, shines by holding the base colour BELOW its
   real value — #b5b5b5 under a white highlight. On a label that is
   already white there is nowhere below to go except grey, which is
   the fade. So the label here is never repainted at all.

   HOW. A second copy of the label sits exactly over the first,
   drawn from data-text. It has no fill of its own — only a glow —
   and a mask slides across it, so at any instant a few letters are
   glowing and the rest of the copy is invisible. Underneath,
   the real label is untouched at full strength the whole time.

   The mask is the component's own motion: 200% wide, 120deg, running
   150% → -50%, holding off-screen for the rest of a 5s cycle.

   TWO GLOWS. White over the labels that sit on dark. Pulse blue for
   .btn-outline, the one dark label on a light button — a white glow
   on navy letters over a white pill reads as the word breaking up,
   which is the thing being avoided.

   aria-hidden is not needed: ::after content is not exposed to
   assistive tech, and the real label is still the element's text.
   ════════════════════════════════════════════════════════════════════ */

/* the band on the pill comes off */
.btn::after,
.btn-outline::after { content: none; }
.btn,
.btn-outline { overflow: visible; }

.btn .shine,
.btn-outline .shine {
  position: relative;
  display: inline-block;
  z-index: 1;
}

.btn .shine::after,
.btn-outline .shine::after {
  content: attr(data-text);
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  pointer-events: none;

  /* no fill: the glow is the whole of it */
  color: transparent;
  text-shadow:
    0 0 9px  rgba(255, 255, 255, .95),
    0 0 20px rgba(255, 255, 255, .55);

  -webkit-mask-image: linear-gradient(120deg,
    transparent 0%, transparent 40%, #000 50%, transparent 60%, transparent 100%);
          mask-image: linear-gradient(120deg,
    transparent 0%, transparent 40%, #000 50%, transparent 60%, transparent 100%);
  -webkit-mask-size: 200% auto;  mask-size: 200% auto;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: 150% center; mask-position: 150% center;

  animation: shine-mask 5s linear infinite;
}

/* the dark label on the light pill */
.btn-outline .shine::after {
  text-shadow:
    0 0 9px  rgba(45, 77, 224, .75),
    0 0 20px rgba(45, 77, 224, .40);
}

@keyframes shine-mask {
  0%   { -webkit-mask-position: 150% center;  mask-position: 150% center; }
  40%  { -webkit-mask-position: -50% center;  mask-position: -50% center; }
  100% { -webkit-mask-position: -50% center;  mask-position: -50% center; }
}

@media (prefers-reduced-motion: reduce) {
  .btn .shine::after,
  .btn-outline .shine::after { animation: none; opacity: 0; }
}


/* ════════════════════════════════════════════════════════════════════
   HERO TEXT ENTRANCE
   As supplied, with START resolved to 1.0s — 800ms of cube arrival
   plus the brief's 0.2. (The full 7.8s settle would have left the
   headline blank for eight seconds; the reasoning is on the script.)

   The padding/margin pair on .w is what stops overflow:hidden cutting
   descenders: the box is .08em deeper than the line and pulled back
   by the same amount, so the clip sits below the baseline where the
   tail of a "y" lives, and nothing around it moves.

   There is no eyebrow or pill in this hero — the section has neither
   — so those two rules match nothing today. They are kept as given so
   the entrance is already there if one is added.
   ════════════════════════════════════════════════════════════════════ */
.hero .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: .08em;
  margin-bottom: -.08em;
}
.hero .w > span {
  display: inline-block;
  transform: translateY(105%);
  filter: blur(8px);
  opacity: 0;
  animation: word-rise .9s cubic-bezier(.2, .8, .2, 1) forwards;
  animation-delay: var(--d);
}
@keyframes word-rise {
  to { transform: none; filter: blur(0); opacity: 1; }
}

.hero .dot {
  display: inline-block;
  transform: scale(0);
  opacity: 0;
  animation: dot-pop .6s cubic-bezier(.34, 1.56, .64, 1) forwards;
  animation-delay: var(--d);
}
@keyframes dot-pop {
  to { transform: none; opacity: 1; }
}

.hero .eyebrow,
.hero .pill {
  opacity: 0;
  animation: eyebrow-in 1.1s cubic-bezier(.2, .8, .2, 1) forwards;
  animation-delay: 1s;
}
@keyframes eyebrow-in {
  from { opacity: 0; letter-spacing: .5em; }
  to   { opacity: 1; }
}

.hero .hero-lede-anim,
.hero .hero-cta-anim {
  opacity: 0;
  transform: translateY(12px);
  animation: fade-up .9s cubic-bezier(.2, .8, .2, 1) forwards;
}
.hero .hero-lede-anim { animation-delay: 1.85s; }
.hero .hero-cta-anim  { animation-delay: 2s; }
@keyframes fade-up {
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .hero .w > span,
  .hero .dot,
  .hero .eyebrow,
  .hero .pill,
  .hero .hero-lede-anim,
  .hero .hero-cta-anim {
    animation: none !important;
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
  }
}


/* ════════════════════════════════════════════════════════════════════
   SHINY TEXT  ·  the glow inside the letters, not over them
   Two things were making it read as something passing across the
   words rather than the words lighting up.

   NO CORE. The overlay was `color: transparent` — all halo, no
   letter. A blurred shape with nothing solid at its centre has no
   letterform to belong to, so it reads as a blob that happens to be
   word-shaped. The overlay now paints the glyphs in the label's own
   colour, so the glow has an edge to sit on.

   AND THE HALO WAS 20px. At that radius the bloom is wider than the
   strokes it comes from — it clears the letter entirely and becomes
   its own object floating above. 4px and 9px hug the stroke: the
   glow stops being a cloud and becomes the letter being bright.

   The band is narrower too — 12% of the mask instead of 20% — so it
   lights two or three letters at a time rather than half the word,
   which is what makes a highlight travel rather than wash.

   The label underneath is still never repainted. The overlay only
   ever adds.
   ════════════════════════════════════════════════════════════════════ */
.btn .shine::after,
.btn-outline .shine::after {
  color: inherit;                 /* the letter, not just its halo */
  text-shadow:
    0 0 4px rgba(255, 255, 255, .95),
    0 0 9px rgba(255, 255, 255, .55);

  -webkit-mask-image: linear-gradient(120deg,
    transparent 0%, transparent 44%, #000 50%, transparent 56%, transparent 100%);
          mask-image: linear-gradient(120deg,
    transparent 0%, transparent 44%, #000 50%, transparent 56%, transparent 100%);
}

/* the dark label on the light pill keeps its own glow colour */
.btn-outline .shine::after {
  text-shadow:
    0 0 4px rgba(45, 77, 224, .70),
    0 0 9px rgba(45, 77, 224, .35);
}


/* ════════════════════════════════════════════════════════════════════
   FAQ  ·  the left column holds while the questions run past it
   One declaration, no script, no scroll-jacking: the head is already
   a grid item in a two-column container whose other column is far
   taller, so position:sticky is the whole feature. It pins when it
   reaches the bar, stays while the questions scroll, and releases on
   its own when the container ends — which is exactly "then it moves
   to the next section", without anything having to decide when.

   Why not a pinned wrapper like How it works had: nothing here needs
   to be driven. The questions are the scroll. Holding the page still
   to move them would be doing by hand what the browser does for free.

   align-self: start matters. The container is align-items: start, so
   this is already the case — it is repeated because a sticky item
   that stretches to the row height has no room to move inside it and
   silently does nothing.

   top is the measured bar plus 28. Below 900 the container collapses
   to one column, the head sits above the questions rather than beside
   them, and sticking it would pin the word "FAQ" over the answers —
   so it only applies where there are two columns.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #faqs .faq-head {
    position: sticky;
    top: calc(var(--nav-h, 91px) + 28px);
    align-self: start;
  }
}


/* ════════════════════════════════════════════════════════════════════
   NAV  ·  Login, before Book a demo
   A stroke button, the same treatment as "Get early access" in the
   hero: white edge and white label while the bar is over the dark
   dome, navy once the bar turns light. Everything over the dome hangs
   off :not(.is-stuck), like the rest of the bar, so the handover is
   free.

   THE BAR IS A THREE-COLUMN GRID NOW. It was flex with space-between,
   which only centres the links while the wordmark and the button on
   either side happen to be the same width — they were, nearly, until
   Login made the right side 110px wider and pushed the links left.
   1fr / auto / 1fr keeps them on the centre line whatever sits either
   side. minmax(max-content, …) so a side column can never be squeezed
   narrower than its contents and slide under the links.

   AND IT IS TIGHTER BETWEEN 901 AND 1180. The bar needed about 990px
   before and needs about 1100px with Login; below that, main's
   overflow:clip would cut Book a demo off the right edge. Trimming
   the link, button and bar padding recovers about 130px.
   ════════════════════════════════════════════════════════════════════ */
.nav-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.btn-login {
  background: transparent;
  border: 1px solid rgba(10, 15, 46, .22);
  color: #0A0F2E;
  box-shadow: none;
}
.btn-login:hover {
  background: transparent;
  border-color: rgba(10, 15, 46, .45);
  color: #0A0F2E;
}

/* over the dome */
.nav:not(.is-stuck) .btn-login {
  border-color: rgba(255, 255, 255, .55);
  color: #FFFFFF;
}
.nav:not(.is-stuck) .btn-login:hover {
  border-color: rgba(255, 255, 255, .85);
  color: #FFFFFF;
}

/* On a light ground the label is navy, and a white glow on navy
   letters reads as the word breaking up — same fix as .btn-outline. */
.nav.is-stuck .btn-login .shine::after,
.nav-drawer .btn-login .shine::after {
  text-shadow:
    0 0 4px rgba(45, 77, 224, .70),
    0 0 9px rgba(45, 77, 224, .35);
}

@media (min-width: 901px) {
  .nav {
    display: grid;
    grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr);
    align-items: center;
  }
  .nav .brand       { justify-self: start; }
  .nav .nav-links   { justify-self: center; }
  .nav .nav-actions { justify-self: end; }
}

@media (min-width: 901px) and (max-width: 1180px) {
  .nav      { padding-left: 28px; padding-right: 28px; }
  .nav-link { padding-left: 10px; padding-right: 10px; }
  .nav .btn { padding-left: 20px; padding-right: 20px; }
}

/* Phones: the bar keeps just Book a demo, as before. Login moves into
   the menu sheet, under the links. */
@media (max-width: 900px) {
  .nav-actions .btn-login { display: none; }
  .nav-drawer .btn + .btn { margin-top: 10px; }
}


/* ── NAV · Login, the same size as Book a demo, with a real edge ───────
   SAME SIZE. The two already shared .btn's padding and a 1px border,
   so they matched in height; they differed in width only because
   "Login" is a shorter word. The pair is now a two-column grid with
   equal columns, so each is exactly as wide as the wider of them —
   Book a demo — and stays that way if either label changes.

   A VISIBLE STROKE. 55% white read as a faint outline rather than a
   button. 75% holds its edge over the dome without competing with the
   filled button beside it. Still 1px, so the heights stay identical.

   White is for while the bar sits on the dark dome. Once the page
   scrolls the bar turns light (rgba(245,246,250,.82)) and the wordmark
   and links go dark with it — a white label there would be white on
   near-white, so Login follows them to navy, as before. */
.nav-actions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 12px;
}
.nav-actions .btn { width: 100%; }

.nav:not(.is-stuck) .btn-login {
  border-color: rgba(255, 255, 255, .75);
  color: #FFFFFF;
}
.nav:not(.is-stuck) .btn-login:hover {
  border-color: #FFFFFF;
  background: rgba(255, 255, 255, .08);
  color: #FFFFFF;
}

@media (max-width: 900px) {
  .nav-actions { display: flex; }
  .nav-actions .btn { width: auto; }
}



/* ── HERO · the lede at 18px, on two lines ─────────────────────
   The copy is 194 characters. At 18px in DM Sans a character averages
   8.5 to 9.5px, so the whole line is 1,650 to 1,840px and two lines
   need up to 920px each. 960px carries that with room to spare at
   either end of the range, and balance evens the two lines.

   Two lines needs a window at least that wide. Below 900px it cannot
   be two lines at any readable size, so phones keep a smaller 16px
   and wrap as they need to. */
.hero .lede {
  font-size: 18px;
  max-width: min(960px, 100%);
  text-wrap: balance;
}
@media (max-width: 900px) {
  .hero .lede { font-size: 16px; }
}


/* ═══════════════════════════════════════════════════════════════════
   WHO BUILT THIS · back to the page ground
   The #070A1F fill comes off and the section is transparent again, as
   every other section here is. Everything that was changed to sit on
   near-black goes back with it:

     · the cubes drop the brightness(0) invert(1) that made them white
       and are their own blue again, at the 4% they were before
     · the wave crest goes back to its lighter values — on the page
       ground a 18% crest would read as a smear rather than a pass
     · the type returns to the light-theme colours: the heading to
       #1B1F7A like every other heading, the paragraph to --text-mid,
       the eyebrow to #1B2A9E
   ═══════════════════════════════════════════════════════════════════ */
#about { background: transparent !important; }

.about-grid::before,
.about-grid::after { filter: none; }

.about-grid::before { opacity: .04; }

@keyframes about-wave {
  0%       { -webkit-mask-size: 2400px 2400px; mask-size: 2400px 2400px;
             opacity: 0;   animation-timing-function: linear; }
  7%       { opacity: .14; animation-timing-function: linear; }
  53.85%   { -webkit-mask-size: 620px 620px;   mask-size: 620px 620px;
             opacity: .14;
             animation-timing-function: cubic-bezier(.14, .86, .28, 1); }
  92%      { opacity: .09; }
  100%     { -webkit-mask-size: 0px 0px;       mask-size: 0px 0px;
             opacity: 0; }
}

#about .about-title { color: #1B1F7A; }
#about .about-body  { color: var(--text-mid); opacity: 1; }
#about .pill        { color: #1B2A9E; }


/* ═══════════════════════════════════════════════════════════════════
   BOOK A DEMO CARD · back to the page ground
   Same again: the near-black fill and the border that went with it come
   off, the type returns to the light-theme colours, and the CTA drops
   the white glow — it was there to give a white-on-dark button an edge
   and does nothing on a light card.

   The card's own glow stays off, as asked earlier; only the drop
   shadow under it remains, so it still sits above the page.
   ═══════════════════════════════════════════════════════════════════ */
#demo .glass.final {
  background: rgba(255, 250, 250, .82) !important;
  border: 0;
}

#demo .glass.final,
#demo .glass.final .final-title,
#demo .glass.final h2,
#demo .glass.final h3,
#demo .glass.final h4,
#demo .glass.final p,
#demo .glass.final a,
#demo .glass.final li,
#demo .glass.final label,
#demo .glass.final span { color: inherit; }

#demo .glass.final .final-title { color: #1B1F7A; }
#demo .glass.final .final-sub   { color: var(--text-mid); }
#demo .glass.final .final-note  { color: var(--text-low); }
#demo .glass.final .final-dim,
#demo .glass.final .dim {
  color: inherit;
  font-style: normal;
  opacity: .5;
}

#demo .glass.final .btn-primary {
  background: #1B2A9E;
  color: #FFFFFF;
  border-color: transparent;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .18),
    0 12px 28px -12px rgba(27, 42, 158, .70);
}
#demo .glass.final .btn-primary:hover {
  background: #2334BC;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .24),
    0 16px 34px -14px rgba(27, 42, 158, .82);
}
#demo .glass.final .btn-primary:focus-visible {
  outline: 2px solid #1B2A9E;
  outline-offset: 3px;
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARD · smoother
   Same four beats — rise, hold, leave, wait — with the sharp edges
   taken off each of them.

   SLOWER, SO NOTHING HAS TO HURRY. 6000ms instead of 4800, and the
   stagger with it (a quarter of the cycle, or the four cards stop
   taking even turns). The rise gets 720ms rather than 600, the leave
   900ms rather than 624.

   GENTLER CURVES. The rise was .12/.82 — nearly all of its travel in
   the first third, which is emphatic rather than smooth. .22/.85 still
   decelerates into the stop but reaches it evenly. The leave was a
   .55/0/.85 ease-in that snapped away at the end; .40/0/.60/.35 lets
   it go rather than throwing it.

   SHORTER TRAVEL, SOFTER SCALE. 34px to 26px on the way in and 30px to
   22px on the way out, with the scale change halved. Less distance
   over more time is most of what reads as smooth.

   THE CROSS-OVER IS LONGER. Leaving now starts at 25% and ends at 40%,
   while the next card rises over the first 12% of its own cycle — so
   the two overlap for the whole of the outgoing card's exit instead of
   meeting at a single frame.
   ═══════════════════════════════════════════════════════════════════ */
.sm-stack .sm-tile,
.sm-stack .sm-tile img {
  animation-duration: 6000ms;
  animation-delay: calc(var(--i) * -1500ms);
}

@keyframes sm-carousel {
  /* rising */
  0%      { transform: translateY(26px) scale(.982); opacity: 0; z-index: 4;
            animation-timing-function: cubic-bezier(.22, .85, .32, 1); }
  /* arrived */
  12%     { transform: translateY(0)    scale(1);    opacity: 1; z-index: 4;
            animation-timing-function: linear; }
  /* held */
  25%     { transform: translateY(0)    scale(1);    opacity: 1; z-index: 2;
            animation-timing-function: cubic-bezier(.40, 0, .60, .35); }
  /* going */
  40%     { transform: translateY(-22px) scale(.97); opacity: 0; z-index: 2;
            animation-timing-function: step-end; }
  /* below again, unseen, waiting its turn */
  40.01%,
  100%    { transform: translateY(26px) scale(.982); opacity: 0; z-index: 4; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARDS · an automatic swiper
   The stack becomes a track. Instead of one card at a time rising,
   holding and fading out in place, all four sit in a row and the row
   slides sideways, one card every 3.5s.

   WHY IT WAS NEVER GOING TO READ AS SMOOTH BEFORE. A card that fades
   out where it stands and another that fades in behind it has no
   continuous motion between the two — there is a moment where neither
   is solid, and no amount of easing removes it. A track that slides
   has exactly one moving thing, always visible, always travelling in
   one direction. That is what a swiper is.

   HOW THE LOOP CLOSES. The track carries five cards, not four: the
   markup ends with a second copy of the first. The slide runs 0 to
   -80%, which lands on that copy, and the animation then restarts at
   0 — the real first card, in the identical position. The reset
   happens on a frame where the two are the same picture, so there is
   nothing to see.

   TIMING. 14s for the cycle: four moves of 900ms with 2.6s to read
   each card between them. The stops are at 0 / 25 / 50 / 75 / 100% of
   the cycle; the hold keyframes carry `linear` and the moving ones an
   ease-in-out, so each card settles rather than arriving flat.

   The per-card animations are switched off — the tiles no longer
   animate at all, the track does.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-card { overflow: hidden; }

.ics-orb-wrap .sm-stack {
  display: flex;
  width: 500%;
  height: 100%;
  animation: sm-swipe 14s infinite;
  will-change: transform;
}

.ics-orb-wrap .sm-stack .sm-tile {
  position: static;
  flex: 0 0 20%;
  width: 20%;
  height: 100%;
  margin: 0;
  box-sizing: border-box;
  opacity: 1;
  transform: none;
  animation: none;
}
.ics-orb-wrap .sm-stack .sm-tile img { animation: none; opacity: 1; }

/* Four moves, five stops. Holds are linear because nothing moves in
   them; the moves ease in and out of their stop. */
@keyframes sm-swipe {
  0%       { transform: translateX(0);    animation-timing-function: linear; }
  18.571%  { transform: translateX(0);    animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
  25%      { transform: translateX(-20%); animation-timing-function: linear; }
  43.571%  { transform: translateX(-20%); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
  50%      { transform: translateX(-40%); animation-timing-function: linear; }
  68.571%  { transform: translateX(-40%); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
  75%      { transform: translateX(-60%); animation-timing-function: linear; }
  93.571%  { transform: translateX(-60%); animation-timing-function: cubic-bezier(.45, 0, .2, 1); }
  100%     { transform: translateX(-80%); }
}

/* stop while it is being read */
.ics-orb-wrap .sm-metrics:hover .sm-stack { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .ics-orb-wrap .sm-stack { animation: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARDS · the stack again, swiping downward
   The row comes out: the track layout, the 500% width and the loop
   copy are all undone, and the tiles go back to sitting one on top of
   another, one visible at a time. That is the set that was there
   before the horizontal version.

   WHAT IS NEW IS THE DIRECTION. Every card now travels the same way —
   downward — and it never stops travelling:

     it enters from above the frame, settles into place,
     is held there to be read,
     then carries on down and out of the frame,

   and the next card starts its own entrance while the one before it is
   still leaving. So at every moment there is a card moving downward
   through the frame, which is what makes it read as a swipe rather
   than as one card replacing another.

   Before, a card rose UP into place and then left UP as well, through
   the space the next card was arriving into. Two cards crossing in
   opposite directions is the thing that was not smooth.

   TIMING, over 6s with four cards a quarter-cycle apart:
     0 - 720ms      enters from 26px above, decelerating into place
     720 - 1500ms   held still
     1500 - 2400ms  leaves downward, easing out of the hold
     then           waits above the frame, invisible, for its turn

   The overlap falls out of the stagger: the next card enters between
   1500ms and 2220ms, which is almost exactly the window the one before
   it spends leaving.
   ═══════════════════════════════════════════════════════════════════ */

/* the row, undone */
.ics-orb-wrap .sm-card  { overflow: visible; }
.ics-orb-wrap .sm-stack {
  display: block;
  position: relative;
  width: auto;
  animation: none;
}

.ics-orb-wrap .sm-stack .sm-tile {
  position: absolute;
  inset: 0;
  width: auto;
  height: 100%;
  transform-origin: 50% 50%;

  animation-name: sm-carousel;
  animation-duration: 6000ms;
  animation-delay: calc(var(--i) * -1500ms);
  animation-iteration-count: infinite;
  animation-timing-function: linear;   /* each beat sets its own */
  animation-fill-mode: none;
}

@keyframes sm-carousel {
  /* above the frame, coming down */
  0%      { transform: translateY(-26px) scale(.985); opacity: 0; z-index: 4;
            animation-timing-function: cubic-bezier(.22, .85, .32, 1); }
  /* in place */
  12%     { transform: translateY(0)     scale(1);    opacity: 1; z-index: 4;
            animation-timing-function: linear; }
  /* held */
  25%     { transform: translateY(0)     scale(1);    opacity: 1; z-index: 2;
            animation-timing-function: cubic-bezier(.36, 0, .5, 1); }
  /* on down, and out */
  40%     { transform: translateY(26px)  scale(.985); opacity: 0; z-index: 2;
            animation-timing-function: step-end; }
  /* back above the frame, unseen, waiting */
  40.01%,
  100%    { transform: translateY(-26px) scale(.985); opacity: 0; z-index: 4; }
}

.ics-orb-wrap .sm-metrics:hover .sm-stack .sm-tile { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .ics-orb-wrap .sm-stack .sm-tile { animation: none; }
}


/* ── ICS METRIC CARDS · one card at a time ────────────────────
   The overlap was arithmetic, not easing. Four cards a quarter-cycle
   apart give each one a 25% slot; the keyframes gave each one 40% of
   visible time, so every card was still leaving when the next arrived
   — by 15% of the cycle, which at 6s was 900ms of two cards on screen.

   Each card now fits inside its own quarter exactly: it enters, is
   held, and is gone by the time the next one begins. Nothing overlaps
   and nothing is missing either — the next card starts on the frame
   the last one finishes.

   The cycle goes to 8s so that quarter is 2s of real time, which is
   enough for an unhurried move at both ends:

     0 - 500ms      enters from 26px above, decelerating into place
     500 - 1500ms   held still, to be read
     1500 - 2000ms  leaves downward
     then           parked above the frame, invisible, until its turn
   ──────────────────────────────────────────────────────────────── */
.ics-orb-wrap .sm-stack .sm-tile {
  animation-duration: 8000ms;
  animation-delay: calc(var(--i) * -2000ms);
}

@keyframes sm-carousel {
  /* above the frame */
  0%      { transform: translateY(-26px) scale(.985); opacity: 0;
            animation-timing-function: cubic-bezier(.22, .85, .32, 1); }
  /* in place */
  6.25%   { transform: translateY(0)     scale(1);    opacity: 1;
            animation-timing-function: linear; }
  /* held */
  18.75%  { transform: translateY(0)     scale(1);    opacity: 1;
            animation-timing-function: cubic-bezier(.36, 0, .5, 1); }
  /* down and out, on the frame the next card starts */
  25%     { transform: translateY(26px)  scale(.985); opacity: 0;
            animation-timing-function: step-end; }
  /* parked above, waiting */
  25.01%,
  100%    { transform: translateY(-26px) scale(.985); opacity: 0; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARDS · the next ones waiting behind
   Same downward travel, but a card no longer appears out of nothing:
   it joins the back of a deck, moves forward a place at a time, comes
   to the front to be read, and then carries on down and out.

   THREE CARDS ARE ON SCREEN at any moment — the one being read and the
   two queued behind it, each smaller, fainter and a little higher than
   the one in front. That is the whole reason to do it this way: the
   section shows that more is coming rather than implying the card is
   the only one.

   WHY THIS DOES NOT BRING BACK THE OVERLAP. The overlap that was wrong
   was two cards in the SAME place at the same time, each half faded.
   Here every card has its own place — back, middle, front — and only
   one is ever at the front. They are layered, not crossing.

   EACH CARD'S 8s, and what it is doing:
     0 - 240ms       fades in at the back of the deck
     240 - 2000ms    waits at the back
     2000 - 2500ms   forward to the middle, brightening
     2500 - 4000ms   waits in the middle
     4000 - 4500ms   forward to the front, to full size and opacity
     4500 - 5500ms   held at the front, to be read
     5500 - 6000ms   leaves downward
     6000 - 8000ms   parked, invisible, until its turn comes round

   The four cards are 2s apart, so as one leaves the next is arriving
   at the front, and a new one is fading in at the back. The stack
   positions and z-index keep the order; nothing is ever mid-fade in
   the same spot as something else.
   ═══════════════════════════════════════════════════════════════════ */
.ics-orb-wrap .sm-stack .sm-tile {
  animation-duration: 8000ms;
  animation-delay: calc(var(--i) * -2000ms);
}

@keyframes sm-carousel {
  /* joining the back of the deck */
  0%       { transform: translateY(-34px) scale(.90); opacity: 0;   z-index: 2;
             animation-timing-function: linear; }
  3%       { transform: translateY(-34px) scale(.90); opacity: .22; z-index: 2;
             animation-timing-function: linear; }
  /* waiting at the back */
  25%      { transform: translateY(-34px) scale(.90); opacity: .22; z-index: 2;
             animation-timing-function: cubic-bezier(.36, 0, .3, 1); }
  /* forward to the middle */
  31.25%   { transform: translateY(-18px) scale(.95); opacity: .5;  z-index: 3;
             animation-timing-function: linear; }
  /* waiting in the middle */
  50%      { transform: translateY(-18px) scale(.95); opacity: .5;  z-index: 3;
             animation-timing-function: cubic-bezier(.22, .85, .32, 1); }
  /* forward to the front */
  56.25%   { transform: translateY(0)     scale(1);   opacity: 1;   z-index: 4;
             animation-timing-function: linear; }
  /* held, to be read */
  68.75%   { transform: translateY(0)     scale(1);   opacity: 1;   z-index: 4;
             animation-timing-function: cubic-bezier(.36, 0, .5, 1); }
  /* down and out */
  75%      { transform: translateY(26px)  scale(.985); opacity: 0;  z-index: 4;
             animation-timing-function: step-end; }
  /* parked at the back, unseen, waiting */
  75.01%,
  100%     { transform: translateY(-34px) scale(.90); opacity: 0;   z-index: 2; }
}


/* ════════════════════════════════════════════════════════════════════
   ONE RAMP, NAMED  ·  tokens for the hero gradient
   The hero paints fifteen stops. Tokenising six of them and rebuilding
   it from those would move twelve colours, so all fifteen are named
   here and the hero is re-declared below referencing them. Its
   computed value is byte for byte what it was.

   The six readable aliases are what the ICS chapter is built from, so
   the two sections are provably the same ramp rather than two ramps
   that happen to look alike.
   ════════════════════════════════════════════════════════════════════ */
:root {
  --page-bg: #F5F6FA;
  --page-rgb: 245, 246, 250;

  --h01: #070A1F;  --h02: #0A0F2E;  --h03: #0D1440;
  --h04: #121C5C;  --h05: #19297C;  --h06: #22389F;
  --h07: #2A46C8;  --h08: #2D4DE0;  --h09: #4863E5;
  --h10: #6E84EB;  --h11: #98A8F1;  --h12: #BEC9F7;
  --h13: #DAE1FA;  --h14: #EEF1FD;  --h15: #F5F6FA;

  --grad-navy:  var(--h01);
  --grad-deep:  var(--h04);
  --grad-blue:  var(--h08);
  --grad-mid:   var(--h11);
  --grad-light: var(--h13);

  /* how far each arc bites into the section. The hero has no geometric
     arc to match, so this is the depth both ICS rims are cut to. */
  --arc: clamp(56px, 9vw, 140px);
}

.hero {
  background: radial-gradient(170% 116% at 50% 0%,
    var(--h01) 0%,    var(--h02) 26%, var(--h03) 38%,   var(--h04) 46%,
    var(--h05) 52%,   var(--h06) 57%, var(--h07) 61%,   var(--h08) 64%,
    var(--h09) 68%,   var(--h10) 72%, var(--h11) 76.5%, var(--h12) 81%,
    var(--h13) 86%,   var(--h14) 91%, var(--h15) 100%) !important;
}


/* ════════════════════════════════════════════════════════════════════
   ICS  ·  the dark chapter, cut on two arcs
   Both rims are convex. border-radius: 50% / var(--arc) rounds all
   four corners away, which leaves the centre at full height: the top
   edge domes upward and the bottom edge bulges downward. One
   pseudo-element, no SVG, no extra markup.

   The gradient is symmetrical. It starts on the ramp's lightest blue
   at the top rim, passes through the electric blue, reaches navy by a
   third of the way down, holds navy across the middle where all the
   content sits, then returns the same way. Outside the arcs nothing is
   painted, so the page ground shows and the curves read as curves.

   KNOWN TRADE. #lines, #field and #cube3d are fixed at z-index 0 and
   <main> is at 1, so anything painted inside a section paints over all
   three. The travelling cube is hidden for the length of this section.
   That is accepted here: ICS carries its own cube diagram and its own
   dot grid, so it reads as a deliberate dark chapter.

   Padding is the arc depth plus 96px at both ends, so the curve never
   reaches the copy at any width.
   ════════════════════════════════════════════════════════════════════ */
#ics {
  padding-top: calc(var(--arc) + 96px);
  padding-bottom: calc(var(--arc) + 96px);
}

/* The ground is NOT painted on the section. #lines, #field and #cube3d
   are fixed at z-index 0 and <main> is at 1, so a background inside ICS
   sits in a stacking context entirely above the cube and hides it for
   the length of the section. Out at body level on -1 the order is:
   page, ground, cube, content, and the cube flies across it. This is
   the same arrangement .mk-bg already uses.

   Top and height are written by the placement script, because a
   body-level layer cannot know where its section starts. */
#ics::before { content: none; }

.ics-bg {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;             /* both written by the placement script */
  height: 0;
  z-index: -1;
  pointer-events: none;

  background:
    radial-gradient(ellipse 88% calc(var(--arc) * 2) at 50% 0%,
      rgb(var(--page-rgb))        0%,
      rgba(var(--page-rgb), .94) 34%,
      rgba(var(--page-rgb), .58) 64%,
      rgba(var(--page-rgb), .20) 84%,
      rgba(var(--page-rgb), 0)  100%),

    radial-gradient(ellipse 88% calc(var(--arc) * 2) at 50% 100%,
      rgb(var(--page-rgb))        0%,
      rgba(var(--page-rgb), .94) 34%,
      rgba(var(--page-rgb), .58) 64%,
      rgba(var(--page-rgb), .20) 84%,
      rgba(var(--page-rgb), 0)  100%),

    /* Both ends of the ramp land on the page colour, not on the
       lightest blue. That is what closes the corners: the ellipses
       reach full strength only in the middle of each rim, so without
       this the corners kept a blue tint and met the next section on a
       line. Now every pixel of both edges is already page colour. */
    linear-gradient(180deg,
      var(--page-bg)      0%,
      var(--grad-light)   5%,
      var(--grad-mid)    11%,
      var(--grad-blue)   19%,
      var(--grad-deep)   27%,
      var(--grad-navy)   34%,
      var(--grad-navy)   66%,
      var(--grad-deep)   73%,
      var(--grad-blue)   81%,
      var(--grad-mid)    89%,
      var(--grad-light)  95%,
      var(--page-bg)    100%);
}

/* Colour only. Every element below sat at navy on near-white and now
   sits on navy, so each one is lifted and nothing else about it moves.
   White on #070A1F is 19.4:1; the lavender is 12.3:1. */
#ics .section-title      { color: #FFFFFF; }
#ics .section-title .dim { color: #C7D0F5; }
#ics .section-sub        { color: #C7D0F5; }
#ics .ics-cube-label     { color: #FFFFFF; }
#ics .signal             { color: #FFFFFF; }

/* The pill keeps the page-wide plate, the indigo tint and its edge,
   with the label in white. */
#ics .section-head .pill { color: #FFFFFF; }

/* .ics-source and .sm-tile are NOT lifted. Both carry their own pale
   card gradient, so their copy is already dark on light and stays
   that way; turning it white would have hidden it on its own card. */


/* ════════════════════════════════════════════════════════════════════
   THE GROUND MOVES TO THE END OF THE PAGE
   .mk-bg is unchanged in every respect but where it is put: the
   placement script now spans it from the top of the demo section to
   the bottom of the footer instead of over the marketing suite. The
   marketing suite is left on the page ground, which is what the
   section itself has always been set to.

   The footer dot grid was drawn in the asset's own blue at 7%, legible
   only because it sat on near-white. On the dome's dark end it would
   vanish, so it is filtered white here, the same treatment the about
   grid already uses, and lifted to carry on the darker ground.
   ════════════════════════════════════════════════════════════════════ */
.footer-grid-art {
  filter: grayscale(1) brightness(0) invert(1);
  opacity: .10;
}


/* ── FOOTER · lifted onto the dome ───────────────────────────
   The footer used to sit on the page's near-white, so its copy was
   navy and its glass was a 28% white wash. It now sits on the dark end
   of the ground that moved here, where navy on blue is unreadable.

   The panel takes the dark side of the same ramp instead of the light
   one, which gives every line in it a single consistent ground rather
   than a wash that runs from pale at the top to deep at the bottom.
   Only colours change: no size, spacing or position is touched.
   ════════════════════════════════════════════════════════════════════ */
.footer-shell .glass.footer {
  background: rgba(7, 10, 31, .46) !important;
  border-color: rgba(255, 255, 255, .14);
}

.footer-shell .footer-col h3,
.footer-shell .footer-brand p { color: #FFFFFF; }

.footer-shell .footer-col a   { color: rgba(255, 255, 255, .72); }
.footer-shell .footer-col a:hover { color: #FFFFFF; }

.footer-shell .footer-base,
.footer-shell .footer-base a  { color: rgba(255, 255, 255, .58); }

/* the wordmark is a dark PNG, so it is inverted rather than replaced */
.footer-shell .footer-logo { filter: brightness(0) invert(1); }


/* ═══════════════════════════════════════════════════════════════════
   ICS METRIC CARDS · a live stack that arrives once
   The looping carousel is replaced by a single arrival. Three cards
   drop in one after another and pile up, so the section reads as a
   feed of decisions that has just come in rather than a slideshow
   that will come round again.

   THE RESTING STATE IS THE CSS, NOT THE SCRIPT. Every tile's place in
   the pile is derived from --d, its depth, which is worked out from
   the --i already in the markup. With no JavaScript at all the stack
   renders settled and correct: the last card in front, the other two
   peeking out behind it. The script only takes that finished state
   apart at load and then puts it back together on screen.

   ONE LEVEL BACK is 14px down, 6% smaller and 35% fainter, so two
   levels back is 28px, 12% and 70%. transform-origin is top center,
   which is what makes the back cards peek out below the front one
   rather than growing out of its middle.

   Only transform and opacity move. Nothing here touches top, margin
   or height, so the whole sequence stays on the compositor.
   ═══════════════════════════════════════════════════════════════════ */
#ics .sm-stack {
  --deck-step: 14px;

  /* The page's own ease-out, not a second one invented for this card.
     --reveal-ease is what all 24 reveals already run on, so the stack
     decelerates exactly the way every other entrance does, and if that
     curve is ever retuned this follows it. */
  --deck-ease: var(--reveal-ease);
}

#ics .sm-stack .sm-tile {
  /* the looping carousel is off. This is a single arrival now. */
  animation: none;

  transform-origin: top center;

  /* Opacity and travel are deliberately NOT the same length. A card
     that fades in across its whole drop is semi-transparent for most
     of it, and since the card behind sits only 14px away with its text
     in the same places, the two read as one doubled card. That is the
     glitch. Resolving the fade in 220ms makes the card solid almost at
     once, so what travels the remaining distance is an opaque card
     landing on a pile, which is the intended motion. */
  --t-move: 600ms;
  --t-fade: 220ms;

  transition: transform var(--t-move) var(--deck-ease),
              opacity   var(--t-fade) var(--deck-ease);

  /* depth: the last of the three is the front card, at 0 */
  --d: calc(2 - var(--i));

  transform: translateY(calc(var(--d) * var(--deck-step)))
             scale(calc(1 - var(--d) * .06));
  opacity: calc(1 - var(--d) * .35);
  z-index: calc(10 - var(--d));
}

/* Above the pile, invisible, waiting its turn. The script puts every
   tile here at load and lifts them off one at a time. */
#ics .sm-stack .sm-tile.is-waiting {
  transform: translateY(-40px) scale(1);
  opacity: 0;
}

/* The fourth card is parked. The stack is three deep, and a fourth
   would compute to -0.05 opacity, so it would never be seen anyway.
   Its markup is untouched, so putting it back is one line. */
#ics .sm-stack .sm-tile:nth-child(4) { display: none; }

@media (max-width: 900px) {
  /* a shorter step, so the pile does not crowd the ICS label below it */
  #ics .sm-stack { --deck-step: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  #ics .sm-stack .sm-tile { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   THE PROBLEM · second pass
   A two column statement: the claim on the left, the puzzle cube on
   the right. The first pass, the orbiting role cards and their old
   heading, is hidden rather than deleted, so it can be restored by
   removing the one rule below.

   The image carries two separate movements. Its arrival is the page's
   own .reveal, registered in the observer's group list so it fades and
   rises exactly like every other block and inherits the 117ms stagger
   against the copy beside it. On top of that it breathes: a slow rise
   and fall with a fraction of a degree of tilt, long enough at 9s that
   it reads as weight rather than as a loop. The float is on the image
   and the reveal on its wrapper, so the two transforms never fight.
   ═══════════════════════════════════════════════════════════════════ */
/* The first pass is shown again: the rule that hid .section-head and
   .problem-grid is removed, so the old heading, the orbit and the three
   role cards are back alongside the second pass above them. */

#problem .problem-intro {
  display: grid;
  grid-template-columns: 1.05fr .95fr;
  gap: 56px;
  align-items: center;
}

#problem .pi-copy .pill { margin-bottom: 22px; }

#problem .pi-copy .section-title {
  max-width: 14ch;
  margin: 0 0 20px;
}

#problem .pi-copy .section-sub {
  max-width: 46ch;
  margin: 0;
}

#problem .pi-art {
  display: flex;
  justify-content: center;
}

#problem .pi-img {
  display: block;
  width: 100%;
  max-width: 560px;
  height: auto;
  animation: pi-float 9s ease-in-out infinite;
  will-change: transform;
}

@keyframes pi-float {
  0%, 100% { transform: translateY(0)     rotate(0deg); }
  50%      { transform: translateY(-14px) rotate(-.6deg); }
}

@media (max-width: 900px) {
  #problem .problem-intro {
    grid-template-columns: 1fr;
    gap: 32px;
  }
  #problem .pi-copy .section-title { max-width: none; }
  #problem .pi-copy .section-sub   { max-width: none; }
  #problem .pi-img { max-width: 420px; }
}

@media (prefers-reduced-motion: reduce) {
  #problem .pi-img { animation: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   HERO CTAs · a different entrance
   The row used to fade up as one block, 12px, 900ms. Both buttons
   arrived on the same frame, which after a headline that lands word by
   word reads as the animation giving up at the last step.

   Now each button arrives on its own, 140ms apart, and does three
   things at once: rises 14px, grows from 94%, and comes out of a 6px
   blur. The blur is what makes it read as focus pulling rather than a
   panel sliding, and it is the part that is genuinely different from
   every other entrance on the page.

   700ms on --reveal-ease, so it sits inside the same timing system as
   the reveals and the ICS stack.

   BACKWARDS, NOT FORWARDS. With forwards the animation keeps ownership
   of transform after it ends, and the button's hover would have
   nothing to move. Declaring the start state in a `from` keyframe and
   filling backwards means the element is handed back to its ordinary
   CSS the moment it lands.

   The shine sweep on the label is untouched.
   ═══════════════════════════════════════════════════════════════════ */
.hero .hero-cta-anim {
  animation: none;
  opacity: 1;
  transform: none;
}

.hero .hero-cta-anim .btn {
  animation: cta-in 700ms var(--reveal-ease) backwards;
}

.hero .hero-cta-anim .btn:nth-of-type(1) { animation-delay: 2s; }
.hero .hero-cta-anim .btn:nth-of-type(2) { animation-delay: 2.14s; }

@keyframes cta-in {
  from {
    opacity: 0;
    transform: translateY(14px) scale(.94);
    filter: blur(6px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .hero .hero-cta-anim .btn {
    animation: none !important;
    transform: none !important;
    filter: none !important;
    opacity: 1 !important;
  }
}



/* ═══════════════════════════════════════════════════════════════════
   THE PROBLEM · six systems drawing into one
   The illustration is still the single PNG. Each piece is a sprite
   window onto its own cube, so the picture comes apart with no second
   request and no new files. Every number below was measured off the
   image: the alpha channel was labelled into connected regions and
   each region's box read from it, so the pieces sit exactly where they
   sit in the artwork.

   THE MERGE IS ONE NUMBER. A scroll handler writes --p, 0 to 1, across
   the section. Everything else is calc: each piece travels its own
   --dx and --dy, in percentages of its own box so the motion holds at
   any width, shrinks as it goes, and fades out over the last quarter.
   Where they arrive, the pulse cube is waiting.

   --dx and --dy are the distance from the piece's own place to the
   middle, divided by the piece's own size. That is what makes them
   converge on one point rather than drift by a fixed amount.
   ═══════════════════════════════════════════════════════════════════ */
#problem .pi-art { display: block; }

#problem .pi-stage {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  aspect-ratio: 2100 / 1920;
  --p: 0;
}

#problem .pi-piece,
#problem .pi-core {
  position: absolute;
  background-image: url('problem-puzzle.png?v=2');
  background-repeat: no-repeat;
  pointer-events: none;
}

/* the faint wireframe at the middle: a window on a region none of the
   six cubes reach, so it shows the target on its own */
#problem .pi-core {
  left: 32.857%;  top: 29.167%;
  width: 34.286%; height: 41.667%;
  background-size: 291.667% 240%;
  background-position: 50% 50%;
  opacity: calc(1 - var(--p) * .7);
}

#problem .pi-piece {
  left:   calc(var(--x) * 1%);
  top:    calc(var(--y) * 1%);
  width:  calc(var(--w) * 1%);
  height: calc(var(--h) * 1%);
  background-size: var(--bs);
  background-position: var(--bp);

  transform:
    translate(calc(var(--dx) * var(--p) * 1%), calc(var(--dy) * var(--p) * 1%))
    scale(calc(1 - .34 * var(--p)));
  opacity: calc(1 - max(0, var(--p) - .55) * 4);
  will-change: transform, opacity;
}

/* measured boxes, and the trip each piece makes to the middle */
#problem .pi-piece:nth-child(2) { --x: 5.0;  --y: 61.5; --w: 21.7; --h: 25.4;
  --bs: 460.5% 393.4%; --bp: 6.33% 82.40%;  --dx:  157.4; --dy:  -95.3; }
#problem .pi-piece:nth-child(3) { --x: 72.8; --y: 19.2; --w: 21.7; --h: 25.4;
  --bs: 460.5% 393.4%; --bp: 92.94% 25.70%; --dx: -155.1; --dy:   71.3; }
#problem .pi-piece:nth-child(4) { --x: 5.3;  --y: 17.5; --w: 20.8; --h: 24.4;
  --bs: 481.7% 410.3%; --bp: 6.73% 23.14%;  --dx:  164.9; --dy:   83.2; }
#problem .pi-piece:nth-child(5) { --x: 75.0; --y: 61.7; --w: 20.0; --h: 23.3;
  --bs: 500.0% 428.6%; --bp: 93.81% 80.43%; --dx: -175.0; --dy: -100.2; }
#problem .pi-piece:nth-child(6) { --x: 40.0; --y: 2.3;  --w: 20.0; --h: 23.3;
  --bs: 500.0% 428.6%; --bp: 50.00% 2.99%;  --dx:    0.0; --dy:  154.7; }
#problem .pi-piece:nth-child(7) { --x: 40.6; --y: 76.0; --w: 19.0; --h: 22.5;
  --bs: 525.0% 444.4%; --bp: 50.12% 98.12%; --dx:   -0.5; --dy: -165.6; }

/* What they become. Not a drawn stand-in any more: this is the page's
   own dot cube, the same 27 tile c3-rig the ICS diagram carries, so
   what the six systems join into is literally the object that then
   travels down the page to ICS. It grows and fades up over the last
   third of the merge, as the sprites fade out around it. */
#problem .pi-rig {
  position: absolute;
  left: 50%; top: 50%;
  width: 200px; height: 200px;
  margin: -100px 0 0 -100px;
  transform: scale(calc(.62 + .38 * var(--p)));

  /* It arrives only after the six have gone: they are clear of the
     stage by .80 and this starts at .82, so the two never sit on top
     of each other. Past 1.05 it drops away again, because by then the
     travelling cube has taken over at the same spot and this copy
     would otherwise scroll off as a second cube. */
  opacity: calc(min(1, max(0, var(--p) - .82) * 5.55)
                  - max(0, var(--p) - 1.05) * 4);
  pointer-events: none;
}

@media (max-width: 900px) {
  #problem .pi-rig { width: 150px; height: 150px; margin: -75px 0 0 -75px; }
}

@media (max-width: 900px) {
  #problem .pi-stage { max-width: 420px; }
}

/* Nothing moves. The pieces sit where the artwork puts them and the
   cube they become is simply not shown. */
@media (prefers-reduced-motion: reduce) {
  #problem .pi-stage { --p: 0 !important; }
}



/* ── the travelling field and its lines, off ─────────────────────────
   The drifting tiles and the lines joining them washed across every
   section, the revenue suite included. Only the cube travels now: it
   sets off from the end of the problem section, once the six systems
   have joined into it, and carries on down to ICS on its own.

   #field and #lines are hidden rather than removed. The journey script
   still measures and drives them, so it is untouched and this is one
   rule to delete if they should come back.
   ════════════════════════════════════════════════════════════════════ */
#field,
#lines { display: none !important; }


/* ── ICS metric deck · narrow desktop ────────────────────────────────
   The deck is absolutely positioned 102px into the orb wrapper at a
   fixed 408px. That fits from about 1200px up. Below it, the wrapper
   has already been pushed left by the fluid container and the deck
   runs past the right edge of the window: measured at 1024 it reached
   1051px, which is what was widening the document and putting a
   horizontal scrollbar on the page.

   314px is the width the deck already carries elsewhere in this file,
   so this is the design's own narrow size rather than a new number. At
   1024 it now ends at 957px, inside the window with room to spare.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) and (max-width: 1199px) {
  .ics-orb-wrap .sm-metrics { width: 314px; }
}


/* ═══════════════════════════════════════════════════════════════════
   THE UNIVERSAL PIXEL
   A dark chapter between the problem and the revenue suite: four steps
   from the hotel's own site to one joined record.

   THE GROUND IS A BODY LEVEL LAYER, not a background on the section.
   #lines, #field and #cube3d are fixed at z-index 0 while <main> is at
   1, so painting inside the section would cover the travelling cube
   for the whole of it and break the journey the problem section hands
   off. At -1 the order is page, ground, cube, content, which is the
   same arrangement .ics-bg and .mk-bg already use.

   The cube in step four is the page's own 27 tile rig, not a drawing
   of one, so the object the four steps end on is the object the reader
   has been following down the page.
   ═══════════════════════════════════════════════════════════════════ */
.up-bg {
  position: absolute;
  left: 0; right: 0;
  top: 0; height: 0;          /* both written by the placement script */
  z-index: -1;
  pointer-events: none;
  overflow: hidden;

  background:
    radial-gradient(ellipse 60% 55% at 12% 88%,
      rgba(110, 132, 235, .30) 0%, rgba(110, 132, 235, 0) 100%),
    radial-gradient(ellipse 70% 60% at 85% 10%,
      rgba(45, 77, 224, .18) 0%, rgba(45, 77, 224, 0) 100%),
    linear-gradient(180deg,
      var(--page-bg) 0%,
      #0B1030 5%,
      var(--h01) 16%,
      var(--h01) 84%,
      #0B1030 95%,
      var(--page-bg) 100%);
}

#pixel { padding-top: 104px; padding-bottom: 104px; }

/* ── the head ───────────────────────────────────────────────────── */
#pixel .up-head {
  display: grid;
  grid-template-columns: 1.15fr .85fr;
  gap: 56px;
  align-items: start;
  margin-bottom: 76px;
}

#pixel .up-pill {
  color: #FFFFFF;
  background: rgba(255, 255, 255, .07);
  border-color: rgba(255, 255, 255, .22);
  text-transform: uppercase;
  letter-spacing: .08em;
  margin-bottom: 26px;
}

#pixel .up-title { color: #FFFFFF; margin: 0; }
#pixel .up-accent { color: #8E9BF2; }

#pixel .up-lede {
  color: #FFFFFF;
  font-size: 20px;
  font-weight: 500;
  margin: 8px 0 14px;
}
#pixel .up-body {
  color: rgba(255, 255, 255, .70);
  font-size: 17px;
  line-height: 1.6;
  margin: 0;
  max-width: 46ch;
}

/* ── the four steps ─────────────────────────────────────────────── */
#pixel .up-flow {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
  position: relative;
}

/* The connector. One line behind the row, so it reads as a single run
   rather than three separate joins. */
#pixel .up-flow::before {
  content: "";
  position: absolute;
  left: 8%; right: 8%;
  top: 34px;
  height: 1px;
  background: linear-gradient(90deg,
    rgba(142, 155, 242, 0),
    rgba(142, 155, 242, .55) 12%,
    rgba(142, 155, 242, .55) 88%,
    rgba(142, 155, 242, 0));
  pointer-events: none;
}

#pixel .up-step {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

#pixel .up-num {
  display: grid;
  place-items: center;
  width: 68px; height: 68px;
  border-radius: 18px;
  background: rgba(255, 255, 255, .05);
  border: 1px solid rgba(255, 255, 255, .16);
  color: #FFFFFF;
  font-size: 28px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  margin-bottom: 20px;
  position: relative;      /* sits over the connector */
  backdrop-filter: blur(2px);
}

#pixel .up-name {
  color: #FFFFFF;
  font-size: 19px;
  font-weight: 500;
  line-height: 1.32;
  margin: 0 0 10px;
}

#pixel .up-copy {
  color: rgba(255, 255, 255, .68);
  font-size: 16px;
  line-height: 1.55;
  margin: 0;
  max-width: 30ch;
}

#pixel .up-art { margin-top: 26px; }
#pixel .up-art-cube { margin-top: 18px; }

/* ── 01, the source ─────────────────────────────────────────────── */
#pixel .up-art-source {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: start;
}

#pixel .up-browser {
  width: 128px;
  padding: 12px;
  border-radius: 14px;
  background: rgba(20, 26, 66, .85);
  border: 1px solid rgba(142, 155, 242, .30);
  display: flex;
  flex-direction: column;
  gap: 9px;
}
#pixel .up-bar     { height: 7px;  border-radius: 4px; background: rgba(142,155,242,.45); width: 62%; }
#pixel .up-block   { height: 42px; border-radius: 8px; background: var(--grad-blue); }
#pixel .up-line    { height: 6px;  border-radius: 4px; background: rgba(255,255,255,.20); }
#pixel .up-line.short { width: 64%; }
#pixel .up-pillbtn { height: 16px; width: 48px; border-radius: 9px; background: var(--grad-blue); }

#pixel .up-signals { list-style: none; margin: 0; padding: 0; display: grid; gap: 11px; }
#pixel .up-signals li {
  display: flex;
  align-items: center;
  gap: 10px;
  color: rgba(255, 255, 255, .80);
  font-size: 14px;
  white-space: nowrap;
}
#pixel .up-signals span {
  display: grid;
  place-items: center;
  width: 24px; height: 24px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .06);
  border: 1px solid rgba(255, 255, 255, .16);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, .70);
  flex: none;
}

/* ── 02, the layers ─────────────────────────────────────────────── */
#pixel .up-art-layers {
  position: relative;
  height: 108px;
  display: grid;
  place-items: center;
}
#pixel .up-art-layers i {
  position: absolute;
  width: 104px; height: 104px;
  background: linear-gradient(180deg, #8E9BF2, var(--grad-blue));
  transform: translateY(calc(var(--i) * 15px)) rotateX(62deg) rotateZ(45deg);
  border-radius: 10px;
  opacity: .92;
}
#pixel .up-art-layers i:nth-child(1) { --i: -1; opacity: 1; }
#pixel .up-art-layers i:nth-child(2) { --i:  0; opacity: .74; }
#pixel .up-art-layers i:nth-child(3) { --i:  1; opacity: .52; }

/* ── 03, the shield ─────────────────────────────────────────────── */
#pixel .up-art-shield { display: grid; place-items: center; height: 108px; }
#pixel .up-art-shield svg { width: 78px; height: auto; display: block; }
#pixel .up-art-shield path {
  fill: rgba(110, 132, 235, .16);
  stroke: #8E9BF2;
  stroke-width: 4;
  stroke-linejoin: round;
}

/* ── 04, the record ─────────────────────────────────────────────── */
#pixel .up-art-cube { display: grid; gap: 10px; justify-items: start; }

/* The box has to hold the LATTICE, not the rig's nominal square. With
   a 42px gap the three cubes span 2*42 + 30 = 114px across, and the
   tilt throws that out to about 190px vertically, so a 132px box put
   the bottom row straight through the chips underneath. */
#pixel .up-rig {
  width: 170px; height: 200px;
  margin: 0;
  --c3-cube: 30px;
  --c3-gap: 42px;
}

#pixel .up-chips {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
#pixel .up-chips li {
  font-size: 13px;
  color: rgba(255, 255, 255, .82);
  padding: 7px 12px;
  border-radius: 9px;
  background: rgba(255, 255, 255, .05);
  border: 1px solid rgba(255, 255, 255, .16);
  white-space: nowrap;
}

/* ── narrower ───────────────────────────────────────────────────── */
@media (max-width: 1100px) {
  #pixel .up-flow { grid-template-columns: repeat(2, 1fr); gap: 44px 32px; }
  #pixel .up-flow::before { display: none; }
}

@media (max-width: 760px) {
  #pixel { padding-top: 72px; padding-bottom: 72px; }
  #pixel .up-head { grid-template-columns: 1fr; gap: 28px; margin-bottom: 48px; }
  #pixel .up-flow { grid-template-columns: 1fr; gap: 40px; }
  #pixel .up-art-source { grid-template-columns: auto 1fr; }
  #pixel .up-num { width: 58px; height: 58px; font-size: 24px; border-radius: 15px; }
}

@media (prefers-reduced-motion: reduce) {
  #pixel .up-rig .c3-spin,
  #pixel .up-rig .c3-tilt,
  #pixel .up-rig .c3-spinner { animation: none; }
}


/* ── Universal pixel · ground and flow off ───────────────────────────
   The dark ground and the four step diagram both come out. Neither is
   deleted: the markup for the steps is still in the page and the
   .up-bg layer is still placed by the script, so this is one block to
   remove if either should come back.

   The copy has to come back to the page's own ink with them. It was
   set in white for the dark ground, and white on #F5F6FA is invisible,
   so the colour overrides are undone here rather than left to be
   discovered later.
   ════════════════════════════════════════════════════════════════════ */
.up-bg          { display: none !important; }
#pixel .up-flow { display: none; }

#pixel { padding-top: 80px; padding-bottom: 80px; }
#pixel .up-head { margin-bottom: 0; }

/* the eyebrow goes back to the page's pill, not the one cut for dark

   THE CASE AND TRACKING HAD TO COME BACK TOO. The first pass reverted
   only the colours, so this was the one eyebrow on the page still
   carrying the dark mockup's uppercase and .08em tracking: every other
   section reads "Revenue Suite", "Marketing Suite", "The problem" in
   sentence or title case at normal tracking. Both are reset here. */
#pixel .up-pill {
  color: var(--pill-ink, #1B2A9E);
  background: rgba(58, 68, 224, .08);
  border-color: rgba(58, 68, 224, .40);
  text-transform: none;
  letter-spacing: normal;
}

#pixel .up-title  { color: var(--text-hi); }
#pixel .up-accent { color: #2D4DE0; }
#pixel .up-lede   { color: var(--text-hi); }
#pixel .up-body   { color: #3C4257; }


/* ── Universal pixel · the workflow ──────────────────────────────────
   The supplied graphic, with the copy for each step under it.

   ALIGNMENT IS NOT GUESSED. The artwork's viewBox is 1248 wide and its
   four step centres fall on 156, 468, 780 and 1092, which are exactly
   the centres of four equal 312 columns. So the copy sits on a plain
   four column grid and lines up with the graphic at every width, with
   no per breakpoint nudging.

   Each step's copy is a left aligned block centred in its column, so
   it reads as a paragraph while sitting under the middle of its own
   piece of artwork. Type is the page's own: the step name matches the
   role card titles at 21px, the copy matches their body at 14.5px.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-flow { display: block; margin-top: 64px; }

#pixel .up-graphic {
  display: block;
  width: 100%;
  height: auto;
  max-width: 1248px;
  margin: 0 auto;
}

#pixel .up-steps {
  list-style: none;
  margin: 26px auto 0;
  padding: 0;
  max-width: 1248px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0 24px;
}

#pixel .up-steps .up-step {
  display: block;
  justify-self: center;
  max-width: 258px;
  min-width: 0;
}

#pixel .up-n {
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .06em;
  color: #1B2A9E;
  font-variant-numeric: tabular-nums;
}

#pixel .up-steps .up-name {
  margin: 0 0 8px;
  font-size: 21px;
  font-weight: 700;
  line-height: 1.24;
  color: var(--text-hi);
}

#pixel .up-steps .up-copy {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.55;
  color: #3C4257;
  max-width: none;
}

/* Two up, then one up. The graphic stays whole and simply scales: it
   is a single run left to right, so cutting it into pieces would lose
   the line that is the point of it. */
@media (max-width: 1000px) {
  #pixel .up-steps { grid-template-columns: repeat(2, 1fr); gap: 30px 24px; }
  #pixel .up-steps .up-step { max-width: 320px; justify-self: start; }
}

@media (max-width: 620px) {
  #pixel .up-flow { margin-top: 44px; }
  #pixel .up-steps { grid-template-columns: 1fr; gap: 26px; }
  #pixel .up-steps .up-step { max-width: none; }
}


/* ── Universal pixel · type and step alignment ───────────────────────
   THE TYPE IS THE PAGE'S, not a copy of its numbers. The headline is a
   plain .section-title with a .dim second line, exactly like every
   other section, and the right column is a .section-sub. The colour
   overrides I had put on them are undone here: the title was sitting
   at #0A0F2E while every other light section's title is #1B1F7A, which
   is what made this one look like a different page.

   THE STEP COPY LINES UP WITH ITS OWN ILLUSTRATION. The artwork's four
   pieces start at 29, 415, 725 and 1017.5 in a 1248 viewBox, and each
   column starts at 0, 312, 624 and 936. The difference is the inset,
   expressed against the column width so it holds at every size:

     29   -   0  =  29    ->   9.295%
     415  - 312  = 103    ->  33.013%
     725  - 624  = 101    ->  32.372%
     1017.5 - 936 = 81.5  ->  26.122%

   Gap is zero and the breathing room is padding on the right, because
   a gap would shift every column off the artwork underneath it.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-title  { color: unset; }
#pixel .up-lede   { color: var(--text-hi); font-size: 18px; font-weight: 600; margin: 4px 0 10px; }
#pixel .up-body   { color: unset; font-size: unset; line-height: unset; max-width: 46ch; margin: 0; }

#pixel .up-steps {
  gap: 0;
  grid-template-columns: repeat(4, 1fr);
}

#pixel .up-steps .up-step {
  justify-self: stretch;
  max-width: none;
  padding-right: 28px;
}

#pixel .up-steps .up-step:nth-child(1) { margin-left:  9.295%; }
#pixel .up-steps .up-step:nth-child(2) { margin-left: 33.013%; }
#pixel .up-steps .up-step:nth-child(3) { margin-left: 32.372%; }
#pixel .up-steps .up-step:nth-child(4) { margin-left: 25.000%; }

/* Below the two up breakpoint the columns no longer sit under the
   artwork, so the insets come off rather than pointing at nothing. */
@media (max-width: 1000px) {
  #pixel .up-steps { gap: 30px 24px; }
  #pixel .up-steps .up-step:nth-child(1),
  #pixel .up-steps .up-step:nth-child(2),
  #pixel .up-steps .up-step:nth-child(3),
  #pixel .up-steps .up-step:nth-child(4) { margin-left: 0; padding-right: 0; }
}


/* ── Universal pixel · the top line off ──────────────────────────────
   The faint rule running across the upper part of the flow was mine,
   a ::before on .up-flow left over from the first build of this
   section before the artwork arrived. It sat at 34px from the top,
   which is nowhere near the middle of the illustrations, so it read as
   a stray line above them.

   The run between the steps belongs to the artwork, at y=120 of its
   250 viewBox, which is the middle of the pieces it joins.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-flow::before { display: none; }



/* ── step four's cube, packed ────────────────────────────────────────
   The cubes were drawn at half-width 22.5 and half-height 13 on a
   lattice whose pitch is 26 by 15, so each one was about 13% too small
   for its own spacing and the whole block read as loose blocks with
   daylight between them. Redrawn at 26 by 15 with a 30 side, which is
   the size the pitch asks for, so the faces meet.

   That moved the cluster's left edge from 1017.5 to 1014, and step
   four's text inset moves with it: (1014 - 936) / 312 = 25%.
   ════════════════════════════════════════════════════════════════════ */


/* ── Universal pixel · the head, levelled ────────────────────────────
   The eyebrow now sits on a row of its own spanning both columns, so
   the headline and the right hand copy both start on the second row
   and their first lines are level. Before this the right column was
   levelling with the eyebrow, which put it a whole line above the
   headline it is meant to sit beside.

   The gap under "See the complete guest journey" was the .section-sub
   carrying its own top margin on top of the heading's bottom margin.
   One of them goes.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-head {
  grid-template-columns: 1.15fr .85fr;
  align-items: start;
}

#pixel .up-head .up-pill {
  grid-column: 1 / -1;
  justify-self: start;
  margin-bottom: 26px;
}

#pixel .up-head-l { grid-column: 1; }
#pixel .up-head-r { grid-column: 2; }

#pixel .up-head-r .up-lede    { margin-top: 0; }
#pixel .up-head-r .section-sub { margin-top: 10px; }

@media (max-width: 760px) {
  #pixel .up-head-l,
  #pixel .up-head-r { grid-column: 1 / -1; }
}

/* The headline carries a flat 20px top margin at every width, measured,
   and the right column carries none, so the two boxes started 20px
   apart. Matching it puts their first lines level: the half leading is
   1.5px on the 50.4px headline against 1.8px on the 18px lede, so the
   text tops land within a third of a pixel of each other. */
#pixel .up-head-r { padding-top: 20px; }

@media (max-width: 760px) {
  #pixel .up-head-r { padding-top: 0; }
}


/* ── Universal pixel · step four is the real cube ────────────────────
   The flat cube is out of the artwork and the page's own 27 tile rig
   sits in its place, so the record the four steps end on is the object
   the reader has been following, not a drawing of it.

   IT IS PLACED ON THE ARTWORK'S OWN NUMBERS. The removed cube occupied
   x 1014 to 1170 and y 32 to 212 in a 1248 by 250 viewBox, so its
   centre is 1092 across and 122 down: 87.5% and 48.8%. The rig is
   centred on exactly that.

   THE SIZE MATCHES WHAT IT REPLACED. The old cluster was 156 wide.
   A rig's lattice spans two gaps plus one cube, so 2 x 59 + 38 = 156.
   Those are the ICS cube's proportions scaled by 156/169.

   Pixel layout cannot follow a fluid SVG in percentages, so a short
   script publishes --up-k, the graphic's width over its 1248 design
   width, and the rig is scaled by it.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-stage {
  position: relative;
  max-width: 1248px;
  margin: 0 auto;
}

#pixel .up-graphic { max-width: none; }

#pixel .up-rig {
  position: absolute;
  left: 87.5%;
  top: 48.8%;
  width: 156px;
  height: 156px;
  margin: 0;

  /* A rig paints wider than its nominal span, because each tile has
     depth of its own and the whole lattice is tilted. Measured, the
     painted width is 1.204 times two gaps plus a cube. So the span is
     set to 156 / 1.204 = 129.6 and the cube and gap keep the ICS
     cube's proportions within it: 2 x 49 + 32 = 130. */
  --c3-cube: 32px;
  --c3-gap: 49px;

  /* ORDER MATTERS HERE. With translate written first it runs last, in
     the parent's coordinates, so it always shifts by half of 156
     however small the graphic has become and the cube walks off its
     mark. Scaling about the top left after the centring translate
     scales the offset with everything else, and the cube stays on
     87.5% / 48.8% at every width. */
  transform-origin: 0 0;
  transform: scale(var(--up-k, 1)) translate(-50%, -50%);
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  #pixel .up-rig .c3-spin,
  #pixel .up-rig .c3-tilt,
  #pixel .up-rig .c3-spinner { animation: none; }
}


/* ── Universal pixel · closing the two gaps ──────────────────────────
   THE EYEBROW SAT 102px ABOVE THE HEADLINE. Every other section on the
   page sets 20px: revenue, marketing and ICS all measure 20.0. Here
   three separate values were stacking, and none of them was wrong on
   its own:

       26px  the pill's own margin-bottom
     + 56px  the head grid's row-gap, which only started applying once
             the eyebrow was moved onto a row of its own to level the
             two columns
     + 20px  the headline's top margin, same as every other section
     ------
       102px

   The row-gap and the pill margin both go. The headline keeps its 20px
   so the number matches the rest of the page exactly, and the right
   column keeps the 20px padding that levels it with the headline.

   THE SEAM ABOVE IT. Problem closed on 80px and pixel opened on 80px,
   the page's standard 160. The puzzle art contributes only about 13px
   of transparent frame, so the space really was layout rather than the
   image. With the problem section's original content hidden, both
   sides of that seam are near empty, and 160px of nothing reads as a
   dropped section. Halved to 40 and 40.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-head { row-gap: 0; }
#pixel .up-head .up-pill { margin-bottom: 0; }

#problem { padding-bottom: 40px; }
#pixel   { padding-top: 40px; }


/* ── Universal pixel · the lede sat 48px off its own body copy ───────
   Measured by scanning the column for rows containing ink, not from
   the box model, because the box model hides most of it: the boxes are
   10px apart and the painted text was 48px apart. Four and a half
   times the 11px that separates the body's own two lines, so the lede
   read as a heading for something else.

   The 10px was not the problem. .section-sub carries a global
   padding-top: 30px, which is right where it was written for: the sub
   sitting beside a 50px headline in a two column section head, where
   it needs that much to optically align. #how and #ics already zero it
   for the same reason this does. Here the sub follows an 18px lede, so
   30px of padding under it is out of proportion to the type.

   Padding off, and the lede's own margin taken to 8px. That lands the
   painted gap at about 16px: comfortably more than the 11px between
   the body's lines, so it still reads as a step down from heading to
   text, and a third of what it was.
   ════════════════════════════════════════════════════════════════════ */
#pixel .up-head-r .section-sub {
  padding-top: 0;
  margin-top: 0;
}

#pixel .up-head-r .up-lede { margin-bottom: 8px; }


/* ════════════════════════════════════════════════════════════════════
   UNIVERSAL PIXEL, PHONE LAYOUT: a vertical timeline
   Everything below is inside one query and nothing above it changes,
   so the tablet and desktop section is untouched.

   760px, not 767. The site switches at 900px in 42 rules, and this
   section already switches at 1000, 760 and 620. The header stacks at
   760, so reusing it means the whole section becomes mobile at one
   width instead of the header changing at 760 and the steps at 767.

   WHAT WAS ACTUALLY BROKEN. The steps never stacked. Two earlier rules
   meant to do it, repeat(2,1fr) at 1000px and 1fr at 620px, are dead:
   a later unconditional rule sets repeat(4,1fr), and a media query
   carries no extra specificity, so the later rule won at every width.
   At 320px that left four 52px columns with every title overflowing.
   Rather than edit those rules, which would be a change outside this
   query, the column count is set again here where it wins.

   THE FOUR ILLUSTRATIONS ARE ONE FILE. pixel-flow.svg holds steps 1 to
   3 and step 4 is a live cube in the DOM. So steps 1 to 3 take their
   art as a cropped window onto that same file, by background-position,
   and step 4 keeps the real cube. Nothing is redrawn, recoloured or
   replaced: each step shows a region of the artwork already there.
   Because the full graphic is hidden, the horizontal connector inside
   it never appears on a phone, so the SVG needed no edit either.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 760px) {

  /* ---- the timeline frame ---------------------------------------- */
  #pixel .up-flow {
    --rail: 28px;          /* the gutter the line and dots live in */
    --art: 96px;           /* art column, fixed so every title aligns */
    --colgap: 16px;
    --rowgap: 28px;
    --dot-y: 9px;          /* centre of the step number, see .up-n */

    position: relative;
    display: grid;
    grid-template-columns: var(--art) minmax(0, 1fr);
    column-gap: var(--colgap);
    row-gap: var(--rowgap);
    padding-left: var(--rail);
    margin-top: 32px;
  }

  /* The list box is removed so the four steps and the cube stage
     become siblings of one grid, which is what lets the live cube sit
     in step 4's art cell. role="list" on the ol keeps it a list for a
     screen reader, which display:contents would otherwise drop. */
  #pixel .up-steps { display: contents; }

  /* ---- one step ---------------------------------------------------
     art on the left spanning the whole step, number then title on the
     right, description under both. */
  #pixel .up-steps .up-step {
    position: relative;
    grid-column: 1 / -1;
    display: grid;
    grid-template-columns: var(--art) minmax(0, 1fr);
    /* Row 1 is fixed at the number's line height. That is what keeps
       --dot-y honest: a taller illustration grows row 2 and never row
       1, so every dot sits the same 9px below its step's top edge and
       the rail can be drawn from one to the next with no measuring. */
    grid-template-rows: 18px auto auto;
    column-gap: var(--colgap);
    align-content: start;
    margin-left: 0;
    padding-right: 0;
    max-width: none;
  }

  /* placed explicitly, because the cube stage is placed explicitly too
     and auto flow would otherwise refuse to share row 4 with it */
  #pixel .up-steps .up-step:nth-child(1) { grid-row: 1; }
  #pixel .up-steps .up-step:nth-child(2) { grid-row: 2; }
  #pixel .up-steps .up-step:nth-child(3) { grid-row: 3; }
  #pixel .up-steps .up-step:nth-child(4) { grid-row: 4; }

  /* Rows 1 and 2 only, so the description in row 3 clears the art
     instead of running underneath it. */
  #pixel .up-steps .up-step::before {
    content: "";
    grid-column: 1;
    grid-row: 1 / span 2;
    align-self: center;
    justify-self: center;
    background-repeat: no-repeat;
  }

  /* Each crop is the same artwork scaled by box width over region
     width, so the aspect ratio is exact and nothing is distorted.
     Region 29,5 254x230 at 96 wide -> k = 0.3780 */
  #pixel .up-steps .up-step:nth-child(1)::before {
    width: 96px; height: 87px;
    background-image: url("pixel-flow.svg?v=8");
    background-size: 471.7px 94.5px;
    background-position: -11px -1.9px;
  }
  /* region 415,71 106x103 at 64 wide -> k = 0.6038 */
  #pixel .up-steps .up-step:nth-child(2)::before {
    width: 64px; height: 62px;
    background-image: url("pixel-flow.svg?v=8");
    background-size: 753.5px 150.9px;
    background-position: -250.6px -42.9px;
  }
  /* region 725,55 110x142 at 64 wide -> k = 0.5818 */
  #pixel .up-steps .up-step:nth-child(3)::before {
    width: 64px; height: 83px;
    background-image: url("pixel-flow.svg?v=8");
    background-size: 726.1px 145.5px;
    background-position: -421.8px -32px;
  }
  /* step 4's art is the live cube below, so no crop here */
  #pixel .up-steps .up-step:nth-child(4)::before {
    width: 64px; height: 64px;
  }

  /* ---- the live cube becomes step 4's illustration ---------------- */
  #pixel .up-stage {
    grid-row: 4;
    grid-column: 1;
    align-self: start;
    margin-top: 6px;
    justify-self: center;
    width: 64px;
    height: 64px;
    max-width: none;
    margin: 0;
  }

  #pixel .up-graphic { display: none; }

  /* 2 gaps plus a cube is 53, which paints at about 64 once the tilt
     and the tiles' own depth are counted, the same 1.204 the desktop
     size was derived from */
  #pixel .up-rig {
    left: 50%;
    top: 50%;
    width: 64px;
    height: 64px;
    --c3-cube: 13px;
    --c3-gap: 20px;
    transform-origin: 50% 50%;
    transform: translate(-50%, -50%);
  }

  /* ---- number, title, description --------------------------------- */
  #pixel .up-steps .up-n {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-size: 13px;
    line-height: 18px;     /* fixes --dot-y at 9px, the number's middle */
  }

  #pixel .up-steps .up-name {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    margin: 2px 0 0;
    font-size: 19px;
    line-height: 1.25;
  }

  #pixel .up-steps .up-copy {
    grid-column: 1 / -1;
    grid-row: 3;
    margin: 10px 0 0;
    font-size: 15px;
    line-height: 1.55;
  }

  /* ---- the rail: one dot per step, joined by segments -------------
     The line is drawn per step rather than once on the list, because
     the list has no box any more. Each segment starts at its own dot
     and ends exactly on the next one, and the last step draws none, so
     the rail runs dot to dot and overshoots neither end. */
  #pixel .up-steps .up-step::after {
    content: "";
    position: absolute;
    left: calc(-1 * var(--rail) / 2 - 5px);
    top: calc(var(--dot-y) - 5px);
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #7871F6;
    box-shadow: 0 0 0 3px var(--page-bg, #F5F6FA);
    z-index: 1;
  }

  #pixel .up-steps .up-step:not(:last-child) .up-copy::after {
    content: "";
    position: absolute;
    left: calc(-1 * var(--rail) / 2 - 0.75px);
    top: var(--dot-y);
    bottom: calc(-1 * (var(--rowgap) + var(--dot-y)));
    width: 1.5px;
    background: #A3A7F8;
  }

  /* the segment is measured against the step, not the paragraph */
  #pixel .up-steps .up-step .up-copy { position: static; }

  /* ---- header ------------------------------------------------------ */
  #pixel .section-title {
    font-size: clamp(30px, 8.5vw, 36px);
    line-height: 1.05;
  }

  #pixel .up-head-r .up-lede { margin-bottom: 13px; }
}


/* ── Universal pixel · the four step texts on one baseline ───────────
   Step 1's title runs to two lines and the other three to one, so its
   body copy started 26px below theirs while the numbers and titles all
   lined up. Reserving two line heights on every title puts the four
   paragraphs on the same line.

   Desktop only. Stacked, each step stands on its own and there is
   nothing to line up with, so the reservation would just add a gap
   under the short titles.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 761px) {
  /* Subgrid rather than a reserved height. A fixed two line minimum
     holds at 1440 and breaks by 820, where the columns narrow enough
     that step 1's title needs three lines. Letting the four steps
     share the row track instead means the number row, the title row
     and the copy row each take the tallest of the four, so the
     paragraphs start together at any width with no measured number in
     the stylesheet at all. Each step has exactly three children, which
     is what the three rows map to. */
  #pixel .up-steps {
    grid-template-rows: auto auto auto;
    align-items: start;
  }

  #pixel .up-steps .up-step {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
  }
}


/* ── Universal pixel · stacked steps read as one block ───────────────
   The description spanned both columns, so it began at the rail while
   its own number and title began 112px further in. Column 2 only puts
   the three on one left edge, so each step reads as number, title,
   text rather than as a title with a caption under the picture.

   The art can span the whole step again now that nothing shares its
   column. Spanning rows 1 and 2 only would leave an empty band under
   the one line titles, since the illustration is taller than they are.
   Row 1 stays pinned at the number's 18px either way, so the dot on
   the rail does not move.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 760px) {
  #pixel .up-steps .up-copy {
    grid-column: 2;
    margin-top: 6px;
  }

  #pixel .up-steps .up-step::before { grid-row: 1 / -1; }
}


/* Stacked, the headline and the lede met with nothing between them:
   measured 0px at both 390 and 760, so "Tied to a booking." sat
   directly on "See the complete guest journey". Above 760 the two are
   side by side in their own columns and this does not apply. */
@media (max-width: 760px) {
  #pixel .up-head-r { margin-top: 20px; }
}



/* ── Universal pixel · step 3's mobile crop follows the new artwork ──
   The shield occupied 725,55 at 110 x 142. The stacked cards sit at
   708,50 at 144 x 144, so the window onto the file moves with them:
   64 wide over a 144 wide region is a scale of 0.4444, which puts the
   sheet at 554.7 x 111.1 and the corner at -314.7, -22.2.
   The step 1 crop is unchanged, because the lavender site is 254 x 230
   and sits on exactly the footprint the old panel had.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 760px) {
  #pixel .up-steps .up-step:nth-child(3)::before {
    width: 64px;
    height: 64px;
    background-size: 554.7px 111.1px;
    background-position: -314.7px -22.2px;
  }
}


/* ════════════════════════════════════════════════════════════════════
   ICS METRIC CARDS as a stack of three
   The composition of the stacked event cards in the universal pixel
   section: the back card furthest up and to the right and turned, the
   middle one up and to the left, the front one lowest and forward.

   Driven by data-depth rather than by --d arithmetic. The old rule
   worked the offset out as a multiple of depth, which only holds while
   every step is the same. These three resting places are not a linear
   series: the x offset goes 0, then left, then right, and only the
   back card turns, so each depth is written out.

   DARK PALETTE. The section paints no background of its own, so the
   colour was read off the pixels behind the card: rgb(7, 10, 31),
   luminance 0.043. On a dark ground the chrome is the accent at
   falling strength rather than the illustration's white and #EEEFFC.
   The card faces themselves are the existing exports and are not
   touched, so every figure inside keeps the contrast it had.

   The stack is already out of the accessibility tree: .sm-card carries
   aria-hidden, and the card holds no text, no buttons and nothing
   focusable, so there is nothing behind the front card to reach by
   keyboard. pointer-events is taken off the two behind anyway.
   ════════════════════════════════════════════════════════════════════ */
#ics .sm-stack .sm-tile {
  transform-origin: 50% 50%;
}

#ics .sm-stack .sm-tile[data-depth="0"] {
  transform: translate(0, 0);
  opacity: 1;
  z-index: 12;
  border-color: rgba(120, 113, 246, .70);
  box-shadow: rgba(27, 31, 122, .35) 0 8px 20px -14px;
}

#ics .sm-stack .sm-tile[data-depth="1"] {
  transform: translate(-8px, -32px);
  opacity: 1;
  z-index: 11;
  border-color: rgba(120, 113, 246, .34);
  box-shadow: none;
}

#ics .sm-stack .sm-tile[data-depth="2"] {
  transform: translate(10px, -56px) rotate(-8deg);
  opacity: .45;
  z-index: 10;
  border-color: rgba(120, 113, 246, .20);
  box-shadow: none;
}

#ics .sm-stack .sm-tile[data-depth="1"],
#ics .sm-stack .sm-tile[data-depth="2"] {
  pointer-events: none;
}

/* A third off the offsets and the turn eased to 5 degrees, so the
   raised corner stays inside the column on a phone. */
@media (max-width: 767px) {
  #ics .sm-stack .sm-tile[data-depth="1"] {
    transform: translate(-5px, -21px);
  }
  #ics .sm-stack .sm-tile[data-depth="2"] {
    transform: translate(7px, -37px) rotate(-5deg);
  }
}


/* The reduced motion rule at line 5599 collapses the pile to its first
   tile, which is left over from when this was a CSS keyframe carousel
   and one card was the sensible still frame. The brief here is the
   opposite: reduced motion should show the stack, just without the
   movement. Restored for the three that make it up, with the parked
   fourth left hidden. The nth-child rule follows the other on purpose,
   since the fourth tile matches both and the later one has to win. */
@media (prefers-reduced-motion: reduce) {
  #ics .sm-stack .sm-tile:not(:first-child) { display: flex; }
  #ics .sm-stack .sm-tile:nth-child(4) { display: none; }
}


/* Between 901 and 1199 the metric column is narrowed to 314px and ends
   up about 20px from the right edge of the window, already overhanging
   its own stage. The turned back card needs 27px of that: 10 of offset
   plus about 17 of corner, which is what a 314 by 262 card sweeps when
   it turns 8 degrees about its centre. Measured at 1024 the page grew
   to 1031 against a 1024 viewport.

   The whole stack is moved inboard rather than the composition being
   altered, so the offsets and the turn stay exactly as specified and
   the three cards keep their relationship to each other.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) and (max-width: 1199px) {
  #ics .sm-stack { transform: translateX(-16px); }
}







/* ── ICS · a title under each of the three stages ────────────────────
   The diagram already reads left to right as three things happening.
   These name them. Sized under the ICS label rather than level with
   it, because ICS is what the middle stage IS and the title is what it
   DOES, so they are not the same rank of text.
   ════════════════════════════════════════════════════════════════════ */
/* .ics-sources is already position:absolute, so it contains its own
   absolute children. Giving it position:relative here unanchored the
   whole left column and dropped it over the heading. */
#ics .ics-step {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 15px;
  font-weight: 500;
  color: #C7D0F5;
}

/* ── The three titles sit on one line ────────────────────────────────
   Each hangs off a different box, so a shared offset would not align
   them: measured against the stage the source pills end at 442, the
   cube wrapper at 297 and the metric wrapper at 315. Those three
   bottoms hold steady from 1024 to 1440, so one target line of 464
   resolves to a different offset per box and the three titles read as
   one row.

   ICS keeps its own place above the middle title rather than being
   carried down with it, since it labels the cube and not the step.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #ics .ics-sources > .ics-step { top: calc(100% + 22px); }
  #ics .ics-step-mid            { top: calc(100% + 167px); }
  #ics .ics-step-end            { top: calc(100% + 149px); }
}

/* Stacked, each group keeps a title of its own just below it. */
@media (max-width: 900px) {
  #ics .ics-sources > .ics-step { top: calc(100% + 18px); }
  #ics .ics-step-mid            { top: calc(100% + 115px); }
  #ics .ics-step-end            { top: calc(100% + 48px); }
}

#ics .sm-stack .sm-tile:nth-child(3) { display: none; }

@media (prefers-reduced-motion: reduce) {
  #ics .sm-stack .sm-tile:nth-child(3) { display: none; }
}


/* ── ICS · the diagram sits 50px higher ──────────────────────────────
   A negative top margin rather than a transform, so the space it gives
   up is returned to the flow and the section closes up behind it
   instead of leaving a gap at the bottom. Everything in the diagram is
   positioned inside the stage, so the wires, the cube, the pills and
   the three titles all travel with it and keep their relationships.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #ics .ics-stage { margin-top: -50px; }
}


/* ── ICS metric stack · four cards, two places ───────────────────────
   All four recommendations cycle. The stack itself is two deep, the
   front and the one behind it, which is the stacked event cards
   composition with its turned back card taken out.

   A card pushed past the second place has left the stack, so depth 2
   is a fade rather than a third position. The rotation goes with it.

   The two parked rules further up the sheet are released here: every
   tile takes part now, so none of them is hidden.
   ════════════════════════════════════════════════════════════════════ */
#ics .sm-stack .sm-tile:nth-child(3),
#ics .sm-stack .sm-tile:nth-child(4),
#ics .sm-stack .sm-tile:nth-child(5) { display: flex; }

/* The faces used to be white PNGs, which carried the card's own
   background. They are HTML now, so the tile supplies it. */
#ics .sm-stack .sm-tile { background: #FFFFFF; }

#ics .sm-stack .sm-tile[data-depth="2"] {
  transform: translate(-16px, -64px);
  opacity: 0;
  z-index: 9;
  box-shadow: none;
}

@media (max-width: 767px) {
  #ics .sm-stack .sm-tile[data-depth="2"] { transform: translate(-10px, -42px); }
}

@media (prefers-reduced-motion: reduce) {
  #ics .sm-stack .sm-tile:nth-child(3),
  #ics .sm-stack .sm-tile:nth-child(4),
  #ics .sm-stack .sm-tile:nth-child(5) { display: flex; }
}


/* The card behind has to read as a separate card. In the illustration
   that is done with colour, a white front against an #EEEFFC back, but
   these faces are one white export used for both, so a white band 32px
   above a white card just looks like one taller card. Letting the dark
   ground come through does the same job: the back card recedes and the
   two edges separate, which is the whole point of a stack.
   ════════════════════════════════════════════════════════════════════ */
#ics .sm-stack .sm-tile[data-depth="1"] {
  opacity: .55;
  border-color: rgba(120, 113, 246, .45);
}


/* ── Problem · the six and the dot cube overlap ──────────────────────
   The pieces reach zero at --p 0.80 and the dot cube only began at
   0.82. Two hundredths of nothing, which was invisible while the merge
   ran over 359px of scroll and is not now that it runs over 179. The
   cube starts coming up at 0.74, so the two cross rather than meet.
   ════════════════════════════════════════════════════════════════════ */
#problem .pi-rig {
  opacity: calc(min(1, max(0, var(--p) - .74) * 5.55) - max(0, var(--p) - 1.05) * 4);
}


/* ── ICS · the third title centres on the card, not its wrapper ──────
   The title hangs off .ics-orb-wrap, which is also where the orb sits,
   and the card is not centred in it. Two separate reasons, measured:

   Above 1199 the wrapper is 408 wide and the card 314, both starting
   at the same left edge, so the card's centre is (408 - 314) / 2 = 47px
   to the left of the wrapper's.

   Between 901 and 1199 the two are the same width, but the stack is
   pulled 16px inboard there to keep the turned corner off the window
   edge, so the card's centre moves with it.

   Below 901 the section is a stacked column and the card is centred in
   its wrapper again, so nothing is needed.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 1200px) {
  #ics .ics-step-end { left: calc(50% - 47px); }
}

@media (min-width: 901px) and (max-width: 1199px) {
  #ics .ics-step-end { left: calc(50% - 16px); }
}


/* ── hidden has to win ───────────────────────────────────────────────
   .problem-intro is set to display:grid and .up-flow to display:grid
   further up the sheet. A rule that names display beats the browser's
   own [hidden] rule, so both blocks stayed on screen after the
   attribute was added. Naming the attribute puts it back in front.
   ════════════════════════════════════════════════════════════════════ */
#problem .problem-intro[hidden],
#pixel .up-flow[hidden] { display: none; }


/* ── Problem · the six cubes take the orbit's place ──────────────────
   The illustration is lifted out of the hidden .problem-intro and put
   in the grid's left column, where the orbit was. The orbit itself is
   kept in the file behind the hidden attribute.

   Not sticky, unlike the orbit it replaces. The merge reads the
   section's own top and the stage's inset into it, and a sticky stage
   reports a pinned inset that stops changing, which would freeze the
   six cubes part way through joining.
   ════════════════════════════════════════════════════════════════════ */
#problem .orbit[hidden] { display: none; }

#problem .problem-grid > .pi-art {
  align-self: start;
  margin: 0;
}


/* ── ICS · the three titles move to the top, on cards of their own ───
   They sat under each group. At the top they read as labels for what
   follows rather than captions for what has passed.

   The line is set by the highest thing in the diagram, which is the
   metric card at 49 from the stage top, not the sources at 58 or the
   cube at 88. Six from the top leaves the title box ending at 36 and
   thirteen clear of the card.
   ════════════════════════════════════════════════════════════════════ */
#ics .ics-step {
  top: 6px;
  padding: 7px 14px;
  border-radius: 10px;
  background: rgba(255, 255, 255, .07);
  border: 1px solid rgba(255, 255, 255, .16);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

/* One line across the diagram. top resolves against each title's own
   parent, not the stage, so a single value gives three different
   heights. Measured from the stage top the parents begin at 58, 127
   and 110, and the line is 2, which is what these three offsets come
   to. The title box is 36 tall, so it ends at 38 and clears the metric
   card at 49 by eleven. */
@media (min-width: 901px) {
  #ics .ics-sources > .ics-step { top: -44px; }
  #ics .ics-step-mid            { top: -113px; }
  #ics .ics-step-end            { top: -96px; }
}

@media (max-width: 900px) {
  #ics .ics-sources > .ics-step,
  #ics .ics-step-mid,
  #ics .ics-step-end { top: auto; bottom: calc(100% + 14px); }
}


/* ── ICS metric cards · 30px shorter, and the fade tracks the move ───
   The card was 262 tall. The fade was 220ms against a 950ms move, so a
   card leaving the stack finished disappearing long before it finished
   travelling, which is what read as a jump rather than a roll. The two
   are matched now.
   ════════════════════════════════════════════════════════════════════ */
#ics .sm-card,
#ics .sm-stack { height: 232px; }

/* SHORT ON PURPOSE. These faces are HTML, so a card at half opacity
   lets the one behind read straight through it and the two sets of
   figures superimpose into nonsense. A fade well shorter than the move
   means the arriving card is opaque before it lands and never blends
   with what it is covering. 780ms here was the cause of the garbled
   text during a handover. */
#ics .sm-stack .sm-tile { --t-fade: 240ms; }


/* The line is 14 from the stage top rather than 2, because below 1440
   the sub copy wraps to a third line and reaches down to 306. One
   value for the whole desktop range: a band boundary at 1200 left the
   overlap in place at exactly 1200.
   ════════════════════════════════════════════════════════════════════ */


/* ── Universal pixel · the previous illustrations, flush left ────────
   Steps 1 to 3 are cropped windows onto pixel-flow.svg, the artwork
   the section used before: the lavender booking site, the plate stack
   and the stacked event cards. Each crop is scaled by box width over
   region width, so the aspect ratio is exact.

   Step 4 keeps the zig-zag's own cube. The one it replaced was a live
   DOM cube driven off .up-stage, which is inside the hidden .up-flow
   and now measures zero, so it cannot size itself.

   Everything sits flush left in its column, illustration and copy on
   the same edge, and the connector path is re-derived from the icon
   widths rather than assuming they are centred.
   ════════════════════════════════════════════════════════════════════ */
/* One class deeper than the section's own inline style block, which
   sits later in the document and would otherwise win. */
#pixel .upz .upz-ico { justify-content: flex-start; }
#pixel .upz .upz-txt { padding-left: 0; padding-right: 24px; }

#pixel .upz-art {
  display: block;
  background-image: url("pixel-flow.svg?v=11");
  background-repeat: no-repeat;
}
#pixel .upz-a1{width:150px;height:136px;background-size:737.0px 147.6px;background-position:-17.1px -3.0px}
#pixel .upz-a2{width:114px;height:111px;background-size:1342.2px 268.9px;background-position:-446.3px -76.4px}
#pixel .upz-a3{width:144px;height:144px;background-size:1248.0px 250.0px;background-position:-708.0px -50.0px}

/* Each crop is a window onto one sheet, so a narrow screen has to
   rescale the background with the box. Setting height:auto on an
   empty element collapsed it to nothing and left background-size at
   the desktop figure, so all three illustrations were 64x0 and drew
   the wrong region. Each one carries its own ratio here. */
@media (max-width: 900px) {
  #pixel .upz-a1{width:64px;height:58px;background-size:314.5px 63.0px;background-position:-7.3px -1.3px}
  #pixel .upz-a2{width:64px;height:62px;background-size:753.5px 150.9px;background-position:-250.6px -42.9px}
  #pixel .upz-a3{width:64px;height:64px;background-size:554.7px 111.1px;background-position:-314.7px -22.2px}
}


/* Flush left, the copy blocks run the full column and the connector's
   vertical legs pass straight through them. The legs sit at 314, 618
   and 934 across a 1285 grid whose columns begin at 78, 399 and 720,
   so a block in one of those three columns has to stop short of 236,
   219 and 214. 200 covers all three with room, and the last column has
   no leg after it so it keeps the full width.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #pixel .upz .t1,
  #pixel .upz .t2,
  #pixel .upz .t3 { max-width: 200px; }
}


/* ── Workspaces · each pane keeps the head its section carried ───────
   The pill and headline were left behind in #revenue and #marketing
   when the suites moved. They belong with the suite, so each pane now
   opens with the one it had.
   ════════════════════════════════════════════════════════════════════ */
#workspaces .ws-head { margin-bottom: 32px; }
#workspaces .ws-head .section-title { margin-top: 16px; }


/* ── The gap · the sub copy sits beside the headline ─────────────────
   Two columns, the same shape the other section heads use, with the
   sub aligned to the headline's first line rather than stacked under
   it. The chips carry more room so the labels are not tight in them.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #compare .cmp-head-grid {
    display: grid;
    grid-template-columns: 1.15fr .85fr;
    column-gap: 56px;
    align-items: start;
  }
  #compare .cmp-head-grid .pill { grid-column: 1 / -1; justify-self: start; }
  #compare .cmp-head-grid .section-title { grid-column: 1; margin-top: 20px; }
  /* margin, not padding: the headline carries a 20px top margin, so
     padding here left the sub's box at the row top and its text 20px
     above the headline's. Matching the margin levels the two. */
  #compare .cmp-head-grid .section-sub {
    grid-column: 2;
    padding-top: 0;
    margin-top: 20px;
    max-width: 46ch;
  }
}

/* one class deeper than the section's own inline style, which is later
   in the document and would otherwise win */
#compare .cmp-col .cmp-chip,
#compare .cmp-labels + .cmp-col .cmp-chip { padding: 9px 18px; }

#compare .cmp-chip-logo {
  display: inline-flex;
  align-items: center;
  padding: 7px 16px;
}

#compare .cmp-chip-logo img {
  display: block;
  height: 22px;
  width: auto;
}


/* ── Universal pixel · step 4 is the dot cube again ──────────────────
   A static copy of the page's own rig, as the step used to carry. The
   original was driven off .up-stage, which is inside the hidden
   .up-flow and measures zero, so this one is sized outright.

   A rig paints about 1.204 times two gaps plus a cube, so 118 across
   needs a span of 98: 2 x 37 + 24, keeping the ICS cube's proportions.
   ════════════════════════════════════════════════════════════════════ */
#pixel .upz-rig {
  position: relative;
  width: 118px;
  height: 118px;
  margin: 0;
  --c3-cube: 24px;
  --c3-gap: 37px;
}

@media (max-width: 900px) {
  #pixel .upz-rig { width: 64px; height: 64px; --c3-cube: 13px; --c3-gap: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  #pixel .upz-rig .c3-spin,
  #pixel .upz-rig .c3-tilt,
  #pixel .upz-rig .c3-spinner { animation: none; }
}


/* ── Workspaces · both suites, no switch ─────────────────────────────
   The toggle is gone and the two panes stack, so each suite reads
   under its own heading the way it did as its own section.
   ════════════════════════════════════════════════════════════════════ */
#workspaces .ws-pane + .ws-pane { margin-top: 96px; }


/* ── ICS · 60px between the head, the titles and the diagram ─────────
   Derived, not guessed. Measured from the stage top the diagram's
   highest content begins at 58 and the title box is 36 tall, so for a
   60px gap either side of the titles the stage has to sit 98 below the
   head's bottom edge, which is margin-top 58 in place of the earlier
   -50. The titles then sit at -38 from the stage top, which is what
   these three offsets come to against their own parents at 58, 127
   and 110.

   The section grows by about 108px. That is what two 60px gaps cost
   where there was a 50px overlap before.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  /* 98, not the 58 the first pass computed: the head's box carries
     slack below its last line, so the measured head to titles gap
     came out 40 short. Measured again and corrected. */
  #ics .ics-stage { margin-top: 98px; }

  #ics .ics-sources > .ics-step { top: -96px; }
  #ics .ics-step-mid            { top: -165px; }
  #ics .ics-step-end            { top: -148px; }
}


/* ── ICS metric cards · the leaving card goes quietly ────────────────
   Depth 2 is the card that has left the stack. It fades on the same
   780ms the others move on, so on the way out it was still readable at
   a quarter opacity while it travelled up to -59, and a third card
   appeared above a two card stack. That is what read as a fault in the
   transition. Its fade is cut to 300ms, so it is gone well before it
   has finished moving.
   ════════════════════════════════════════════════════════════════════ */
#ics .sm-stack .sm-tile[data-depth="2"] { --t-fade: 300ms; }


/* The arriving card is opaque from the first frame of its travel, not
   faded in over it. Anything less and it is see through while it slides
   across the card it is covering, and two sets of figures read at once.
   It still slides for the full LAND, so the motion is unchanged: only
   its opacity stops being part of the animation.
   ════════════════════════════════════════════════════════════════════ */
#ics .sm-stack .sm-tile[data-depth="0"] { --t-fade: 0ms; }
#ics .sm-stack .sm-tile[data-depth="1"] { --t-fade: 240ms; }


/* ── Problem · the dot cube goes as soon as the other has it ─────────
   The travelling cube is up to 0.78 by --p 1.09 and moving off. The
   dot cube was fading from 1.05 to 1.30, so it was still readable at a
   quarter while the other had travelled 322px, and the two showed as
   two cubes. It now clears between 1.02 and 1.12, which is inside the
   window where the two are still within 30px of each other.
   ════════════════════════════════════════════════════════════════════ */
#problem .pi-rig {
  opacity: calc(min(1, max(0, var(--p) - .74) * 5.55) - max(0, var(--p) - 1.02) * 10);
}


/* ── Workspaces · each suite is its own destination ──────────────────
   Revenue Suite and Marketing Suite in the menus used to share one
   #workspaces target, so both landed on the top of the section and
   Marketing was off screen below the Revenue pane. Each pane carries
   its own id now, and the scroll offset keeps the pane heading clear
   of the bar rather than under it: --nav-h is the bar's measured
   height, republished on every resize.
   ════════════════════════════════════════════════════════════════════ */
#workspaces .ws-pane { scroll-margin-top: calc(var(--nav-h, 91px) + 28px); }


/* ── Universal pixel · step 2 copy nudged left ───────────────────────
   Twenty pixels, on the desktop grid only. .upz-txt is already
   position:relative, so this offsets the block without taking it out
   of the grid or changing what the column reserves. Below 901px the
   steps stack into a single column where the same shift would push
   the copy toward the edge, so it stays put there.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #pixel .upz .t2 { left: -20px; }
}


/* ── Why pulse. · the diagram on a phone ─────────────────────────────
   Four things were wrong below 901px, all of them measured at 390 and
   430.

   1. The three titles hang above their own parent on
      bottom: calc(100% + 14px). The first one's parent is the pill
      grid at the top of the stage, so the title landed on the last
      line of the section copy above it, overlapping by 10px. The
      stage carries a top margin now, which moves the pills and the
      title with them and opens a real gap.

   2. The last title sits above .ics-orb-wrap, and the ICS label under
      the cube ends exactly where that title begins, so the two boxes
      intersected at every width tested. It clears the label now.

   3. The metric card was width: min(100%, 340px), centred. At 390 that
      is a 25px gutter against the container's 20, and at 430 it is 45,
      so the card drifted further out of line the wider the phone. It
      takes the container's own gutter instead.

   The six sources stay on one row. They are cramped at 8.5px, but
   .ics-wires-m is a static path whose six x coordinates assume an even
   six across its 600 unit viewBox, so wrapping them to two rows would
   leave every wire pointing at nothing.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  /* 90, not 50. The stage already carried 40, so the first value only
     bought 10px and left the title touching the copy at exactly 0. */
  #ics .ics-stage { margin-top: 90px; }

  #ics .ics-step-end { bottom: calc(100% + 44px); }

  #ics .ics-orb-wrap .sm-metrics { width: 100% !important; }
}


/* ── Why pulse. · the two lower titles sit under what they name ──────
   On a phone the diagram reads top to bottom, so a title hanging above
   its own element named the thing before it. These two now sit under
   the thing they describe: "Unifies every signal" under the cube and
   its ICS label, "Recommends the next move" under the metric card.
   Both hang off their parent's bottom edge rather than its top, so
   they keep the centring they already had.

   The first title is left above the pills. It is the top of the
   diagram, and there is nothing above it but the section copy.

   Desktop is untouched. The three sit on one line across the diagram
   there, which is what the min-width rules above set up.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics .ics-step-mid,
  #ics .ics-step-end {
    top: calc(100% + 14px);
    bottom: auto;
  }
}

/* The last title now sits below the card, where the section has faded
   out to near white. The frosted treatment the other two wear is pale
   text on a translucent white pill, which is legible over the dark
   diagram and all but invisible down there, so this one is a solid
   brand pill instead. Same size, same centring, readable on the light
   end of the gradient. */
@media (max-width: 900px) {
  #ics .ics-step-end {
    background: #1B1F7A;
    border-color: #1B1F7A;
    color: #FFFFFF;
  }
}


/* ── The gap · pulse. leads the table on a narrow screen ─────────────
   The table scrolls sideways from 900px down, and pulse. was the last
   of six columns: measured at 390 it began 670px into a 790px track
   with 350px of it on screen, so reaching it took a 440px swipe that
   most people will never make. It sits first now, right beside the
   sticky row labels, and the four it is measured against follow.

   Order only. The markup still reads labels, RMS, agencies, BI,
   ChatGPT, pulse., and every cell carries its own .sr-only row name
   and value, so a cell still says what it is on its own.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #compare .cmp-labels       { order: 0; }
  #compare .cmp-col          { order: 2; }
  #compare .cmp-col.is-pulse { order: 1; }
}


/* ── One rhythm for every section ────────────────────────────────────
   Measured at 1440, the pairs ran 80/40, 225.6/225.6, 40/80, 80/0 and
   210/210, so the space between one section and the next came out at
   56, 64, 195, 266, 192, 80, 0, 80 and 33px. Nine different gaps down
   one page, including two touching sections and a 266px field of bare
   gradient between the diagram and the pixel steps.

   Every content section now carries the same figure, so every gap is
   twice it. 80 at desktop and 40 below 901px, which is what the two
   sections nobody objected to already used.

   ICS is the exception, and has to be. Its two rims are cut to --arc,
   a real curve the content cannot sit inside, so it carries the arc
   plus the standard figure rather than the figure alone. The hero and
   the logo strip keep what they had: one opens the page and the other
   is a strip, not a section.
   ════════════════════════════════════════════════════════════════════ */
:root { --section-y: 80px; }
@media (max-width: 720px) { :root { --section-y: 48px; } }

#problem, #pixel, #workspaces, #compare,
#about, #faqs, #demo {
  padding-top: var(--section-y);
  padding-bottom: var(--section-y);
}
#ics {
  padding-top: calc(var(--arc) + var(--section-y) / 4);
  padding-bottom: calc(var(--arc) + var(--section-y) / 4);
}

/* The four pairs that measured wide against the rest. Each is the sum
   of the two paddings either side, so half of the correction goes on
   each. The strip belongs with the hero, so it keeps a smaller figure
   below it than a full section would. */
.integrations { padding-bottom: 0; }
/* The strip carries no bottom padding, so this figure is the whole
   visible gap between the last logo label and the problem eyebrow.
   Half of --section-y put it at 40px, which read as the strip and
   the section running together. Explicit rather than a multiple of
   --section-y, because 104 and 72 are not the same fraction of 80
   and 48. */
#problem  { padding-top: 104px; }
@media (max-width: 720px) { #problem { padding-top: 72px; } }
#pixel    { padding-bottom: calc(var(--section-y) * 0.6); }
#workspaces { padding-top: calc(var(--section-y) * 0.6);
              padding-bottom: calc(var(--section-y) * 0.55); }
#compare  { padding-top: calc(var(--section-y) * 0.55); }
#demo     { padding-bottom: calc(var(--section-y) * 0.4); }
.footer-shell { padding-top: calc(var(--section-y) * 0.4); }

/* The demo card carries its own slack under the last line, which was
   153px of the 313 between it and the footer. */
#demo .glass.final { padding-bottom: 40px; }


/* ── Hero · the empty third below the buttons ────────────────────────
   min-height: 100svh held the hero open to the whole viewport, and the
   body added 128px under the buttons on top of that, so 207 to 316px
   of the hero was empty depending on the window. The floor keeps it
   from collapsing on a short laptop window, and the tiles are placed
   in percentages of this box so they rescale with it rather than
   being cut.
   ════════════════════════════════════════════════════════════════════ */
/* The height is held by .hero-body's own min-height in the head block,
   calc(100svh - 96px), not by .hero. Both come down by the same 150 so
   the section cannot spring back to the full viewport. */
.hero { min-height: max(560px, calc(100svh - 150px)); }
.hero-body {
  min-height: max(460px, calc(100svh - 246px));
  padding-bottom: 32px;
}

/* The rim itself was the rest of it. 9vw put the curve 129.6px deep at
   1440, on both edges, and a rim that deep is most of what read as a
   field of bare gradient. 6vw capped at 96 keeps the curve and returns
   about 43px a side. */
:root { --arc: clamp(48px, 6vw, 96px); }


/* ── Universal pixel · the flow runs down the left on a phone ────────
   Below 901px the four steps stack, and .upz-path, the zig-zag that
   joins them across the desktop grid, is switched off. That left the
   steps as four unconnected blocks with nothing showing they are one
   flow. This is that connector redrawn for a column: a dashed line
   down the icon rail, from under one illustration to the top of the
   next.

   Measured, the rail is a 72px grid column holding a 64px artwork at
   its left edge, so the line sits on 32 less half its own width. The
   cells stretch to their row height, so bottom: -28px is exactly the
   row gap and the line lands on the next artwork's top edge.

   The dashes match the desktop connector: a 10 on, 22 off pattern on
   a 32px period, travelling one whole period every 1.6s, over the same
   pale base line.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #pixel .upz-ico::after {
    content: "";
    position: absolute;
    left: 30.5px;                 /* 32 less half of the 3px line */
    top: 74px;                    /* under the tallest artwork, which is 64 */
    bottom: -28px;                /* across the row gap to the next step */
    width: 3px;
    border-radius: 2px;
    background-color: #C9CDF3;
    background-image: repeating-linear-gradient(180deg,
                        #2B33C4 0 10px, rgba(255, 255, 255, 0) 10px 32px);
    background-repeat: repeat-y;
    filter: drop-shadow(0 0 6px rgba(43, 51, 196, .25));
    animation: upz-flow-v 1.6s linear infinite;
  }

  /* nothing follows the last step, so it carries no line */
  #pixel .upz-ico.i4::after { content: none; }
}

@keyframes upz-flow-v { to { background-position: 0 32px; } }

@media (prefers-reduced-motion: reduce) {
  #pixel .upz-ico::after { animation: none; }
}


/* ── Universal pixel · the copy column fits the screen ───────────────
   .upz-txt is a grid item, so its min-width is auto and the 1fr track
   could not shrink below the column's min-content: the widest chip row
   (133 + 6 + 115 = 254) plus the 24px right padding, which pinned the
   track at 277.8px. Below about 414px the column ran past the grid box
   and MAIN, which hides overflow, cut it off.

   min-width: 0 lets the track shrink, the right padding goes, and at
   the narrowest sizes the chips fall to one per row so the min-content
   floor drops under the track that is actually available.
   ════════════════════════════════════════════════════════════════════ */
/* The section's own <style> block sits after this file, so a rule of
   equal weight there wins. Each of these carries one more class than
   the rule it has to beat. */
@media (max-width: 900px) {
  #pixel .upz .upz-txt {
    min-width: 0;
    padding-right: 0;
  }
  #pixel .upz .upz-chips { min-width: 0; }
}

@media (max-width: 460px) {
  #pixel .upz.upz { grid-template-columns: 64px 1fr; column-gap: 12px; }
  #pixel .upz .upz-chips { grid-template-columns: max-content; }
  #pixel .upz .upz-chips span { white-space: normal; }
}


/* ── Integrations · the strip carries no background of its own ───────
   The section painted a 90deg gradient whose stops are all #F5F6FA,
   the same colour the page already sits on. The marquee keeps its own
   edge fade, which is a mask on .logo-marquee and is untouched.
   ════════════════════════════════════════════════════════════════════ */
.integrations { background-image: none; }


/* ── The travelling cube keeps off the logo strip ────────────────────
   The rig is fixed to the viewport and crosses the whole page, so on
   the way down it passed straight over the logos. It is faded out
   while the strip is on screen, the same way it already gives way to
   the pixel graphic, and comes back on its own.
   ════════════════════════════════════════════════════════════════════ */

/* The tiles are placed in percentages of the tile field, which fills the
   hero, so a shorter hero brings them all up with it. At 390 the lowest
   one ended 7px past the hero's clip edge, so the field stops just short
   of the bottom there. */
@media (max-width: 900px) {
  .hero .tiles { bottom: 32px; }
}


/* ── Who built this · the team strip ─────────────────────────────────
   The old #about is kept in the file behind the hidden attribute. A
   plain [hidden] is only a UA rule, so anything that names display
   beats it: this one is explicit.
   ════════════════════════════════════════════════════════════════════ */
#about[hidden] { display: none; }

/* The strip runs the full width, under the copy rather than inside the
   container, so the figures can travel off both edges. */
#team .tm-marquee {
  position: relative;
  margin-top: 64px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg,
    transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(90deg,
    transparent 0, #000 7%, #000 93%, transparent 100%);
}

#team .tm-track {
  display: flex;
  width: max-content;
  animation: tm-marquee var(--tm-duration, 46s) linear infinite;
}
#team .tm-set {
  display: flex;
  align-items: flex-end;     /* one baseline, whatever a figure's width */
  flex: none;
  gap: clamp(24px, 3vw, 56px);
  padding-right: clamp(24px, 3vw, 56px);
}
@keyframes tm-marquee { to { transform: translateX(-50%); } }

/* Reading a name takes longer than reading a logo, so the whole strip
   stops while the pointer is anywhere on it. */
#team .tm-marquee:hover .tm-track,
#team .tm-marquee:focus-within .tm-track { animation-play-state: paused; }

#team .tm-card {
  position: relative;
  flex: none;
  margin: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

#team .tm-photo {
  display: block;
  width: auto;
  height: clamp(210px, 22vw, 320px);
  filter: grayscale(1) contrast(1.02);
  opacity: .72;
  transition: opacity 260ms ease, filter 260ms ease, transform 260ms ease;
}

#team .tm-card:hover .tm-photo,
#team .tm-card:focus-within .tm-photo {
  opacity: 1;
  transform: translateY(-6px);
}

/* The label sits over the figure's shoulders, out of the way of the
   face, and only on the one being pointed at. */
#team .tm-meta {
  position: absolute;
  left: 50%;
  bottom: 18%;
  transform: translate(-50%, 8px);
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 10px 16px;
  border-radius: 12px;
  background: rgba(255, 255, 255, .92);
  border: 1px solid #E4E5FB;
  box-shadow: 0 18px 40px -22px rgba(10, 15, 46, .45);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 220ms ease, transform 220ms ease;
}
#team .tm-card:hover .tm-meta,
#team .tm-card:focus-within .tm-meta {
  opacity: 1;
  transform: translate(-50%, 0);
}

#team .tm-role {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #2B33C4;
}
#team .tm-name {
  font-size: 16px;
  font-weight: 700;
  color: #0A0F2E;
}

@media (max-width: 900px) {
  #team .tm-marquee { margin-top: 40px; }
  #team .tm-photo   { height: clamp(170px, 42vw, 230px); }
  #team .tm-set     { gap: 20px; padding-right: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  #team .tm-track { animation: none; }
  #team .tm-marquee { overflow-x: auto; }
  #team .tm-photo { transition: none; }
  #team .tm-meta  { transition: none; }
}


/* ── Who built this · the label is text, not a chip ──────────────────
   No card behind the name any more, so it needs clear space rather
   than a surface of its own: the figure carries a band above it for
   the label to sit in.
   ════════════════════════════════════════════════════════════════════ */
/* The band has to clear the longest role on two lines, and the label
   gets a width of its own rather than inheriting the figure's: the
   narrowest card is 294px and the widest 423, so a label sized to its
   card would wrap differently under every person. */
#team .tm-card { padding-top: 76px; }

#team .tm-meta {
  top: 10px;
  bottom: auto;
  width: 250px;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  border-radius: 0;
  white-space: normal;
  text-align: center;
}

#team .tm-name {
  font-weight: 500;
  font-size: 15px;
  line-height: 1.25;
}

/* Sentence case, and no tracking. Uppercase with letter-spacing made
   "Manager, Digital Performance & Acquisition" three lines deep. */
#team .tm-role {
  color: rgba(0, 0, 0, .7);
  font-size: 11.5px;
  line-height: 1.35;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
}

@media (max-width: 900px) {
  #team .tm-card { padding-top: 72px; }
  #team .tm-meta { width: 180px; }
  #team .tm-name { font-size: 14px; }
  #team .tm-role { font-size: 11px; }
}


/* ── Why pulse. · the cube sits centre, the wires run shorter ────────
   The cube was at left 48% and then pushed a further 80px right, so it
   measured 55px off the stage's centre line. It is centred now.

   The wires are drawn from the dots to the cube's left face, both
   measured, so shortening the run is a matter of moving the two ends
   closer. Centring the cube brings it 55px nearer on its own, and the
   source column goes from 232 to 320 wide for the rest: the cards keep
   their left edge, so they stay in line with the headline above, and
   the gap between the last card and the cube drops by about 145px.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #ics .ics-cube-wrap {
    left: 50%;
    transform: translate(-50%, -50%);
  }

  #ics .ics-sources { width: 320px; }
}

/* The three labels sit on the dark middle of the section, where a pale
   lavender read as barely there. White, and the plate behind them is
   a little stronger so the text has something to sit on. */
#ics .ics-step {
  color: #FFFFFF;
  background: rgba(255, 255, 255, .12);
  border-color: rgba(255, 255, 255, .24);
}

/* The solid pill this caption used to carry was for when it sat below
   the metric card, on the pale end of the gradient. It sits above the
   card now, on the dark band, so it takes the same frosted plate as
   the other two. */


/* ── Who built this · one card out, and the label a size up ──────────
   A card hidden by the attribute alone would still be laid out by the
   flex row, since [hidden] is only a UA rule.
   ════════════════════════════════════════════════════════════════════ */
#team .tm-card[hidden] { display: none; }

#team .tm-meta { width: 290px; }
#team .tm-card { padding-top: 88px; }
#team .tm-name { font-size: 18px; }
#team .tm-role { font-size: 13px; }

@media (max-width: 900px) {
  #team .tm-meta { width: 210px; }
  #team .tm-card { padding-top: 84px; }
  #team .tm-name { font-size: 16px; }
  #team .tm-role { font-size: 12px; }
}


/* ── Mobile · Why pulse. and the pixel steps ─────────────────────────
   Three things, all below 901px.

   The section carries more of its own background, so the diagram sits
   inside the dark band rather than filling it edge to edge.

   The cube and the metric card come down: 170 to 132 and 350x232 to
   304x202. The card keeps its proportion, because the recommendation
   inside it is sized in container units and would reflow against a
   different one.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics {
    padding-top: calc(var(--arc) + 44px);
    padding-bottom: calc(var(--arc) + 44px);
  }

  /* The lattice is sized by its own variables, not by the element box,
     and those were the desktop figures at every width, which is why it
     took 58% of a 390 phone. Both come down by about a third. */
  #ics .ics-cube.c3-rig          { --c3-gap: 42px; }
  #ics .ics-cube.c3-rig .c3-cube { --c3-cube: 27px; }
  #ics .ics-cube.c3-rig.is-static { width: 112px; height: 112px; }

  #ics .ics-orb-wrap .sm-metrics {
    width: 304px !important;
    max-width: 100% !important;
  }
  /* 210, not 202: the recommendation inside measures 203 at 304 wide,
     and a card shorter than its own content clips the buttons. */
  #ics .sm-card,
  #ics .sm-stack { height: 210px; }
}


/* The four chips under a step go two across and two down. They were
   one per row, which ran each step four rows deep. minmax(0, 1fr)
   rather than max-content so a long label wraps inside its own chip
   instead of pushing the column past the screen. */
@media (max-width: 900px) {
  #pixel .upz .upz-chips {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px;
  }
  #pixel .upz .upz-chips span {
    white-space: normal;
    text-align: center;
  }
}

/* Step 3 carries no chips, so its block came out 79px against 153 for
   the rest and the four steps sat on an uneven rhythm. A floor under
   the copy block evens them up without touching the taller ones. */
@media (max-width: 900px) {
  #pixel .upz .upz-txt { min-height: 152px; }
}

/* Below 340 the longer chip labels take a third line, so the floor
   rises with them. */
@media (max-width: 340px) {
  #pixel .upz .upz-txt { min-height: 176px; }
}


/* ── Mobile · the cube to the metric card, as one connector ──────────
   The old link was a single 1.39px bar hung off .ics-orb-wrap, 80px
   long against a 183px gap, and it ran straight through the "Unifies
   every signal" pill on its way down. Three measured segments instead,
   each anchored to the thing it leaves, so the chain reads

        cube  ->  ICS  ->  the pill  ->  the card

   The gaps are 37, 14 and 34px, identical at 430, 390 and 360, so the
   segments are those figures exactly. Two pixels wide rather than one
   and a bit, which is what made the old one look like a smudge.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics .ics-cube-label { position: relative; }

  #ics .ics-cube-label::before,
  #ics .ics-cube-label::after,
  #ics .ics-step-mid::after {
    content: '';
    position: absolute;
    left: 50%;
    width: 2px;
    margin-left: -1px;
    border-radius: 1px;
    background: #2C34D8;
  }

  /* 46, not 37: the cube spins slowly, so its painted bottom edge
     moves between about 38 and 44px above the label depending on the
     phase. The longer segment tucks under the artwork, which paints
     above it, so the join holds at every rotation. */
  #ics .ics-cube-label::before { bottom: 100%; height: 46px; }   /* from the cube  */
  #ics .ics-cube-label::after  { top: 100%;    height: 14px; }   /* on to the pill */
  #ics .ics-step-mid::after    { top: 100%;    height: 34px; }   /* on to the card */

  /* the bar this replaces */
  #ics .ics-orb-wrap::before { display: none; }
}


/* ── The gap · the sub copy goes back under the headline ─────────────
   It was set beside the headline in a two column head. Back in the one
   column the rest of the page uses, so the block reads top to bottom.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #compare .cmp-head-grid {
    display: block;
  }
  #compare .cmp-head-grid .section-sub {
    padding-top: 0;
    margin-top: 20px;
    max-width: 62ch;
  }
}


/* ── Who built this · a touch screen has no hover ────────────────────
   The name and role were revealed on hover, which on a phone means
   never. Where the pointer cannot hover they are simply always shown,
   and the lift that went with the hover is dropped so every figure
   reads the same. Desktop is untouched.
   ════════════════════════════════════════════════════════════════════ */
@media (hover: none) {
  /* Always on, so the label has to live inside its own card: at 210px
     against a 184px card it ran straight into the next person's name.
     The card takes side padding to make room, and the label is bound
     to the card's width. */
  #team .tm-card {
    padding-left: 15px;
    padding-right: 15px;
  }
  #team .tm-meta {
    width: 100%;
    padding: 0 4px;
    opacity: 1;
    transform: translate(-50%, 0);
  }
  #team .tm-photo {
    opacity: 1;
  }
}


/* ── The gap · the row labels join the table ─────────────────────────
   The five columns are white cards and the label column was bare page
   behind them, so the rows did not read as rows. It takes the same
   surface, border and radius, and the same row rules the columns use.
   ════════════════════════════════════════════════════════════════════ */
/* The section's own <style> sets background: inherit on this column and
   sits after this file, so the rule needs one more class to win. */
#compare .cmp .cmp-labels {
  background: #FFFFFF;
  border: 1px solid #E4E5FB;
  border-radius: 18px;
  overflow: hidden;
}
#compare .cmp-labels .cmp-r {
  padding-left: 18px;
  padding-right: 12px;
  border-bottom: 1px solid #F0F1FA;
}
#compare .cmp-labels .cmp-r:last-child { border-bottom: 0; }
#compare .cmp-labels .cmp-h {
  border-bottom: 1px solid #E4E5FB;
}


/* ── Why pulse. · the cube sits on the wire bundle ───────────────────
   The six wires converge on a lane anchored to the third source pill,
   not to the cube, and that is deliberate: the script's own note says
   anchoring the bundle on the cube left every wire about 29px to turn
   in, so the corners ate the whole run and it read as one S-curve.
   The line therefore stays where it is and the cube comes to it.

   Measured, the lane sits at y 216.5 of the 500 unit stage at every
   width from 901 up, and the cube's own top of 42.4% puts its centre
   at 212, so it rides 4.5px lower and the bundle runs dead through
   its middle.

   Desktop only. Below 901px the cube is in the column flow and the
   three connector segments under it are measured to the pixel.
   ════════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #ics .ics-cube-wrap {
    transform: translate(-50%, -50%) translateY(4.5px);
  }
}


/* ── The gap · six cards become one table ────────────────────────────
   The columns were separate rounded cards with 12px between them, so
   a row did not read across. One surface now: the grid carries the
   border, the radius and the line colour, and a 1px gap lets that
   colour through as the rule between columns. Done this way rather
   than with a border on each column because pulse. is reordered to
   the front below 901px, and a per column border would leave the rule
   on the wrong edge.
   ════════════════════════════════════════════════════════════════════ */
/* Via .cmp-wrap, because the section's own <style> sets gap on
   "#compare .cmp" and sits after this file. */
#compare .cmp-wrap .cmp {
  gap: 1px;
  background: #E4E5FB;
  border: 1px solid #E4E5FB;
  border-radius: 18px;
  overflow: hidden;
}

#compare .cmp .cmp-col,
#compare .cmp .cmp-labels {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* The row rules inside a column would double up with the grid lines
   at every join, so the horizontal ones stay and the outer edges go. */
#compare .cmp .cmp-col .cmp-r:last-child,
#compare .cmp .cmp-labels .cmp-r:last-child { border-bottom: 0; }

#compare .cmp .cmp-col.is-pulse { background: #1B1F7A; }


/* ── Mobile · the cube clears the wire bundle ────────────────────────
   The bundle from the six sources ended at y 618 and the cube's
   artwork began at 621, so the two were touching and the cube read as
   hanging off the line rather than sitting below it.

   The margin goes on the wrap, which carries the ICS label and the
   "Unifies every signal" pill with it, so the three connector segments
   underneath keep the 37, 14 and 34px they are measured to. It needs
   !important because an earlier mobile rule sets margin: 0 !important
   on this element.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics .ics-cube-wrap { margin-top: 34px !important; }
}


/* ── Mobile · the stem reaches, the cube is on top, ICS tucks under ──
   Three faults, all from the margin that opened the gap above the cube.

   1. The stem is a pseudo-element of .ics-cube-wrap at top: -30px, so
      the margin carried it down too and left a 34px void between the
      wires and the line. It starts 34 higher and runs 34 longer, so it
      meets the bundle exactly where the bundle ends.

   2. The cube takes an explicit layer above that stem, so the line
      disappears under the artwork instead of stopping against it.

   3. The ICS caption sits 5px under the cube. Measured across a full
      rotation the artwork's bottom edge swings 15px, so the caption is
      set against its LOWEST point: 5px clear at the closest, never
      touching. The old cube to caption segment goes, there being no
      room for a line in 5px, and the caption to pill segment grows by
      what the caption moved.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics .ics-cube-wrap::before {
    top: -64px;
    height: 76px;
    width: 2px;
    margin-left: -1px;
  }

  #ics .ics-cube.c3-rig.is-static { z-index: 2; }

  /* 42, down from the 70 an earlier rule sets with !important, which
     is why a plain value here did nothing. Measured, the artwork's
     lowest point sits 33px above the caption at that 70, so taking 28
     off leaves 5px at the closest part of the rotation.

     The pill and the card below are positioned off the wrap, and the
     wrap shrinks by the same 28, so both follow and the 14 and 34px
     segments under the caption are unaffected. */
  #ics .ics-cube-label { margin-top: 42px !important; }

  /* No line between the two now: 5px has no room for one. */
  #ics .ics-cube-label::before { content: none; }
}


/* ── The gap · the label column stops being a card ───────────────────
   It keeps its text and stays opaque, because it is sticky and the
   columns slide underneath it when the table is swiped: a transparent
   column would let them read straight through. Page colour rather than
   white, so it reads as the page the rows sit against instead of a
   sixth card, and no border or radius of its own.
   ════════════════════════════════════════════════════════════════════ */
#compare .cmp .cmp-labels {
  background: #F5F6FA;
  border: 0;
  border-radius: 0;
}
#compare .cmp .cmp-labels .cmp-h,
#compare .cmp .cmp-labels .cmp-r { border-bottom-color: #E4E5FB; }


/* The table clips its own corners so the columns sit inside one
   rounded surface. That is right where it fits, and wrong where it
   does not: below 901px the grid is 750px inside a 350px wrapper, and
   clipping it meant the four columns past pulse. could not be reached
   at all. The wrapper scrolls again there. */
@media (max-width: 900px) {
  #compare .cmp-wrap .cmp {
    overflow: visible;
    border-radius: 0;
    border-left: 0;
    border-right: 0;
  }
}

/* The label column was sticky, but a sticky grid item is held inside
   its own grid area, so it pinned for about 120px of swipe and then
   gave up and scrolled away with its text clipped mid word against the
   screen edge. Below 901px the table scrolls as one piece instead, so
   no column ever sits over the labels. */
@media (max-width: 900px) {
  #compare .cmp .cmp-labels { position: static; }
}


/* ── The gap · the label column gets room on a phone ─────────────────
   150px less 30 of padding left 120 for the text, so three of the five
   labels broke onto two lines and the second line ran right up to the
   first column. Wider, with more room on its right, and the rows grow
   to whatever the label needs rather than being held at 56.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #compare .cmp-wrap .cmp {
    grid-template-columns: 192px repeat(5, 118px);
  }
  /* A page coloured border acts as the space between the labels and the
     first column. A wider grid gap would have opened every join, and the
     table is meant to read as one surface. */
  #compare .cmp .cmp-labels { border-right: 14px solid #F5F6FA; }
  #compare .cmp .cmp-labels .cmp-r {
    padding-right: 18px;
    line-height: 1.3;
  }
  /* A two line label needs more than the fixed row height, and every
     column has to grow with it or the rows stop lining up. */
  #compare .cmp .cmp-labels .cmp-r,
  #compare .cmp .cmp-col .cmp-r {
    height: auto;
    min-height: var(--cmp-row);
  }
}


/* ── The gap · the header chips stop being clipped ───────────────────
   The chips are nowrap, so "Marketing agencies" measured 160px inside a
   118px column and "ChatGPT / Claude" 150, and both spilled past their
   column and were cut. They wrap now, which the 64px header row has
   room for: two lines at this size come to 44px including the padding.
   ════════════════════════════════════════════════════════════════════ */
/* Unscoped, because the columns are narrowest just above the mobile
   breakpoint: at 901 they are minmax(132px, 1fr) and the chip still
   overflowed by 21. Wrapping costs nothing where they already fit. */
#compare .cmp .cmp-chip {
  white-space: normal;
  line-height: 1.25;
  text-align: center;
  max-width: 100%;
}
@media (max-width: 900px) {
  #compare .cmp .cmp-chip { padding: 6px 10px; }
}


/* ── The gap · the pulse. mark reads on white ────────────────────────
   The chip was the brand blue carrying the white logo. White ground
   now, and the logo goes to solid black: there is only the one logo
   file and it is white, so brightness(0) flattens every channel to
   black while keeping the alpha, rather than shipping a second asset.
   ════════════════════════════════════════════════════════════════════ */
/* No capsule at all: the mark sits straight on the navy column, and the
   logo file is already white so it needs no filter. Through .is-pulse,
   because the section's own <style> sets the blue on
   "#compare .is-pulse .cmp-chip" and sits after this file. */
#compare .cmp .is-pulse .cmp-chip-logo {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
#compare .cmp .is-pulse .cmp-chip-logo img {
  filter: none;
}


/* ── Footer · the integrations list matches the other two columns ────
   Company and Solutions are anchors at rgba(255,255,255,.72). The
   integrations are plain spans, so they never picked that up and sat
   at #5A6180, a slate that all but disappears on the dark footer.
   ════════════════════════════════════════════════════════════════════ */
footer .footer-col li > span {
  color: rgba(255, 255, 255, .72);
}


/* ── Workspaces · the suite menu on a phone ──────────────────────────
   The five tabs wrapped as an inline run, which put two on the first
   line and one on each of the next three: four ragged rows. A two
   column grid gives them an even shape. Five into two columns is 2, 2
   and 1, since five cannot fill two rows at a legible size.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #workspaces .suite-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    align-items: stretch;
  }
  #workspaces .suite-list .suite-item {
    width: auto;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}


/* ── Universal pixel · step 3 fills its frame, copy sits at the top ──
   Step 3's crop was the 144x144 square the artwork was authored in,
   but the ink only occupies 106x111 of it, 74 by 77 per cent, where
   steps 1 and 2 fill theirs. That is why the stacked cards read a
   third smaller than everything else. Cropped to the ink instead, so
   the same box carries a noticeably larger drawing and the connector,
   which is derived from the icon WIDTHS, is untouched at 144.

   And the copy column was centring itself in the row, so a short block
   like Validate's sat 49px below the top of its illustration while the
   others sat 12. Every block starts at the top now.
   ════════════════════════════════════════════════════════════════════ */
#pixel .upz-a3 {
  width: 144px;
  height: 151px;
  background-size: 1695.4px 339.6px;
  background-position: -987.6px -95.1px;
}

@media (max-width: 900px) {
  #pixel .upz-a3 {
    width: 64px;
    height: 67px;
    background-size: 753.5px 150.9px;
    background-position: -438.9px -42.3px;
  }

  #pixel .upz .upz-txt { justify-content: flex-start; }
}


/* ── The gap · the header chips sit on one line ──────────────────────
   Two faults in that row on a phone.

   The padding was not uniform: "#compare .cmp-labels + .cmp-col
   .cmp-chip" gives 9px 18px to whichever column follows the labels in
   the MARKUP, and pulse. is reordered to the front below 901px, so the
   odd padding landed on a column that is no longer first on screen.
   All five take the same figure now, through a selector carrying one
   more class so it outranks that adjacent sibling rule.

   And the chips were 36, 47, 30, 47 and 36 tall, because each shrank
   to its own text, so the row read as a ragged line. A floor under all
   of them evens it up, and they centre their label whether it takes
   one line or two.
   ════════════════════════════════════════════════════════════════════ */
/* To 1100, not 900: the columns are narrowest just above the mobile
   breakpoint, and at 901 two of the labels still took a second line and
   left the row ragged by 17px. Above 1100 nothing wraps and the desktop
   chip is untouched. */
@media (max-width: 1100px) {
  #compare .cmp .cmp-h .cmp-chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 46: a two line label comes to 45.1 with this padding, so the
       floor has to clear it or those two chips stay a pixel taller. */
    min-height: 46px;
    padding: 5px 9px;
  }
}


/* ── Mobile · the two captions move above what they name ─────────────
   "Unifies every signal" sat under the cube and "Recommends the next
   move" under the metric card. Both now sit above, so each reads as a
   heading for the thing beneath it and the column runs

     wires -> Unifies -> cube -> ICS -> Recommends -> card

   The cube's wrapper carries more top margin, because the band between
   the wires and the artwork was only 33px and a caption is 36 tall.
   The stem is rebuilt around the caption rather than running past it.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics .ics-cube-wrap { margin-top: 65px !important; }

  #ics .ics-step-mid {
    top: -81px;
    bottom: auto;
  }
  #ics .ics-step-end {
    top: 32px;
    bottom: auto;
  }

  /* the stem, in two pieces around the first caption */
  #ics .ics-step-mid::before,
  #ics .ics-step-mid::after,
  #ics .ics-step-end::before,
  #ics .ics-step-end::after {
    content: '';
    position: absolute;
    left: 50%;
    width: 2px;
    margin-left: -1px;
    border-radius: 1px;
    background: #2C34D8;
  }
  #ics .ics-step-mid::before { bottom: 100%; height: 14px; }   /* wires -> caption */
  #ics .ics-step-mid::after  { top: 100%;    height: 14px; }   /* caption -> cube  */
  #ics .ics-step-end::before { bottom: 100%; height: 36px; }   /* ICS -> caption   */
  #ics .ics-step-end::after  { top: 100%;    height: 12px; }   /* caption -> card  */

  /* the pieces these replace */
  #ics .ics-cube-wrap::before  { content: none; }
  #ics .ics-cube-label::after  { content: none; }
}


/* ── Workspaces · the dashboard is readable on a phone ───────────────
   The screenshot is 920x504 and was being squeezed into 300x164, a
   third of its size, which put its figures below anything legible. It
   is drawn at 560 now and the frame scrolls sideways, so the numbers
   can be read by swiping rather than not at all.
   ════════════════════════════════════════════════════════════════════ */
/* .suite-screen is the class on the IMG, not on a frame around it. The
   frame is .suite-view, inside .suite-views. */
@media (max-width: 900px) {
  /* The revenue pane wraps its five screens in .suite-views, which sizes
     to the widest of them; the marketing pane has no such wrapper. Both
     are held to the card. */
  #workspaces .suite-grid { grid-template-columns: minmax(0, 1fr); }
  #workspaces .suite-views,
  #workspaces .suite-view {
    /* min-width defaults to auto on a grid item, so the track grew to
       the 560px screenshot instead of the screenshot scrolling inside
       the card. */
    min-width: 0;
    width: 100%;
    max-width: 100%;
  }
  #workspaces .suite-view {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    /* without this the frame sizes to the 560px image and pushes past
       the card instead of scrolling inside it */
    width: 100%;
    max-width: 100%;
  }
  #workspaces img.suite-screen {
    width: 560px;
    max-width: none;
    height: auto;
  }
}

/* The tab labels are nowrap, so "Groups and displacement" measured 154
   inside its 140px cell and spilled over its neighbours. They wrap
   inside the cell now, which is what makes the two columns read as a
   grid rather than a ragged run. */
@media (max-width: 900px) {
  #workspaces .suite-list .suite-item h3 {
    white-space: normal;
    overflow-wrap: break-word;
    text-align: center;
    line-height: 1.25;
  }
}


/* ── Mobile · the metric card drops clear of its caption ─────────────
   The overlap was not against the card's box but against the deck: the
   card behind the front one is offset up by 32, so the visible stack
   starts 21px above .sm-metrics and ran 9px into the caption. Fifty
   down, and the connector between them grows to match.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  /* !important because an earlier mobile rule sets margin: 0 auto
     !important on this element. */
  #ics .ics-orb-wrap .sm-metrics { margin-top: 30px !important; }
  #ics .ics-step-end::after { height: 21px; }
}


/* ── Workspaces · the dashboard fits, the tabs scroll ────────────────
   The screenshot went back to fitting the card, so the whole dashboard
   is on screen at once. The horizontal scroll moves to the tab strip
   instead: five tabs on one row that swipes, rather than a 2 by 3 grid
   that pushed the dashboard down the card.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #workspaces .suite-view {
    overflow-x: hidden;
  }
  #workspaces img.suite-screen {
    width: 100%;
    max-width: 100%;
  }

  #workspaces .suite-list {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    gap: 8px;
    padding-bottom: 4px;
    scrollbar-width: none;
  }
  #workspaces .suite-list::-webkit-scrollbar { display: none; }
  #workspaces .suite-list .suite-item {
    flex: 0 0 auto;
    width: auto;
  }
  #workspaces .suite-list .suite-item h3 {
    white-space: nowrap;
  }
}

/* The mark at half again its size: 22px was small against the column
   and the "by dhi Hospitality" line under it was unreadable. */
#compare .cmp .is-pulse .cmp-chip-logo img { height: 33px; }


/* ── The gap · the header row makes room for its chips ───────────────
   The row is 64px and the pulse. chip is 53 of it since the mark went
   up by half, so the capsule cleared the rule under it by 6px and read
   as touching. 80 gives the tallest chip 13 either side, and the text
   chips 17, and every column grows together because the labels share
   the same variable.
   ════════════════════════════════════════════════════════════════════ */
/* On #compare.section, because the section's own <style> declares
   --cmp-head on plain "#compare" and sits after this file. */
#compare.section { --cmp-head: 80px; }


/* ── Nav · the links sit on the page's centre line ───────────────────
   The bar is a three column grid, and the actions group is far wider
   than the wordmark, so once space tightened the first column gave way
   and the middle drifted: measured 14px off centre at 1150 and 89 at
   1000. Taking the links out of the flow and centring them on the bar
   fixes it where there is room for it.

   Only from 1100 up. Below that a centred row would run into the Login
   and Book a demo buttons by as much as 65px, because the band between
   the wordmark and the actions is not itself centred.
   ════════════════════════════════════════════════════════════════════ */
/* Not by taking the links out of the flow: that collapses the middle
   column and the buttons slide left into them. The drift comes from
   the two outer columns being unequal, the actions group needing more
   than its 1fr share once space tightens and the wordmark column
   giving way. Narrowing the row below 1200 buys back that share. */
@media (min-width: 1366px) {
  /* The outer columns floored at their own max-content, 201px for the
     wordmark against 311px for the two buttons, which pushed the row
     55px left of the bar's middle: 148px of air after the wordmark and
     32px before Login. Centring against the bar cannot work either,
     since 311px reserved on both sides plus a 767px row needs about
     1485px. So the middle column takes the whole span between the two,
     and the row centres inside that. Even air on both sides. */
  nav.nav { grid-template-columns: auto minmax(0, 1fr) auto; }
  nav.nav .nav-links {
    justify-self: stretch;
    justify-content: center;
    gap: 4px;
  }
  nav.nav .nav-link  {
    font-size: 13px;
    padding: 9px 10px;
    line-height: 1.2;
  }
}


/* ── Book a demo · the card's content sits centred in it ─────────────
   The padding was 115px over the block and 40 under it, so everything
   rode high. Even top and bottom now.
   ════════════════════════════════════════════════════════════════════ */
#demo .glass.final {
  padding-top: 72px;
  padding-bottom: 72px;
}
@media (max-width: 900px) {
  #demo .glass.final {
    padding-top: 56px;
    padding-bottom: 56px;
  }
}

/* Three tracks now the integrations column has gone, so the two lists
   spread across the footer instead of bunching left against an empty
   fourth column. The narrow breakpoints already collapse to 2 and 1. */
@media (min-width: 901px) {
  .footer-grid { grid-template-columns: 1.5fr 1fr 1fr; }
}


/* ── The problem · the three role labels read as words ───────────────
   They were uppercase with 1.82px of tracking, which is a setting for
   caps. In sentence case that tracking reads as gappy, so it comes
   back to the normal figure with it.
   ════════════════════════════════════════════════════════════════════ */
#problem .role-top .eyebrow {
  text-transform: none;
  letter-spacing: .01em;
}


/* ═══════════════════════════════════════════════════════════════════
   UNIVERSAL PIXEL  ·  the swipe stepper, under 901px only

   No wrappers were added around the steps. .upz-ico and .upz-txt are
   siblings, so the rail runs its columns in two rows: each step's icon
   lands in row 1 and its copy in row 2 of the same column. The card is
   those two halves sharing a border, the icon taking the top edge and
   the copy the bottom, with no row gap between them so the seam does
   not show. Shared grid rows give every card the same height for free.

   Everything here is inside the one media block. Desktop, the zig-zag
   and .upz-path are untouched.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {

  /* ── the rail ───────────────────────────────────────────────────── */
  /* Through .container and a doubled class: the section's own <style>
     declares grid-template-columns on "#pixel .upz" and sits after this
     file, and an earlier rule of mine used "#pixel .upz.upz". This has
     to outrank both. */
  #pixel .container .upz.upz {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 82vw;
    grid-template-columns: none;
    grid-template-rows: auto 1fr;
    column-gap: 12px;
    row-gap: 0;
    margin-top: 20px;

    /* bleeds to both screen edges, first card on the 16px gutter */
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    /* trailing gutter = whatever the last card leaves over, so it can still
       scroll all the way to the 16px left gutter and snap there */
    padding: 0 calc(18vw - 16px) 0 16px;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-padding-left: 16px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  #pixel .upz::-webkit-scrollbar { display: none; }
  #pixel .upz:focus-visible { outline: 2px solid #2B33C4; outline-offset: 4px; }

  /* the vertical connector belonged to the stacked list */
  #pixel .upz-ico::after { content: none; }

  /* ── the card, top half ─────────────────────────────────────────── */
  #pixel .container .upz .upz-ico {
    /* the old stacked list pinned both halves with
       "grid-column:1 !important; grid-row:auto !important" in the
       section's own <style>, which parked all four cards in one column.
       Same weight, so this has to win on specificity. */
    grid-row: 1 !important;
    grid-column: auto !important;
    order: 0;
    scroll-snap-align: start;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 132px;
    padding: 20px 20px 0;
    background: #FFFFFF;
    border: 1px solid #E4E5FB;
    border-bottom: 0;
    border-radius: 20px 20px 0 0;
  }

  /* The illustration draws straight onto the white card. It is a crop
     window onto one SVG sheet, so box, background-size and the negative
     offsets all have to scale together. These are the stacked-list
     figures at 1.5x, which suits an 82vw card. */
  #pixel .container .upz .upz-ico > .upz-art { flex: none; }
  #pixel .container .upz .upz-a1 {
    width: 96px; height: 87px;
    background-size: 471.8px 94.5px;
    background-position: -11.0px -2.0px;
  }
  #pixel .container .upz .upz-a2 {
    width: 96px; height: 93px;
    background-size: 1130.3px 226.4px;
    background-position: -375.9px -64.4px;
  }
  #pixel .container .upz .upz-a3 {
    width: 96px; height: 101px;
    background-size: 1130.3px 226.4px;
    background-position: -658.4px -63.5px;
  }

  /* Step 04 is a live lattice, not a sheet crop, so it scales by its
     vars and paints about 1.25x its box across and 1.39x down. At a
     96px box that overran the icon half and the white text half painted
     over the bottom row. Sized so the whole lattice sits in the band. */
  #pixel .container .upz .upz-ico > .upz-rig {
    flex: none;
    width: 72px; height: 72px;
    --c3-cube: 15px;
    --c3-gap: 22px;
    /* the projection paints downward, so the box has to ride high for
       the drawing itself to sit centred in the band */
    top: -10px;
  }

  /* ── the card, bottom half ──────────────────────────────────────── */
  #pixel .container .upz .upz-txt {
    grid-row: 2 !important;
    grid-column: auto !important;
    order: 0;
    /* the stacked list gave this a floor to even its rows up */
    min-height: 0 !important;
    min-width: 0;
    min-height: 0;
    padding: 16px 20px 20px;
    background: #FFFFFF;
    border: 1px solid #E4E5FB;
    border-top: 0;
    border-radius: 0 0 20px 20px;
    justify-content: flex-start;
  }

  /* the number reads as its place in the run */
  #pixel .container .upz .upz-n {
    display: inline-block;
    width: auto;
    height: auto;
    margin: 12px 0 0;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .04em;
    color: #2B33C4;
  }
  #pixel .container .upz .upz-n::after { content: " / 04"; }

  #pixel .upz .upz-t { margin-top: 6px; }

  /* chips wrap on their own rather than holding a 2 by 2 grid */
  #pixel .container .upz .upz-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    grid-template-columns: none !important;
  }
  #pixel .upz .upz-chips span { white-space: nowrap; }

  /* All four cards read the same: white, one border, no inner panel.
     Step 04 keeps the navy chips the section already gives .is-out. */

  /* ── the progress bar ───────────────────────────────────────────── */
  #pixel .upz-prog {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 6px;
    margin: 28px 0 0;
  }
  #pixel .upz-seg {
    height: 4px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: #E4E5FB;
    cursor: pointer;
    transition: background 220ms ease;
  }
  #pixel .upz-seg.is-on { background: #2B33C4; }
  #pixel .upz-seg:focus-visible { outline: 2px solid #2B33C4; outline-offset: 3px; }

  /* ── the hint ───────────────────────────────────────────────────── */
  #pixel .upz-hint {
    margin: 14px 0 0;
    font-size: 12px;
    text-align: center;
    color: #6F758C;
    transition: opacity 220ms ease;
  }
  #pixel .upz-hint.is-done { opacity: 0; }
}

/* Desktop never sees any of it. */
@media (min-width: 901px) {
  #pixel .upz-prog,
  #pixel .upz-hint { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  #pixel .upz { scroll-behavior: auto; }
  #pixel .upz-seg,
  #pixel .upz-hint { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   TEAM · the LinkedIn mark beside each name
   The name was a plain block, so the mark goes in a centred row with
   it rather than dropping to a line of its own.
   ═══════════════════════════════════════════════════════════════════ */
#team .tm-name {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
#team .tm-li {
  display: inline-flex;
  flex: none;
  color: #1B1F7A;
  opacity: .55;
  transition: opacity .18s var(--ease-out), color .18s var(--ease-out);
}
#team .tm-li:hover,
#team .tm-li:focus-visible { opacity: 1; color: #2B33C4; }
#team .tm-li:focus-visible { outline: 2px solid #2B33C4; outline-offset: 3px; border-radius: 3px; }
#team .tm-li svg { display: block; }

/* The meta panel is pointer-events:none so it never eats hover while it
   is invisible, and the reveal rule only put the opacity back. That left
   the mark unclickable at every moment it was on screen. It takes clicks
   for exactly as long as it is shown. */
#team .tm-card:hover .tm-meta,
#team .tm-card:focus-within .tm-meta { pointer-events: auto; }
#team .tm-li { cursor: pointer; }

@media (max-width: 900px) {
  #team .tm-name { gap: 5px; }
  #team .tm-li svg { width: 12px; height: 12px; }

  /* There is no hover on touch, so the panel is simply shown here. It
     has to take taps as well, and a 12px mark is far under a usable
     target, so the padding widens the hit area without moving anything:
     the negative margin gives the space straight back to the layout. */
  #team .tm-meta { pointer-events: auto; }
  #team .tm-li {
    padding: 8px;
    margin: -8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  #team .tm-li { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS · the recommendation priority pill
   .rc-p is red whatever it says, so a MEDIUM read as an alert. This
   drops it to the page's muted ink rather than inventing a third hue.
   ═══════════════════════════════════════════════════════════════════ */
#ics .rc-p.is-med {
  color: #6F758C;
  border-color: rgba(111, 117, 140, .35);
}


/* ═══════════════════════════════════════════════════════════════════
   ACCESSIBILITY PASS
   ═══════════════════════════════════════════════════════════════════ */

/* Skip link. Off screen until it takes focus, then it lands on the
   page rather than in a corner. */
.skip-link {
  position: absolute;
  left: 16px;
  top: -64px;
  z-index: 100;
  padding: 10px 16px;
  border-radius: 10px;
  background: #1B1F7A;
  color: #FFFFFF;
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  transition: top .18s var(--ease-out);
}
.skip-link:focus { top: 16px; }

/* Footer links were 20px of inline text, under the 24px a thumb needs.
   The padding buys the height; the negative margin gives the column
   its rhythm back so nothing moves. */
.footer-col a {
  display: inline-block;
  padding: 3px 0;
  margin: -3px 0;
}

/* The stepper bar reads as a 4px rule but has to be tappable, so the
   hit area is padded out and pulled back in. */
@media (max-width: 900px) {
  #pixel .upz-prog { margin-top: 18px; }
  #pixel .upz-seg {
    padding: 10px 0;
    margin: -10px 0;
    background-clip: content-box;
  }

  /* 8.5px is below what anyone can read on a phone. */
  #ics .ics-source { font-size: 11px; }
}

/* The priority pill sat at 3.9:1 on white, under AA for 9px text.
   Through .rc-top, because the section's own <style> sets the colour on
   "#ics .rc-p" and sits after this file. */
#ics .rc-top .rc-p { color: #C42A2E; border-color: rgba(196, 42, 46, .32); }
#ics .rc-top .rc-p.is-med { color: #5C6478; border-color: rgba(92, 100, 120, .38); }
#ics .rc-top .rc-p.is-low { color: rgb(120, 128, 150); border-color: rgba(120, 128, 150, 0.3); }
/* .rc-l.rc-type, not .rc-type: the section's inline <style> sets #ics .rc-l
   to grey at the same weight and later in the page, so the purple lost.
   #6659E2 is the cube's own colour (--line-rgb, and the tiles in dot.png). */
#ics .rc-l.rc-type { margin-bottom: 1.2cqw; font-size: calc(.72em + 1px); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; color: #6659e2; }
#ics .rc-note { font-size: 12px; color: rgba(255, 255, 255, 0.75); text-align: center; margin: 14px 0 0; position: relative; z-index: 2; }
/* On a phone the stack sits below the navy band, on the pale end of the
   gradient, where white text washed out. Half black there; desktop keeps
   the white, on the navy. */
@media (max-width: 900px) {
  #ics .rc-note { color: rgba(0, 0, 0, 0.5); }
}


/* ═══════════════════════════════════════════════════════════════════
   NAV · the menu on every width, at the right edge
   The bar keeps the sections a visitor jumps to; The problem and The
   gap live in the menu, which lists every section. The button was a
   phone and tablet control, first in the bar, with the drawer coming
   in from the left. It now sits after Book a demo on every width, so
   the drawer comes in from the right, the side it is opened from.

   On the desktop the bar already carries Login and Book a demo, so the
   drawer drops its own copies above 900px. Below that they stay, since
   the bar there has no Login.
   ═══════════════════════════════════════════════════════════════════ */
.nav-burger {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 40px; height: 40px;
  padding: 0 9px;
  border: 0; border-radius: 10px;
  background: transparent;
  cursor: pointer;
  flex: none;
}
.nav-burger span {
  display: block; height: 2px; border-radius: 2px;
  background: #1B1F7A;
}
.nav-burger:focus-visible { outline: 2px solid #2B33C4; outline-offset: 2px; }
body.nav-open .nav-burger span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
body.nav-open .nav-burger span:nth-child(2) { opacity: 0; }
body.nav-open .nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* Login and Book a demo stay the same width as each other; the button
   takes only its own 40px after them. */
.nav-actions {
  grid-template-columns: 1fr 1fr;
  grid-auto-columns: auto;
}
.nav-actions .nav-burger { width: 40px; }

/* Between 901 and 1365 the links are hidden, so without this the
   actions would drop into the middle column. */
@media (min-width: 901px) {
  .nav .nav-actions { grid-column: 3; }
}

.nav-scrim {
  display: block;
  position: fixed; inset: 0; z-index: 998;
  background: rgba(10, 15, 46, .38);
  opacity: 0;
  transition: opacity .24s var(--ease-out);
}
.nav-scrim.is-open { opacity: 1; }

.nav-drawer {
  display: flex;
  flex-direction: column;
  gap: 6px;
  position: fixed;
  top: 0; right: 0; bottom: 0; left: auto; z-index: 999;
  width: min(78vw, 320px);
  padding: 84px 22px 28px;
  background: #FFFAFA;
  box-shadow: 0 0 60px -16px rgba(10, 15, 46, .45);
  transform: translateX(100%);
  transition: transform .26s cubic-bezier(.22, .8, .28, 1);
  overflow-y: auto;
}
.nav-drawer.is-open { transform: translateX(0); }

.nav-drawer ul { list-style: none; margin: 0 0 20px; padding: 0; }
.nav-drawer li + li { margin-top: 2px; }
.nav-drawer a:not(.btn) {
  display: block;
  padding: 13px 12px;
  border-radius: 10px;
  font-size: 16px; font-weight: 500;
  color: #1B1F7A;
  text-decoration: none;
}
.nav-drawer a:not(.btn):hover { background: rgba(27, 31, 122, .06); }
.nav-drawer .btn { width: 100%; }

.nav-close {
  position: absolute; top: 18px; right: 16px;
  width: 38px; height: 38px;
  border: 0; border-radius: 10px;
  background: transparent;
  font-size: 26px; line-height: 1; color: #1B1F7A;
  cursor: pointer;
}

body.nav-open { overflow: hidden; }

@media (min-width: 901px) {
  .nav-drawer .btn { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .nav-drawer, .nav-scrim, .nav-burger span { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════
   TEAM · tighter, smaller, and moved by hand
   THE GAPS. Pill to title was 40px against 24 everywhere else in the
   block, and 136px of empty grid sat between the button and the first
   photo: 48 of margin plus an 88px band held for a label that only
   shows on hover. The band is cut to what the smaller label needs.

   SMALLER. The photos were 317px tall at 1440, which made the strip
   taller than the copy above it. They top out at 250 now, and the
   label drops a size with them.

   AKILA sits 5px lower than the others. The strip clips at its own
   bottom edge, so she is cropped flush with the row rather than
   hanging below it.

   BY HAND. The script sets .is-js and drives the strip itself, so the
   CSS animation stands down. Holding it pauses it; a drag or swipe
   moves it directly.
   ═══════════════════════════════════════════════════════════════════ */
/* The copy column is a flex column with a 24px gap, and the title
   carried 16px of margin on top of it. The gap goes, so each space is
   set once, here. */
#team > .container  { gap: 0; }
#team .about-title  { margin-top: 20px; }
#team .about-body   { margin-top: 16px; margin-bottom: 0; }
#team .container .btn { margin-top: 28px; }

#team .tm-marquee { margin-top: 24px; }
#team .tm-card    { padding-top: 64px; }
#team .tm-photo   { height: clamp(180px, 17vw, 250px); }
#team .tm-meta    { top: 6px; width: 240px; }
#team .tm-name    { font-size: 16px; }
#team .tm-role    { font-size: 12px; }

#team .tm-photo--low { transform: translateY(5px); }
#team .tm-card:hover .tm-photo--low,
#team .tm-card:focus-within .tm-photo--low { transform: translateY(-1px); }

#team .tm-marquee.is-js .tm-track {
  animation: none;
  will-change: transform;
}
#team .tm-marquee.is-js {
  cursor: grab;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
#team .tm-marquee.is-dragging { cursor: grabbing; }
#team .tm-marquee.is-js img { -webkit-user-drag: none; }

@media (max-width: 900px) {
  #team .tm-marquee { margin-top: 20px; }
  #team .tm-card    { padding-top: 60px; }
  #team .tm-photo   { height: clamp(150px, 38vw, 200px); }
  #team .tm-meta    { width: 180px; }
  #team .tm-name    { font-size: 14px; }
  #team .tm-role    { font-size: 11px; }
}

@media (hover: none) {
  #team .tm-meta { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  #team .tm-marquee.is-js { overflow: hidden; }
}


/* ═══════════════════════════════════════════════════════════════════
   THE WORDMARK · the nav must not squeeze it
   At 320px the bar ran out of room and the global img max-width:100%
   compressed the mark to 84.1px against 85.7 everywhere else. The
   height was right, so it read as very slightly narrower rather than
   smaller, which is the harder kind of drift to spot. The brand column
   holds its content width now and the mark is exempt from the cap, so
   it is 85.7 x 42 on every screen, matching the auth and onboarding
   pages.
   ═══════════════════════════════════════════════════════════════════ */
.nav .brand { flex: none; min-width: max-content; }
.brand-logo { max-width: none; }


/* ═══════════════════════════════════════════════════════════════════
   HOW IT COMPARES · the pulse. column heads with the logo as vector
   The mark is the white logo's own paths, inline, cropped to the
   artwork and filled with currentColor, so it is the logo exactly and
   stays sharp at any size and pixel density, where the PNG softened.
   Same 33px height the image had.
   ═══════════════════════════════════════════════════════════════════ */
#compare .cmp .is-pulse .cmp-chip-logo .cmp-logo {
  display: block;
  height: 33px;
  width: auto;
  color: #FFFFFF;
}


/* ═══════════════════════════════════════════════════════════════════
   HOW IT COMPARES · the sub copy goes to the right again
   Same two columns as the Why pulse. head: pill and headline on the
   left, the sub in the right column level with the headline. Phones
   keep the single column.
   ═══════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #compare .cmp-head-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    column-gap: clamp(var(--s-6), 5vw, var(--s-9));
    align-items: start;
  }
  #compare .cmp-head-grid .pill          { grid-column: 1; grid-row: 1; justify-self: start; }
  #compare .cmp-head-grid .section-title { grid-column: 1; grid-row: 2; }
  #compare .cmp-head-grid .section-sub {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    padding-top: 10px;     /* level with the Why pulse. sub, measured */
    max-width: 46ch;
  }
}


/* ═══════════════════════════════════════════════════════════════════
   HERO  ·  the ramp finishes below the buttons, not across them
   On a phone the hero is short and the CTA row sits low in it, so the
   gradient had already reached its pale stops by the time it got to
   Get early access. That button is a hairline outline and white text
   on whatever is behind it, which measured 1.80:1 at 390 and 1.60:1 at
   414: legible in theory, invisible in practice.

   Same fifteen colours, same order, same arc. Only the stop positions
   move, so the dark holds through the buttons and the ramp to page
   white is compressed into the space beneath them. h15 still lands at
   100%, which is what keeps the hero's bottom edge matching the strip
   below it with no seam.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .hero {
    background: radial-gradient(170% 160% at 50% 0%,
      var(--h01) 0%,    var(--h02) 26%, var(--h03) 38%,   var(--h04) 46%,
      var(--h05) 52%,   var(--h06) 57%, var(--h07) 61%,   var(--h08) 64%,
      var(--h09) 68%,   var(--h10) 72%, var(--h11) 76.5%, var(--h12) 81%,
      var(--h13) 86%,   var(--h14) 91%, var(--h15) 100%) !important;
  }
}


/* ═══════════════════════════════════════════════════════════════════
   NAV  ·  no hairline under the bar on a phone
   .nav.is-stuck draws a 1px rgba(10,15,46,.08) bottom border once the
   bar leaves the top. Over the hero it reads as a stray line across
   the full width, which is what it looks like on a phone where the bar
   is over artwork the whole way down. The bar keeps its blur and
   background, so it still separates from what scrolls under it.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .nav.is-stuck { border-bottom-color: transparent; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  the six sources, three to a row on a phone
   Six across a 350px row left each card 55px wide with 3px of side
   padding, so the labels ran edge to edge and "Ad Intelligence" was
   cut. Measured: at six across it only stops clipping at 9px or less,
   which is under the size anything should be read at.

   Three to a row gives each card 111px, so every label sits on one
   line at 11px with real padding on both sides and nothing trimmed.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  #ics .ics-sources {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 4px;
  }
  #ics .ics-source {
    width: auto;
    padding: 8px 6px;
    font-size: 11px;
  }

  /* The fan sat a full flex gap below the row, 30px, which read as the
     lines starting somewhere else rather than leaving the cards. The
     stage gap also spaces the fan from the cube, so only the fan moves
     up and the run into the rig is left alone. */
  #ics .ics-wires-m { margin-top: -18px; }
}


/* ═══════════════════════════════════════════════════════════════════
   ICS  ·  5px between a card and its data line
   The dot is the wire's anchor, so the gap between the card's right
   edge and the dot is the gap the line appears to start at. right:-22px
   on a 6px dot left 15px of air. This closes it to 5px. The wires are
   measured from the dots at run time, so the lines follow.
   ═══════════════════════════════════════════════════════════════════ */
@media (min-width: 901px) {
  #ics .ics-source .ics-dot { right: -11px; }
}


/* ═══════════════════════════════════════════════════════════════════
   NAV  ·  the stuck bar dissolves into what is under it
   Dropping the 1px border was not the line. The bar itself is
   rgba(245,246,250,.82) over an 18px backdrop blur, so on a phone it
   ends on a hard step: 206,208,219 at its last row against 9,14,46 in
   the hero four pixels later. A skirt of the same tint, fading to
   nothing just below the bar, carries the light down instead of
   cutting it. It sits under the bar's own content and takes no
   pointer events, so nothing in the bar moves or changes.
   ═══════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .nav.is-stuck::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    height: 28px;
    pointer-events: none;

    /* The bar's backdrop blur is what leaves the last visible edge: the
       tint can be ramped but the blur stops dead at the bar's bottom.
       Carrying the same blur into the skirt and masking it out with the
       same curve fades the blur as well as the colour. */
    -webkit-backdrop-filter: blur(18px) saturate(1.4);
    backdrop-filter: blur(18px) saturate(1.4);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0,0,0,.45) 45%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 0%, rgba(0,0,0,.45) 45%, transparent 100%);

    background: linear-gradient(to bottom,
      rgba(245, 246, 250, .82) 0%,
      rgba(245, 246, 250, .62) 18%,
      rgba(245, 246, 250, .38) 42%,
      rgba(245, 246, 250, .16) 68%,
      rgba(245, 246, 250, 0)   100%);
  }
}
