/* =====================================================================
   VivSuite — CINEMATIC edition
   Apple (elegant type, spacing, scroll storytelling)
   + Stripe (gradient bands, refined transitions)
   + Linear (crisp minimal components, subtle motion)
   ===================================================================== */

:root {
  /* single accent — used sparingly, the way Apple reserves color for one moment per view.
     Darkened from #6366f1 to clear WCAG AA 4.5:1 in every direction it's used:
     as link text on white (5.81:1) and on --bg-soft (5.33:1), and as the
     button fill behind white label text (5.81:1). The original failed all
     three at ~4.1-4.47:1. */
  --indigo: #4f52e0;

  --ink:      #1d1d1f;   /* Apple-style near-black for text + the one dark section */
  --text:     #1d1d1f;
  --text-soft:#6e6e73;
  /* was #86868b — only 3.62:1 on white, below the 4.5:1 AA threshold */
  --text-mut: #6e6e73;

  --bg:       #ffffff;
  --bg-soft:  #f5f5f7;
  --line:     rgba(0,0,0,.08);
  --line-2:   rgba(0,0,0,.13);

  --grad: var(--indigo);
  --grad-cool: var(--indigo);
  /* the accent is tuned for dark text on light ground; on --ink it only
     reaches 2.90:1, under the 3:1 large-text minimum. This lighter tint
     hits 5.64:1 there and is used wherever accent text sits on the dark
     sections (finale headline, 404 code). */
  --indigo-on-dark: #818cf8;

  --radius-sm: 14px;
  --radius:    20px;
  --radius-lg: 30px;

  --shadow-sm: 0 2px 14px rgba(0,0,0,.05);
  --shadow:    0 20px 50px rgba(0,0,0,.10);
  --shadow-lg: 0 40px 90px rgba(0,0,0,.18);

  --maxw: 1140px;
  --maxw-text: 820px;
  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --ease: cubic-bezier(.22, 1, .36, 1);
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 90px; -webkit-text-size-adjust: 100%; }
body {
  margin: 0; font-family: var(--font); color: var(--text); background: var(--bg);
  line-height: 1.6; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; overflow-x: hidden;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* Available to screen readers, removed from the visual layout. Used for
   things sighted users infer from styling but that need to be spoken —
   required-field markers, a form's accessible name, and so on. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Skip link (WCAG 2.4.1 Bypass Blocks): without it, keyboard and screen
   reader users have to tab through the whole header — and on the homepage
   all seven constellation nodes — before reaching page content. Visually
   hidden until focused, then pinned to the top-left. */
.skip-link { position: absolute; left: 12px; top: -60px; z-index: 200;
  background: var(--indigo); color: #fff; padding: 12px 20px; border-radius: 0 0 12px 12px;
  font-weight: 600; font-size: .95rem; transition: top .2s var(--ease); }
.skip-link:focus { top: 0; outline: 2px solid #fff; outline-offset: -4px; }

h1,h2,h3,h4 { font-family: var(--font-display); color: var(--ink); margin: 0; line-height: 1.06; letter-spacing: -.025em; font-weight: 600; }
h2 { font-size: clamp(2.1rem, 5.2vw, 4.2rem); letter-spacing: -.03em; font-weight: 600; }
h3 { font-size: 1.18rem; letter-spacing: -.015em; font-weight: 600; }
p { margin: 0; }

.container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: 28px; }
.grad-text { color: var(--indigo); }
/* on the dark sections the standard accent drops to 2.90:1 — use the
   lighter tint so accent headline text stays above the 3:1 minimum */
.band .grad-text, .finale .grad-text { color: var(--indigo-on-dark); }

.kicker {
  display: inline-block; font-family: var(--font-display); font-weight: 600; font-size: .8rem;
  letter-spacing: .16em; text-transform: uppercase; color: var(--indigo); margin-bottom: 16px;
}

/* scroll progress (Linear-style hairline) */
.scroll-progress { position: fixed; top: 0; left: 0; height: 2px; width: 0; z-index: 100; background: var(--grad); box-shadow: 0 0 12px rgba(168,85,247,.6); }

/* scroll-to-top: fixed bottom-right, appears once scrolled past the hero */
.scroll-top {
  position: fixed; right: clamp(16px, 3vw, 28px); bottom: clamp(16px, 3vw, 28px); z-index: 90;
  width: 46px; height: 46px; border-radius: 50%; border: 0; cursor: pointer;
  display: grid; place-items: center; background: var(--ink); color: #fff; box-shadow: var(--shadow);
  opacity: 0; transform: translateY(12px) scale(.9); pointer-events: none;
  transition: opacity .25s var(--ease), transform .25s var(--ease), background .2s;
}
.scroll-top.show { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.scroll-top:hover { background: var(--indigo); }
.scroll-top:focus-visible { outline: 2px solid var(--indigo); outline-offset: 3px; }
.scroll-top svg { width: 20px; height: 20px; }
@media (max-width: 680px) { .scroll-top { width: 42px; height: 42px; } }

/* ---------- Buttons ---------- */
.btn {
  --py: 13px; --px: 24px;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--font-display); font-weight: 600; font-size: .98rem;
  padding: var(--py) var(--px); border-radius: 999px; border: 1px solid transparent; cursor: pointer;
  transition: opacity .2s var(--ease), background .2s, color .2s, border-color .2s; white-space: nowrap;
}
.btn-sm { --py: 9px; --px: 18px; font-size: .88rem; }
.btn-lg { --py: 16px; --px: 30px; font-size: 1.02rem; }
.btn-block { width: 100%; }
.btn-primary { background: var(--indigo); color: #fff; }
/* .84 opacity looked fine by eye, but on a white background it dilutes
   the button to #6b6ee5 — 4.21:1 for the white label text, under WCAG
   AA's 4.5:1 minimum (found via axe-core, not spotted visually; on the
   dark finale/footer backgrounds the same opacity still clears 6.9:1+,
   so this was only ever a problem on white). .90 keeps a visible hover
   lightening while staying at 4.77:1. */
.btn-primary:hover { opacity: .90; }
.btn-line { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn-line:hover { border-color: var(--ink); }
.btn-line-light { background: rgba(255,255,255,.08); color: #fff; border-color: rgba(255,255,255,.35); backdrop-filter: blur(6px); }
.btn-line-light:hover { background: rgba(255,255,255,.16); }

/* ---------- Header (Linear minimal) ---------- */
.site-header { position: sticky; top: 0; z-index: 60; background: rgba(255,255,255,.72); backdrop-filter: saturate(160%) blur(16px); border-bottom: 1px solid transparent; transition: border-color .3s, background .3s; }
.site-header.scrolled { border-color: var(--line); }
.nav { display: flex; align-items: center; justify-content: space-between; height: 70px; }
.brand { display: inline-flex; align-items: center; gap: 10px; }
/* real product logo; object-fit guards against the source PNG's aspect ratio
   stretching the mark if it is ever swapped for a non-square file */
.brand-mark { width: 40px; height: 40px; object-fit: contain; flex: none; transition: transform .3s var(--ease); }
.brand:hover .brand-mark { transform: scale(1.06); }
.brand-name { font-family: var(--font-display); font-weight: 700; font-size: 1.22rem; letter-spacing: -.02em; color: var(--ink); }
.nav-links { display: flex; align-items: center; gap: 30px; }
.nav-links > a { font-weight: 500; font-size: .94rem; color: var(--text-soft); transition: color .2s; }
.nav-links > a:hover { color: var(--ink); }
.nav-links .nav-cta { color: #fff; }
/* secondary action, distinct from both the plain nav links and the primary
   CTA — a light bordered pill so it still reads as clickable at a glance */
.nav-login { font-weight: 600; color: var(--ink) !important; border: 1px solid var(--line-2);
  border-radius: 999px; padding: 8px 16px; transition: border-color .2s, background .2s; }
.nav-login:hover { border-color: var(--ink); background: var(--bg-soft); }
.nav-toggle { display: none; flex-direction: column; gap: 5px; background: none; border: 0; cursor: pointer; padding: 8px; }
.nav-toggle span { width: 24px; height: 2px; background: var(--ink); border-radius: 2px; transition: .3s var(--ease); }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* =====================================================================
   HERO — cinematic
   ===================================================================== */
.hero { position: relative; text-align: center; padding: clamp(70px, 12vh, 150px) 0 0; overflow: hidden; }
/* Spans the full viewport rather than a fixed centre island, so the colour reaches
   the left and right edges on wide screens. The blooms are pushed out past the
   content column and overscan horizontally so no hard edge shows at the sides. */
.hero-aura { position: absolute; top: -10%; left: 50%; transform: translateX(-50%); width: 118vw; height: 820px; z-index: -2;
  background: radial-gradient(46% 48% at 50% 28%, rgba(99,102,241,.14), transparent 72%);
  filter: blur(18px); }
.hero-grid { position: absolute; inset: 0 0 -40% 0; z-index: -1;
  background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 64px 64px; -webkit-mask: radial-gradient(70% 60% at 50% 30%, #000, transparent 75%); mask: radial-gradient(70% 60% at 50% 30%, #000, transparent 75%); opacity: .5; }

/* flowing gradient ribbon border — a continuously-moving indigo/violet/sky/
   pink stream around the whole outline, picked deliberately for this one
   hero accent rather than the site's usual single-indigo restraint */
.pill {
  display: inline-block; font-family: var(--font-display); font-weight: 500; font-size: .84rem; color: var(--text-soft);
  padding: 8px 16px; border-radius: 999px; backdrop-filter: blur(8px); margin-bottom: 30px;
  border: 1.5px solid transparent;
  background:
    linear-gradient(rgba(255,255,255,.82), rgba(255,255,255,.82)) padding-box,
    linear-gradient(90deg, #6366f1, #a855f7, #0ea5e9, #ec4899, #6366f1) border-box;
  background-size: 100% 100%, 300% 100%;
  animation: ribbonFlow 5s linear infinite;
}
.hero-title { font-size: clamp(2.6rem, 7vw, 5.6rem); line-height: 1.0; letter-spacing: -.045em; max-width: 16ch; margin: 0 auto; }
.hero-lead { max-width: 60ch; margin: 28px auto 0; color: var(--text-soft); font-size: clamp(1.05rem, 1.8vw, 1.3rem); }
.hero-actions { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; margin-top: 36px; }

/* =====================================================================
   HERO — interactive app constellation (neural core)
   Nodes are real anchors: clicking one scrolls to that app's card in #apps
   ===================================================================== */
/* Its own section between "Why VivSuite" and the tour, so it just needs
   normal section rhythm rather than the viewport-height offset it used when
   it sat inside the hero below the headline block. */
/* overflow:hidden is load-bearing, not cosmetic: .hero-stage-bg and its aura
   are deliberately far wider than the viewport (~2090px) to spread the glow,
   and relied on the hero's own clipping when this block lived inside it.
   Without it here they widen the scroll box and the whole page scrolls
   sideways at every width. */
.constellation-section { position: relative; overflow: hidden;
  padding: clamp(48px, 8vw, 96px) 0 clamp(56px, 9vw, 110px); }
.hero-stage { position: relative; max-width: 960px; margin: 0 auto; padding-inline: 28px; }
.constellation-hint { font-family: var(--font-display); font-weight: 500; font-size: .82rem; letter-spacing: .04em;
  color: var(--text-mut); text-align: center; margin-bottom: 4px; position: relative; z-index: 1; }

/* Backdrop for the globe area only — the headline/CTA block above has its own
   .hero-aura/.hero-grid and is untouched. This is a separate, bolder layer
   scoped to .hero-stage so the plain white space around the orbit gets some
   life without changing anything above "Click any app to explore it". */
.hero-stage-bg { position: absolute; z-index: 0; pointer-events: none;
  top: -60px; left: 50%; transform: translateX(-50%);
  width: 145vw; height: calc(100% + 140px);
}
.hero-stage-aura { position: absolute; inset: 0;
  background:
    radial-gradient(30% 34% at 20% 24%, rgba(99,102,241,.4), transparent 68%),
    radial-gradient(26% 30% at 82% 20%, rgba(236,72,153,.32), transparent 66%),
    radial-gradient(32% 36% at 50% 55%, rgba(14,165,233,.26), transparent 70%),
    radial-gradient(24% 28% at 70% 84%, rgba(168,85,247,.28), transparent 65%);
  filter: blur(52px) saturate(130%);
  animation: heroStageDrift 18s ease-in-out infinite alternate;
}
@keyframes heroStageDrift {
  0%   { transform: scale(1) rotate(0deg); }
  100% { transform: scale(1.1) rotate(3deg); }
}
.hero-stage-grid { position: absolute; inset: 0;
  background-image: linear-gradient(rgba(99,102,241,.1) 1px, transparent 1px), linear-gradient(90deg, rgba(99,102,241,.1) 1px, transparent 1px);
  background-size: 56px 56px;
  mask: radial-gradient(48% 62% at 50% 46%, #000, transparent 82%);
  -webkit-mask: radial-gradient(48% 62% at 50% 46%, #000, transparent 82%);
  opacity: .85;
}
@media (prefers-reduced-motion: reduce) { .hero-stage-aura { animation: none; } }

/* Stage box: everything inside is positioned against a fixed 760x680 frame, then the
   whole frame is scaled as one unit. The wrapper reserves the *scaled* height (a
   transform doesn't shrink layout size) so the constellation stays centred and the
   page below it doesn't inherit a 760px-wide box on small screens. */
.net-wrap { width: 100%; height: calc(680px * var(--net-scale, 1)); display: flex; justify-content: center; }
.net { position: relative; width: 760px; height: 680px; flex: none;
  transform: scale(var(--net-scale, 1)) translateY(24px); transform-origin: top center;
  opacity: 0; transition: transform .9s var(--ease), opacity .9s var(--ease); }
.hero-stage.in .net { transform: scale(var(--net-scale, 1)) translateY(0); opacity: 1; }

/* ---- beams: hidden at rest, only the hovered app's beam lights up ---- */
.beams { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; z-index: 1; pointer-events: none; }
.beam { stroke: url(#beamg); stroke-width: 1.6; fill: none; opacity: 0; transition: opacity .25s, stroke-width .25s; }
.beam.active { opacity: 1; stroke-width: 2.6; stroke: var(--indigo); filter: drop-shadow(0 0 4px rgba(99,102,241,.7)); }
.pulse { stroke: var(--indigo); stroke-width: 4.5; stroke-linecap: round; fill: none; opacity: 0;
  filter: drop-shadow(0 0 5px var(--indigo)); stroke-dasharray: 14 2000; stroke-dashoffset: 2000; }
.pulse.active { opacity: 1; animation: beamFire .7s linear infinite; }
@keyframes beamFire { from { stroke-dashoffset: 2000 } to { stroke-dashoffset: 1860 } }

/* ---- orbit rings ---- */
.ring { position: absolute; top: 50%; left: 50%; border-radius: 50%; transform: translate(-50%,-50%); border: 1px solid rgba(99,102,241,.22); }
.r1 { width: 470px; height: 470px; }
.r2 { width: 640px; height: 640px; border-color: rgba(14,165,233,.16); }
.scan { display: none; }

/* ---- the core (VivSuite logo) ---- */
.core-wrap { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 3;
  width: 260px; height: 260px; display: flex; align-items: center; justify-content: center; }
.aura { position: absolute; width: 300px; height: 300px; border-radius: 50%;
  background: radial-gradient(circle, rgba(99,102,241,.16), rgba(99,102,241,.06) 40%, transparent 68%); }
.halo { position: absolute; width: 220px; height: 220px; border-radius: 50%;
  background: radial-gradient(circle, rgba(99,102,241,.22), transparent 72%); }
.energy { position: absolute; border-radius: 50%; border: 1.5px solid var(--line-2); }
.e1 { width: 172px; height: 172px; border-style: dashed; }
.e2 { width: 138px; height: 138px; }
.core { position: relative; width: 118px; height: 118px; border-radius: 50%; background: #fff;
  border: 1px solid var(--line-2); display: flex; align-items: center; justify-content: center;
  box-shadow: 0 14px 30px -10px rgba(0,0,0,.18); transition: box-shadow .3s var(--ease); }
.net:hover .core { box-shadow: 0 0 40px 6px rgba(99,102,241,.28), 0 14px 30px -10px rgba(0,0,0,.2); }
.core img { width: 74px; max-height: 74px; height: auto; object-fit: contain; }
.spark { display: none; }

/* ---- app nodes (anchors) ---- */
.app-node { position: absolute; left: 50%; top: 50%; width: 120px; margin-left: -60px; margin-top: -48px;
  z-index: 3; text-align: center; cursor: pointer; color: inherit; }
.app-node .ic { width: 76px; height: 76px; margin: 0 auto; border-radius: 50%; background: #fff; border: 1px solid var(--line-2);
  display: flex; align-items: center; justify-content: center; position: relative; z-index: 4; overflow: hidden;
  box-shadow: var(--shadow-sm); transition: transform .25s var(--ease), border-color .25s, box-shadow .25s; }
.app-node .ic img { width: 46px; height: 46px; object-fit: contain; transition: transform .25s var(--ease); }
.app-node b { display: inline-block; font-family: var(--font-display); font-size: .84rem; font-weight: 600; margin-top: 10px;
  position: relative; z-index: 5; background: var(--bg); padding: 2px 10px; border-radius: 999px; transition: color .25s, background .25s, box-shadow .25s; }
.app-node .tag { display: block; margin: 7px auto 0; max-width: 158px; font-size: .72rem; line-height: 1.4; color: var(--text-soft);
  background: #fff; border: 1px solid var(--line); border-radius: 12px; padding: 6px 10px; position: relative; z-index: 5;
  opacity: 0; transform: translateY(-4px); transition: opacity .22s, transform .22s; pointer-events: none; box-shadow: var(--shadow-sm); }
/* one shared indigo/purple/sky treatment for every app node on hover —
   not a per-app --c color anymore, so all seven nodes read the same way,
   matching the "Why VivSuite" pillar bar's palette */
.app-node:hover .ic, .app-node:focus-visible .ic { transform: scale(1.12); border-color: color-mix(in srgb, var(--indigo) 70%, transparent);
  box-shadow: 0 0 26px -2px color-mix(in srgb, var(--indigo) 40%, transparent), 0 0 26px -2px color-mix(in srgb, #a855f7 30%, transparent); }
.app-node:hover .ic img { transform: scale(1.06); }
.app-node:hover b, .app-node:focus-visible b { color: var(--indigo); background: #fff; box-shadow: var(--shadow-sm); }
.app-node:hover .tag, .app-node:focus-visible .tag { opacity: 1; transform: translateY(0); }
.app-node:focus-visible { outline: none; }
.app-node:focus-visible .ic { outline: 2px solid var(--indigo); outline-offset: 3px; }

/* was a purely decorative, non-interactive hint before — now a real button
   that scrolls to the next section, so it isn't a dead click target. Same
   flowing gradient ribbon border as .pill, so the two read as a pair. */
.scroll-cue { display: block; width: 26px; height: 42px; padding: 0; background: none; cursor: pointer;
  border-radius: 14px; margin: 40px auto 0; position: relative;
  border: 2px solid transparent;
  /* .hero has zero bottom padding of its own — this cue is the only thing
     separating it from #why below, so it needs its own trailing space;
     every other section already has generous bottom padding built in */
  background:
    linear-gradient(#fff, #fff) padding-box,
    linear-gradient(90deg, #6366f1, #a855f7, #0ea5e9, #ec4899, #6366f1) border-box;
  background-size: 100% 100%, 300% 100%;
  animation: ribbonFlow 5s linear infinite; transition: transform .2s var(--ease), animation-duration .2s; }
.hero .scroll-cue { margin-bottom: 70px; }
.scroll-cue:hover { transform: scale(1.08); animation-duration: 2s; }
.scroll-cue:focus-visible { outline: 2px solid var(--indigo); outline-offset: 3px; }
.scroll-cue span { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); width: 4px; height: 8px; border-radius: 2px; background: var(--text-mut); animation: cue 1.6s ease-in-out infinite; }
/* on dark sections a white fill would look like a stray blob — swap the
   inner fill and the dot color to match the dark ground instead */
.band .scroll-cue, .finale .scroll-cue {
  background: linear-gradient(var(--ink), var(--ink)) padding-box,
    linear-gradient(90deg, #6366f1, #a855f7, #0ea5e9, #ec4899, #6366f1) border-box;
  background-size: 100% 100%, 300% 100%;
}
.band .scroll-cue span, .finale .scroll-cue span { background: rgba(255,255,255,.6); }
@keyframes cue { 0%,100%{ opacity:0; transform: translate(-50%,0) } 40%{ opacity:1 } 80%{ opacity:0; transform: translate(-50%,12px) } }
@keyframes ribbonFlow { to { background-position: 0 0, -300% 0; } }
@media (prefers-reduced-motion: reduce) { .pill, .scroll-cue { animation: none; } }

/* ---------- Sections ---------- */
/* Roomier than before: Apple's premium feel comes largely from letting each
   section breathe rather than from decoration. */
.section { padding: clamp(96px, 13vw, 180px) 0; position: relative; }
/* Anchor ids live on this heading, not on .section — its own scroll-margin-top
   keeps jumps landing right under the sticky header instead of at the
   section's top padding edge (which would leave 90-180px of dead space
   above the heading). */
.section-head { max-width: var(--maxw-text); margin: 0 auto clamp(56px,8vw,96px); text-align: center; scroll-margin-top: 40px; }
.section-sub { color: var(--text-soft); font-size: clamp(1.08rem,1.5vw,1.24rem); margin-top: 20px; }

/* solid near-black, matching the one accent-restrained dark tone used across
   the app stage and finale — no colored mesh, one continuous palette */
.band { color: #fff; background: var(--ink); isolation: isolate; }
.band > .container { position: relative; z-index: 1; }
.band h2, .band h3 { color: #fff; }
.band .section-sub { color: rgba(255,255,255,.7); }
.band .kicker { color: rgba(255,255,255,.6); }

/* =====================================================================
   JOURNEY ("Why VivSuite") — the site's central sales argument
   Four stages of running a firm, told as one continuous story (Chase ->
   Plan -> Perform -> Collect) rather than a flat feature grid. This is
   deliberately the most art-directed section on the page: it's the
   moment that has to make a visitor believe the platform, not just list
   what it does. A dashed connecting path implies the thread running
   between stages; cards reveal in sequence (staggered, not at once) as
   the section scrolls into view; a full-bleed dark "payoff band" closes
   the story on an emotional beat instead of another card. ---------- */
/* Both top AND bottom padding are much smaller than a normal .section:
   this sits directly under the hero (whose own .scroll-cue already adds
   70px of trailing space) and directly above .constellation-section
   (which has its own clamp(48px,8vw,96px) top padding) — stacking this
   section's full padding on top of either neighbor's own spacing left a
   large dead gap on both sides. The payoff band's own dark background
   already reads as a clear visual break, so it doesn't need much extra
   whitespace under it to separate from what comes next. */
.journey { position: relative; padding: clamp(24px, 3vw, 48px) 0 clamp(32px, 4vw, 64px); overflow: hidden; isolation: isolate; }
.journey-glow {
  position: absolute; inset: -10% -10% auto -10%; height: 700px; z-index: -1; pointer-events: none;
  background: radial-gradient(60% 60% at 50% 0%, rgba(79,82,224,.09), transparent 70%);
}
.journey-head { max-width: var(--maxw-text); margin: 0 auto 40px; text-align: center; }
.journey-head p { margin-top: 20px; color: var(--text-soft); font-size: clamp(1.05rem,1.6vw,1.22rem); max-width: 58ch; margin-inline: auto; }

/* ---- the connecting path: a single thread the eye follows stage to stage ---- */
.journey-path { position: relative; margin-top: clamp(56px, 7vw, 96px); }
.journey-path-svg { position: absolute; top: 60px; left: 0; width: 100%; height: 120px; z-index: 0; overflow: visible; }
.journey-path-line { fill: none; stroke: url(#journeyPathGrad); stroke-width: 2.5; stroke-dasharray: 8 10; stroke-linecap: round; }

.journey-stages { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; }
.journey-stage { opacity: 0; transform: translateY(28px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.journey-stage.in { opacity: 1; transform: none; }

.journey-card {
  position: relative; height: 100%; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 30px 24px 28px; box-shadow: var(--shadow-sm);
  transition: transform .35s var(--ease), box-shadow .35s var(--ease), border-color .35s var(--ease);
}
.journey-card:hover { transform: translateY(-6px); box-shadow: var(--shadow); border-color: var(--line-2); }
.journey-card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 22px; }
.journey-tag {
  font-family: var(--font-display); font-weight: 800; font-size: .74rem; letter-spacing: .04em;
  color: #fff; background: var(--stage-tag-c, var(--indigo)); padding: 5px 11px; border-radius: 999px; white-space: nowrap;
}
.journey-ic {
  width: 44px; height: 44px; border-radius: 13px; display: grid; place-items: center; flex: none;
  background: color-mix(in srgb, var(--stage-c, var(--indigo)) 12%, white);
  border: 1.5px solid color-mix(in srgb, var(--stage-c, var(--indigo)) 35%, white);
}
.journey-ic svg { width: 22px; height: 22px; stroke: var(--stage-c, var(--indigo)); }
.journey-card h3 { font-size: 1.32rem; margin-bottom: 8px; }
.journey-card p { color: var(--text-soft); font-size: .94rem; }

/* four stages, four accent colors — same family the rest of the site
   already uses for its one multi-color gradient moment (indigo/violet/
   sky/emerald), so this doesn't introduce a new palette */
/* --stage-c drives the icon/border (decorative use — sits against a
   light tint of itself, not white text, so full saturation is fine).
   --stage-tag-c drives the pill's solid background behind white text —
   3 of the 4 base hues fail 4.5:1 as white-on-color at full saturation
   (purple 3.96:1, sky 2.77:1, green 2.28:1 — measured, not assumed), so
   the pill uses a darkened copy of the same hue instead. */
.journey-stage:nth-child(1) .journey-card { --stage-c: var(--indigo); --stage-tag-c: var(--indigo); }
.journey-stage:nth-child(2) .journey-card { --stage-c: #a855f7; --stage-tag-c: #8b21e0; }
.journey-stage:nth-child(3) .journey-card { --stage-c: #0ea5e9; --stage-tag-c: #0369a1; }
.journey-stage:nth-child(4) .journey-card { --stage-c: #22c55e; --stage-tag-c: #15803d; }

/* ---- payoff band: the emotional close, not another card ---- */
.journey-payoff {
  margin-top: clamp(64px, 8vw, 110px); background: var(--ink); border-radius: var(--radius-lg);
  padding: clamp(50px,7vw,90px) clamp(28px,6vw,80px); text-align: center; position: relative; overflow: hidden;
}
.journey-payoff::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(50% 80% at 50% 100%, rgba(79,82,224,.35), transparent 70%);
}
.journey-payoff-line {
  position: relative; z-index: 1; font-family: var(--font-display); font-weight: 700;
  font-size: clamp(1.6rem, 3.4vw, 2.6rem); color: #fff; letter-spacing: -.02em; line-height: 1.22;
  max-width: 22ch; margin: 0 auto;
}
.journey-payoff .grad-text { color: var(--indigo-on-dark); }
.journey-payoff-sub { position: relative; z-index: 1; margin-top: 16px; color: rgba(255,255,255,.65); font-size: 1rem; }

@media (max-width: 980px) {
  .journey-stages { grid-template-columns: 1fr 1fr; }
  .journey-path-svg { display: none; }
}
@media (max-width: 640px) {
  .journey-stages { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
  .journey-stage { transition: none; opacity: 1; transform: none; }
  .journey-card { transition: none; }
  .journey-card:hover { transform: none; }
}

/* ---------- Tour (Wrike-style tabbed walkthrough) ----------
   Tabs across the top (icon + name, underline fills + auto-advances);
   below, a two-column panel per app — copy on the left, an animated
   abstract product mockup on the right, themed with --c (the app's own
   accent, set inline per tab/panel). */
/* This section carries the product screenshots, so it earns more room than
   the site's default rhythm: a wider container, tighter vertical padding,
   and less gap under the heading. Left at the default it sat in a 1140px
   column with ~180px of padding above and below and ~96px under the
   heading, which read as a small panel floating in white space. */
.tour-section { overflow: visible; padding: clamp(64px, 7vw, 104px) 0 clamp(56px, 6vw, 88px); }
.tour-section > .container { max-width: 1320px; }
.tour-section .section-head { margin-bottom: clamp(34px, 4vw, 52px); }
.tour { position: relative; }

.tour-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: none;
  margin-bottom: clamp(40px, 6vw, 64px); }
.tour-tabs::-webkit-scrollbar { display: none; }
.tour-tab { position: relative; flex: none; display: flex; align-items: center; gap: 10px;
  padding: 14px 18px 16px; background: none; border: none; cursor: pointer;
  font-family: var(--font-display); font-size: 1.02rem; font-weight: 600; color: var(--text-soft);
  transition: color .2s var(--ease); }
/* object-fit: contain, not just a fixed box — the source icons aren't all
   square (v-fleet is 1536x1024, v-contacts 2274x1851, v-planner 1970x1904),
   so a plain width/height box was stretching them out of proportion */
.tour-tab img { width: 20px; height: 20px; object-fit: contain; opacity: .6; transition: opacity .2s var(--ease); }
.tour-tab span { white-space: nowrap; }
.tour-tab[aria-selected="true"] { color: var(--ink); }
.tour-tab[aria-selected="true"] img { opacity: 1; }
.tour-tab:hover { color: var(--ink); }
.tour-tab:focus-visible { outline: 2px solid var(--indigo); outline-offset: 2px; border-radius: 8px; }
/* the underline: a full-width dim track, with a colored fill shown solid
   the instant a tab becomes active — switching is manual only (a click),
   so there's no fill-over-time animation to run here. */
.tour-tab::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--line-2); }
/* one shared indigo/purple/sky bar for every app's tab underline — not a
   per-app --c color, so all seven tabs fill with the same gradient,
   matching the "Why VivSuite" pillar bar's palette */
.tour-tab-fill { position: absolute; left: 0; bottom: -1px; height: 2px; width: 0;
  background: linear-gradient(90deg, var(--indigo), #a855f7, #0ea5e9); z-index: 1; }
.tour-tab[aria-selected="true"] .tour-tab-fill { width: 100%; }

/* 40/60 split: the screenshot is the point of this section, so it gets
   more of the row than the copy does. */
/* 46/54 rather than 40/60: the copy column holds two side-by-side bullet
   lists, and at 40% they were narrow enough that almost every bullet wrapped
   to a second line. The screenshot still gets the larger half. */
.tour-panel { display: grid; grid-template-columns: 46fr 54fr; gap: clamp(28px, 3.4vw, 48px); align-items: center; }
/* grid children default to min-width:auto, which refuses to shrink below
   the content's intrinsic width (here, .tour-mock-row-text's white-space:
   nowrap) — without this the mock card blew past the column and caused
   horizontal page overflow on narrow screens. Same fix pattern used
   elsewhere on this site for the same trap. */
.tour-panel > * { min-width: 0; }
/* the plain [hidden] attribute selector and .tour-panel{display:grid} carry
   equal specificity, and this rule loads later — without this override the
   hidden panels stayed laid out (display: grid beat display: none) and every
   app's panel rendered stacked on the page at once. */
.tour-panel[hidden] { display: none; }

/* ---- coming-soon panel: a placeholder, not a two-column write-up ----
   V Invoice (and any future unshipped app) has no screenshot and no real
   feature list yet, so it gets a single centered block instead of forcing
   content into the normal .tour-copy/.tour-visual split — an empty
   screenshot frame would read as broken, not "coming soon". */
.tour-panel-soon { grid-template-columns: 1fr; padding: clamp(40px, 6vw, 64px) 0; }
.tour-soon { max-width: 480px; margin: 0 auto; text-align: center; display: flex; flex-direction: column; align-items: center; }
.tour-soon-ic { width: 72px; height: 72px; border-radius: 20px; display: grid; place-items: center;
  background: var(--bg-soft); border: 1px solid var(--line); margin-bottom: 22px; }
.tour-soon-ic img { width: 40px; height: 40px; object-fit: contain; }
/* --c-badge, not --c or --c-text: the raw accent at full saturation fails
   4.5:1 as text on its own tint background (measured 4.01:1 for V
   Invoice's red on a 12%-over-white tint), and --c-text is only safe
   against plain white, not this badge's actual tinted background.
   --c-badge is main.js's textSafeAccent() computed specifically against a
   14%-over-var(--bg-soft) tint — same formula the tour tabs' status
   badges already use — so the background below matches that same 14%/
   --bg-soft mix, not an independent 12%/white one. */
.tour-soon-badge { display: inline-block; font-family: var(--font-display); font-weight: 700; font-size: .74rem;
  letter-spacing: .1em; text-transform: uppercase; color: var(--c-badge, var(--indigo));
  background: color-mix(in srgb, var(--c, var(--indigo)) 14%, var(--bg-soft));
  border: 1px solid color-mix(in srgb, var(--c, var(--indigo)) 30%, white);
  padding: 5px 14px; border-radius: 999px; margin-bottom: 16px; }
.tour-soon h3 { font-size: clamp(1.5rem, 2.4vw, 1.9rem); margin-bottom: 12px; }
.tour-soon p { color: var(--text-soft); font-size: 1.02rem; line-height: 1.65; }

/* fixed indigo, not a per-app --c-text color — every app's eyebrow label
   reads the same shared accent instead of its own hue */
.tour-copy-eyebrow { display: block; font-family: var(--font-display); font-weight: 600; font-size: .8rem;
  letter-spacing: .12em; text-transform: uppercase; color: var(--indigo); margin-bottom: 12px; }
.tour-panel h3 { font-size: clamp(1.6rem, 2.6vw, 2.1rem); letter-spacing: -.02em; margin-bottom: 14px; }
.tour-panel > .tour-copy > p { color: var(--text-soft); font-size: 1.05rem; line-height: 1.65; margin-bottom: 24px; }
/* two labelled groups side by side: what it does, and what you get out of it */
.tour-lists { display: grid; grid-template-columns: 1fr 1fr; gap: 26px 24px; }
.tour-lists > * { min-width: 0; }
.tour-list h4 { font-family: var(--font-display); font-size: .72rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-mut);
  margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.tour-benefits { list-style: none; padding: 0; margin: 0; display: grid; gap: 11px; }
.tour-benefits li { display: flex; align-items: flex-start; gap: 8px; font-size: .9rem;
  line-height: 1.45; color: var(--text); font-weight: 500; }
.tour-benefits li svg { flex: none; width: 16px; height: 16px; margin-top: 2px; color: var(--indigo); }
/* the two lists stack before the panel itself does — side by side they get
   too narrow to keep each bullet on one line */
@media (max-width: 1080px) { .tour-lists { grid-template-columns: 1fr; gap: 22px; } }
.tour-jump-link { border-color: var(--line-2); }
.tour-jump-link:hover { border-color: var(--indigo); color: var(--indigo); }

/* ---- the screenshot frame ----
   Same fixed size for every app, regardless of that app's own screenshot's
   native dimensions — switching tabs must not resize the layout around it.
   The frame is reduced to a hairline border (no title bar, no traffic-light
   dots, no logo) since the screenshot itself is the point of this section;
   object-fit: contain keeps every screenshot fully visible with no
   cropping inside that fixed box, letterboxed on whichever axis is needed.
   Apps with 2+ screenshots crossfade between them on a loop (see
   renderTour()'s setInterval) — .tour-shot stacks every frame absolutely
   and only the one with .in is opaque. */
.tour-visual { display: flex; justify-content: center; }
/* .tour-mock is a <button>, not a <div> — it opens the lightbox — so browser
   button chrome (padding, font, border-style) is reset back to how the
   plain frame looked before. */
.tour-mock { position: relative; width: 100%; max-width: 900px; border-radius: 10px; overflow: hidden;
  background: var(--bg); border: 1px solid var(--line); box-shadow: var(--shadow);
  padding: 0; font: inherit; cursor: zoom-in; display: block;
  transition: box-shadow .2s var(--ease), transform .2s var(--ease); }
.tour-mock:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
.tour-mock:focus-visible { outline: 2px solid var(--indigo); outline-offset: 3px; }
.tour-mock-shots { position: relative; aspect-ratio: 8 / 5; background: var(--bg-soft); }
.tour-shot { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; object-position: center;
  opacity: 0; transition: opacity .6s var(--ease); }
.tour-shot.in { opacity: 1; }
/* small zoom-in badge, bottom-right — only appears on hover/focus so it
   doesn't clutter the screenshot the rest of the time */
.tour-mock-zoom { position: absolute; right: 14px; bottom: 14px; width: 34px; height: 34px; border-radius: 50%;
  background: rgba(17,17,17,.72); color: #fff; display: grid; place-items: center;
  opacity: 0; transform: scale(.85); transition: opacity .2s var(--ease), transform .2s var(--ease); }
.tour-mock-zoom svg { width: 16px; height: 16px; }
.tour-mock:hover .tour-mock-zoom, .tour-mock:focus-visible .tour-mock-zoom { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .tour-shot { transition: none; }
  .tour-mock, .tour-mock-zoom { transition: none; }
  .tour-mock:hover { transform: none; }
}

/* ---- V Forms-only carousel: video first, then manually-paged screenshots ----
   Same outer frame size as the regular .tour-mock (max-width 900px, 8:5
   box) so switching between V Forms and any other tab doesn't jump the
   layout, but navigation is explicit arrows instead of an auto-timer —
   the video slide is a real playing embed, not something to crossfade. */
.tour-carousel { position: relative; width: 100%; max-width: 900px; }
.tour-carousel-track { position: relative; aspect-ratio: 8 / 5; border-radius: 10px; overflow: hidden;
  background: var(--bg); border: 1px solid var(--line); box-shadow: var(--shadow); }
.tour-carousel-slide { position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none; transition: opacity .35s var(--ease); }
.tour-carousel-slide.in { opacity: 1; pointer-events: auto; }
/* video slide isn't a <button> (nothing to zoom on a playing embed), so it
   needs its own sizing instead of inheriting .tour-mock's button reset */
.tour-carousel-video iframe { width: 100%; height: 100%; border: 0; display: block; }
/* screenshot slides reuse .tour-mock for the zoom-cursor/hover-badge/
   focus-ring styling, but position: absolute here (stacking in the track)
   overrides .tour-mock's own position: relative; max-width/width are
   dropped too since the track, not this button, now sets the frame size */
.tour-carousel-slide.tour-mock { position: absolute; max-width: none; width: 100%; box-shadow: none; border: 0; }
.tour-carousel-slide.tour-mock img { width: 100%; height: 100%; object-fit: contain; object-position: center; display: block; }
.tour-carousel-arrow { position: absolute; top: 50%; translate: 0 -50%; z-index: 2;
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--line-2);
  background: rgba(255,255,255,.92); backdrop-filter: blur(4px); color: var(--ink);
  display: grid; place-items: center; cursor: pointer; box-shadow: var(--shadow-sm);
  transition: background .2s var(--ease), transform .2s var(--ease); }
.tour-carousel-arrow:hover { background: #fff; transform: translateY(-50%) scale(1.06); }
.tour-carousel-arrow:focus-visible { outline: 2px solid var(--indigo); outline-offset: 2px; }
.tour-carousel-arrow svg { width: 20px; height: 20px; }
.tour-carousel-arrow.prev { left: 14px; }
.tour-carousel-arrow.next { right: 14px; }
.tour-carousel-arrow[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .tour-carousel-slide { transition: none; }
  .tour-carousel-arrow { transition: none; }
  .tour-carousel-arrow:hover { transform: translateY(-50%); }
}
@media (max-width: 860px) {
  .tour-carousel { max-width: 100%; }
}
@media (max-width: 520px) {
  .tour-carousel-arrow { width: 36px; height: 36px; }
  .tour-carousel-arrow svg { width: 18px; height: 18px; }
  .tour-carousel-arrow.prev { left: 8px; }
  .tour-carousel-arrow.next { right: 8px; }
}

@media (max-width: 860px) {
  .tour-panel { grid-template-columns: 1fr; }
  .tour-visual { order: -1; }
  .tour-mock { max-width: 100%; }
}
@media (max-width: 520px) {
  .tour-tab { padding: 12px 14px 14px; font-size: .92rem; }
  .tour-tab span { display: none; }
  .tour-tab img { width: 22px; height: 22px; object-fit: contain; }
}

/* ---- screenshot lightbox ----
   Full-screen overlay, one shared instance for all 7 apps. Click anywhere
   on it — backdrop, image, or the close button — closes it; there's no
   distinct "inside vs outside" click zone by design. */
.tour-lightbox { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center;
  padding: clamp(24px, 5vw, 64px); background: rgba(10,10,14,.92); cursor: zoom-out;
  animation: lightboxIn .18s var(--ease); }
@keyframes lightboxIn { from { opacity: 0; } to { opacity: 1; } }
.tour-lightbox[hidden] { display: none; }
/* the stage is sized to the actual screenshot's aspect ratio (set inline as
   --lb-ratio once the image loads — see openTourLightbox()) and capped to
   the available viewport space; .tour-lightbox-img frames stack inside it
   absolutely so multi-screenshot apps can crossfade the same way the
   in-page thumbnail does, instead of freezing on one static frame. */
.tour-lightbox-stage { position: relative; max-width: 100%; max-height: 100%;
  width: min(100%, calc((100vh - 128px) * var(--lb-ratio, 16 / 10)));
  aspect-ratio: var(--lb-ratio, 16 / 10); border-radius: 8px; overflow: hidden; box-shadow: var(--shadow-lg); }
.tour-lightbox-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  opacity: 0; transition: opacity .5s var(--ease); cursor: zoom-out; background: #0b0b10; }
.tour-lightbox-img.in { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .tour-lightbox-img { transition: none; }
}
/* z-index needed: on narrow viewports the stage's own width calculation
   (min(100%, ...)) pushes its right edge into the same top-right corner
   as this button, and with both this and .tour-lightbox-stage sitting at
   z-index:auto, DOM order alone decided paint order — the stage (which
   comes after this button in the markup) was covering it, making the
   close button visible but unclickable on phone-width screens. */
.tour-lightbox-close { position: absolute; top: clamp(14px, 3vw, 28px); right: clamp(14px, 3vw, 28px); z-index: 2;
  width: 42px; height: 42px; border-radius: 50%; border: none; background: rgba(255,255,255,.1);
  color: #fff; display: grid; place-items: center; cursor: pointer; transition: background .2s var(--ease); }
.tour-lightbox-close:hover { background: rgba(255,255,255,.2); }
.tour-lightbox-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.tour-lightbox-close svg { width: 18px; height: 18px; }
@media (prefers-reduced-motion: reduce) {
  .tour-lightbox { animation: none; }
}

/* these are wide, landscape desktop screenshots — on a narrow phone
   viewport, max-width:100% caps them to the small amount of side padding
   left after the phone's own width, well before max-height ever becomes
   the limit, so the "full screen" view rendered smaller than the in-page
   thumbnail did. Cut padding to near zero on narrow screens so the image
   actually gets to use the width it has. */
@media (max-width: 560px) {
  .tour-lightbox { padding: 10px 10px 60px; align-items: flex-start; }
  .tour-lightbox-img { margin-top: 44px; }
}

/* ---------- Finale ---------- */
.finale { position: relative; overflow: hidden; text-align: center; padding: clamp(110px,16vw,200px) 0; background: var(--ink); color: #fff; }
.finale-aura { position: absolute; inset: -20%; z-index: 0;
  background: radial-gradient(46% 50% at 50% 40%, rgba(99,102,241,.28), transparent 65%);
  filter: blur(28px); }
.finale-inner { position: relative; z-index: 1; scroll-margin-top: 40px; }
.finale h2 { font-size: clamp(2.4rem,6vw,4.6rem); color: #fff; letter-spacing: -.03em; font-weight: 600; }
.finale p { color: rgba(255,255,255,.78); font-size: clamp(1.1rem,1.8vw,1.35rem); margin: 22px auto 38px; }
.finale-actions { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }

/* ---------- Contact ---------- */
/* min-width:0 on the grid/flex children below stops them refusing to shrink
   past their content's intrinsic width — without it the form's inputs and
   the long address line push the whole section past the viewport on very
   narrow screens (320px), causing horizontal page scroll */
.contact { display: grid; grid-template-columns: .9fr 1.1fr; gap: 64px; align-items: start; }
.contact > * { min-width: 0; }
.contact-copy { scroll-margin-top: 40px; }
.contact-points { list-style: none; padding: 0; margin: 30px 0 0; display: flex; flex-direction: column; gap: 14px; }
.contact-points li { display: flex; align-items: center; gap: 14px; font-weight: 500; color: var(--ink); min-width: 0; }
.contact-points li > :not(.ci) { min-width: 0; overflow-wrap: anywhere; }
.ci { width: 40px; height: 40px; border-radius: 12px; color: #fff; display: grid; place-items: center; background: var(--ink); flex: none; }
.contact-form { background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 36px; box-shadow: var(--shadow-sm); }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.field-row > * { min-width: 0; }
.field { margin-bottom: 16px; display: flex; flex-direction: column; min-width: 0; }
.field input, .field select, .field textarea { max-width: 100%; }
.field label { font-family: var(--font-display); font-weight: 500; font-size: .86rem; color: var(--ink); margin-bottom: 7px; }
.field input, .field select, .field textarea { font-family: var(--font); font-size: .98rem; color: var(--text); padding: 13px 15px; border: 1px solid var(--line-2); border-radius: 12px; background: var(--bg-soft); transition: border-color .2s, box-shadow .2s, background .2s; resize: vertical; }
/* A real 2px outline, not just a 14%-alpha glow: the old ring was far too
   faint to serve as the visible focus indicator WCAG 2.4.7/2.4.11 require.
   The soft halo stays underneath as decoration. */
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--indigo); background: #fff; box-shadow: 0 0 0 4px rgba(79,82,224,.22); }
.field input:focus-visible, .field select:focus-visible, .field textarea:focus-visible { outline: 2px solid var(--indigo); outline-offset: 2px; }
/* a field the script marked invalid gets a visible border too, so the
   error isn't communicated by the message text alone */
.field input[aria-invalid="true"] { border-color: #c1123c; box-shadow: 0 0 0 3px rgba(193,18,60,.15); }
.form-note { margin-top: 14px; font-size: .92rem; font-weight: 600; color: var(--indigo); text-align: center; min-height: 1.2em; }

/* ---------- Footer ---------- */
.site-footer { background: var(--ink); color: rgba(255,255,255,.66); padding-top: 72px; }
.footer-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1.2fr; gap: 44px; padding-bottom: 52px; }
.site-footer .brand-name { color: #fff; }
.footer-brand p { margin-top: 16px; font-size: .94rem; max-width: 36ch; color: rgba(255,255,255,.55); }
/* h2, not h4: these label sections inside the footer landmark, and jumping
   straight from the page's h2s to an h4 skipped a level (WCAG 1.3.1 /
   heading-order). Visual size is set here, so the tag change is invisible. */
.footer-col h2 { color: #fff; font-size: .8rem; letter-spacing: .12em; text-transform: uppercase; margin-bottom: 18px; margin-top: 0; font-weight: 600; }
/* :not(.btn) keeps this from overriding the "Start free trial" button's own
   display/padding — it's a .btn living inside a .footer-col, not a plain link */
.footer-col a:not(.btn) { display: block; color: rgba(255,255,255,.6); font-size: .94rem; padding: 5px 0; transition: color .2s; }
.footer-col a:not(.btn):hover { color: #fff; }
.footer-col .btn { margin-top: 2px; }
.footer-cta-text { font-size: .94rem; margin-bottom: 16px; color: rgba(255,255,255,.6); }
/* .55 alpha (not .45) so the copyright line clears 4.5:1 against --ink */
.footer-bottom { display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap; padding: 24px 0; border-top: 1px solid rgba(255,255,255,.1); font-size: .86rem; color: rgba(255,255,255,.55); text-align: center; }

/* ---------- Scroll reveal (cinematic) ---------- */
.reveal { opacity: 0; transform: translateY(38px) scale(.985); transition: opacity .9s var(--ease), transform .9s var(--ease); }
.reveal.in { opacity: 1; transform: none; }
/* Without JS the IntersectionObserver never runs, so every .reveal element
   would stay at opacity 0 — i.e. most of the page's content permanently
   invisible. <html> carries .no-js until main.js removes it, so the
   scroll-reveal is opt-in on top of content that is visible by default. */
.no-js .reveal { opacity: 1; transform: none; transition: none; }

/* ---------- Responsive ---------- */
@media (max-width: 980px) {
  .contact { grid-template-columns: 1fr; gap: 44px; }
  .footer-grid { grid-template-columns: 1fr 1fr; gap: 36px; }
}
@media (max-width: 680px) {
  .nav-toggle { display: flex; }
  .nav-links { position: fixed; inset: 70px 14px auto 14px; flex-direction: column; align-items: stretch; gap: 4px;
    background: rgba(255,255,255,.92); border: 1px solid var(--line); border-radius: var(--radius); padding: 16px 18px 22px; box-shadow: var(--shadow); backdrop-filter: blur(18px);
    transform: translateY(-140%); transition: transform .35s var(--ease); visibility: hidden; }
  .nav-links.open { transform: translateY(0); visibility: visible; }
  .nav-links > a { padding: 12px 6px; border-bottom: 1px solid var(--line); }
  .nav-links .nav-cta { margin-top: 10px; border-bottom: 0; }
  /* the pill border reads better as its own row than mixed in with the
     bottom-border list style used by the plain text links */
  .nav-login { border-bottom: 0; margin-top: 10px; text-align: center; padding: 10px 16px; }
  .field-row { grid-template-columns: 1fr; gap: 0; }
  .footer-grid { grid-template-columns: 1fr; }
  .footer-bottom { justify-content: center; text-align: center; }
}
@media (max-width: 440px) {
  .container { padding-inline: 18px; }
  /* 36px of padding each side is a big share of a ~320px viewport — tighten
     it so the fields keep a usable width */
  .contact-form { padding: 22px 18px; }
}
/* the hero aura is wider than the viewport on very narrow screens; .hero clips it,
   but it still widens the scroll box unless it is taken out of flow measurement */
@media (max-width: 380px) { .hero-aura { width: 100%; } }

/* --- constellation: scale the whole 760x680 stage as one unit ---
   Its visual width is the ring plus one node either side (~700px of the 760px frame),
   so each step keeps 700 x scale inside the viewport with room for the page gutter. */
@media (max-width: 900px) { .hero-stage { --net-scale: .84; } }
@media (max-width: 760px) { .hero-stage { --net-scale: .72; padding-inline: 12px; } }
@media (max-width: 620px) { .hero-stage { --net-scale: .60; } }
@media (max-width: 520px) { .hero-stage { --net-scale: .50; } }
@media (max-width: 430px) { .hero-stage { --net-scale: .42; } }
@media (max-width: 370px) { .hero-stage { --net-scale: .38; } }
/* on touch screens the tagline can't be hovered — keep the labels clean */
@media (hover: none) { .app-node .tag { display: none; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
  .reveal { opacity: 1; transform: none; }
  .net { opacity: 1; transform: scale(var(--net-scale, 1)); }
}

/* =====================================================================
   LEGAL PAGES (Terms of Use / Privacy Policy)
   Own header/footer markup identical to index.html for visual consistency;
   the body is a two-column layout — a sticky section jump-list beside the
   long-form text — using the same tokens as the rest of the site.
   ===================================================================== */
.legal-hero { padding: clamp(64px, 9vw, 110px) 0 clamp(32px, 5vw, 56px); text-align: center; }
.legal-eyebrow { display: inline-block; font-family: var(--font-display); font-weight: 600; font-size: .8rem;
  letter-spacing: .16em; text-transform: uppercase; color: var(--indigo); margin-bottom: 16px; }
.legal-hero h1 { font-size: clamp(2.2rem, 5vw, 3.6rem); letter-spacing: -.03em; }
.legal-updated { margin-top: 14px; font-family: var(--font-display); font-size: .86rem; color: var(--text-mut); }
.legal-intro { max-width: 720px; margin: 24px auto 0; color: var(--text-soft); font-size: 1.02rem; line-height: 1.7; text-align: left; }
.legal-intro p + p { margin-top: 14px; }
.legal-intro a { color: var(--indigo); text-decoration: underline; text-underline-offset: 2px; }

.legal-layout { display: grid; grid-template-columns: 240px 1fr; gap: 56px; align-items: start;
  padding-bottom: clamp(80px, 10vw, 140px); }
.legal-toc { position: sticky; top: 100px; display: flex; flex-direction: column; gap: 2px; }
.legal-toc-label { font-family: var(--font-display); font-weight: 600; font-size: .72rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-mut); margin-bottom: 12px; }
.legal-toc a { display: block; padding: 8px 12px; margin-left: -12px; border-radius: 10px; font-size: .9rem;
  color: var(--text-soft); transition: background .2s, color .2s; }
.legal-toc a:hover { background: var(--bg-soft); color: var(--ink); }

.legal-body { max-width: 720px; color: var(--text-soft); font-size: 1rem; line-height: 1.75; }
.legal-body h2 { font-size: 1.4rem; letter-spacing: -.015em; color: var(--ink); margin: 44px 0 14px; scroll-margin-top: 100px; }
.legal-body h2:first-child { margin-top: 0; }
.legal-body h3 { font-size: 1.08rem; color: var(--ink); margin: 22px 0 10px; }
.legal-body p { margin: 0 0 14px; }
.legal-body ul { margin: 0 0 14px; padding-left: 22px; display: flex; flex-direction: column; gap: 8px; }
.legal-body li { line-height: 1.6; }
.legal-body a { color: var(--indigo); text-decoration: underline; text-underline-offset: 2px; }
.legal-body strong { color: var(--ink); font-weight: 600; }
.legal-body code { font-family: monospace; font-size: .92em; background: var(--bg-soft); padding: 1px 6px; border-radius: 5px; }
.legal-contact-card { margin-top: 8px; padding: 20px 24px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-soft); font-style: normal; line-height: 1.8; }

@media (max-width: 900px) {
  .legal-layout { grid-template-columns: 1fr; gap: 32px; }
  .legal-toc { position: static; flex-direction: row; flex-wrap: wrap; gap: 8px; }
  .legal-toc-label { width: 100%; margin-bottom: 4px; }
  .legal-toc a { margin-left: 0; padding: 7px 14px; border: 1px solid var(--line-2); border-radius: 999px; }
  .legal-body { max-width: none; }
}
@media (max-width: 480px) {
  .legal-hero { padding-top: 48px; }
  .legal-body h2 { font-size: 1.24rem; margin: 34px 0 12px; }
}
