/* Collie landing page.
   Layout and copy structure follow Sean's FigJam. Type, colour, shape and motion
   are the product's own tokens from BRAND.md, so the page sits next to a Collie
   embed without looking like a different company built it. */

:root {
  --n0:   #FFFFFF;
  --n50:  #FAFAFA;
  --n100: #F5F5F5;
  --n150: #EBEBEB;
  --n200: #E5E5E5;
  --n400: #A3A3A3;
  --n500: #737373;
  --n600: #525252;
  --n900: #0A0A0A;

  --blue:      #2563EB;
  --blue-bg:   #EFF6FF;
  --blue-lift: #93C5FD;   /* the link blue, lifted enough to read on near-black */
  --green:     #059669;

  --radius: 8px;
  --border: 1px solid var(--n200);

  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  --t-fast: 150ms;
  --t-base: 220ms;
  --t-slow: 300ms;
  --t-reveal: 560ms;

  --shell: 60rem;
  --gutter: 1.25rem;
  --band-y: 4.5rem;

  /* Three measures, by role. Display type takes a short line, smaller headings
     a slightly longer one, body copy a comfortable one. Everything on the page
     picks one of these rather than inventing its own, which is what left the
     h2, its subhead and its bullets all stopping at different places. */
  --measure-title: 26ch;
  --measure-subtitle: 34ch;
  --measure-body: 50ch;

  /* Rhythm between blocks. Spacing inside a component (card padding, gaps
     between list items) is its own business and is not on this scale. */
  --space-1: 0.75rem;
  --space-2: 1.25rem;
  --space-3: 1.75rem;
  --space-4: 2.5rem;

  /* The embed insets its own card, by an amount that scales with the width it
     is given. Matching it puts the label and small print on the card's edge
     rather than the invisible iframe's. */
  --card-inset: 18px;

  /* The product scales its root size fluidly; everything else is in rem. */
  font-size: clamp(16px, 0.83vw + 0.15rem, 22px);
}

*, *::before, *::after { box-sizing: border-box; }

/* Author-level display rules otherwise beat the UA stylesheet's [hidden]. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--n0);
  color: var(--n900);
  font-family: "Figtree", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 1rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

img, video, svg { display: block; max-width: 100%; }
a { color: var(--blue); }
ul { margin: 0; padding: 0; list-style: none; }

h1, h2 {
  margin: 0;
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1.08;
  text-wrap: balance;
}

/* ---------- reveal on scroll ----------
   Gated on .js, set by an inline script in the head. A browser without JS, or
   one that never runs the observer, gets the finished page rather than a blank
   one. Opacity and transform only: nothing here can reflow the document. */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--t-reveal) var(--ease),
              transform var(--t-reveal) var(--ease);
}

.js [data-reveal].is-in { opacity: 1; transform: none; }

/* ---------- layout primitives ---------- */

.shell {
  width: 100%;
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.band { padding: var(--band-y) 0; }
.band-n50 { background: var(--n50); border-top: var(--border); border-bottom: var(--border); }

.band-dark { background: var(--n900); color: var(--n50); }

.band-dark .section-sub,
.band-dark .generic-body p { color: #C4C4C4; }

/* Consecutive bordered bands would otherwise draw a 2px seam, and an n50
   border sitting on top of a dark band reads as a stray light hairline. */
.band-n50 + .band-n50 { border-top: 0; }
.band-dark + .band-n50 { border-top: 0; }
.band-n50:has(+ .band-dark) { border-bottom: 0; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 1.125rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}

.btn-primary { background: var(--n900); color: var(--n0); }

@media (hover: hover) {
  .btn-primary:hover { background: #262626; }
}

.btn-primary:active { background: #333333; transform: translateY(1px); }

:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: var(--radius);
}

.band-dark :focus-visible { outline-color: var(--blue-lift); }

/* ---------- labels ----------
   One label type across the page: uppercase, tracked, small. `.eyebrow-badge`
   only adds the chip around it, and changes nothing about the type. Every
   section kicker uses this, including the one above the h1. */

.eyebrow {
  margin: 0 0 var(--space-3);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--n500);
  text-wrap: balance;
}

.eyebrow-accent { margin-bottom: var(--space-1); color: var(--blue); }

.eyebrow-badge {
  display: inline-block;
  margin-bottom: var(--space-3);
  padding: 0.4375rem 0.75rem;
  border-radius: 999px;
  background: var(--blue-bg);
  color: var(--blue);
}

.band-dark .eyebrow-badge {
  background: rgba(255, 255, 255, 0.09);
  color: var(--blue-lift);
}

.hero .eyebrow { margin-bottom: var(--space-2); }

/* ---------- nav ---------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 10;
  background: rgba(255, 255, 255, 0.92);
  -webkit-backdrop-filter: saturate(160%) blur(8px);
  backdrop-filter: saturate(160%) blur(8px);
  border-bottom: var(--border);
  padding-top: env(safe-area-inset-top);
}

.nav-inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 0.5rem var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.nav-logo {
  display: flex;
  align-items: center;
  min-height: 44px;
  text-decoration: none;
}

.nav-logo img { width: auto; height: 1.375rem; }

.nav-links { display: flex; align-items: center; gap: 0.5rem; }

.nav-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.75rem;
  color: var(--n500);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  transition: color var(--t-fast) var(--ease);
}

@media (hover: hover) {
  .nav-link:hover { color: var(--n900); }
}

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

.hero { padding-top: 3.5rem; padding-bottom: 3.5rem; }

.hero h1 {
  max-width: 17ch;
  font-size: 2.25rem;
  letter-spacing: -0.035em;
  line-height: 1.04;
}

.lede {
  margin: var(--space-2) 0 0;
  max-width: 34ch;
  color: var(--n500);
  font-size: 1.0625rem;
  line-height: 1.55;
  text-wrap: pretty;
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.875rem 1.25rem;
  margin-top: var(--space-3);
}

.cta-note { color: var(--n500); font-size: 0.875rem; }

/* ---------- stat ---------- */

.stat .shell { text-align: center; }

.stat-figure {
  margin: 0;
  font-size: 3.5rem;
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1;
  /* The number counts up. Tabular figures keep 7 and 100 the same width, so
     the block does not jitter on its way there. */
  font-variant-numeric: tabular-nums;
}

.stat-label {
  margin: 0.5rem 0 0;
  color: var(--n500);
  font-size: 0.9375rem;
}

.stat-body {
  margin: var(--space-3) auto 0;
  max-width: 34ch;
  font-size: 1.0625rem;
  line-height: 1.5;
  text-wrap: pretty;
}

/* ---------- features ---------- */

/* Padding, not margin, so the rule sits midway in the gap between two rows.
   At 2rem a phone showed the tail of one row, a whole row, and the start of the
   next, which meant two dogs on screen competing with each other. */
.feature {
  display: grid;
  gap: 1.5rem;
  padding: 6rem 0;
  border-top: var(--border);
}

.feature:first-of-type { padding-top: 0; border-top: 0; }

.feature:last-child { padding-bottom: 0; }

.feature h2 {
  font-size: 1.3125rem;
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.2;
}

.feature-sub {
  margin: var(--space-1) 0 0;
  max-width: var(--measure-body);
  color: var(--n500);
  font-size: 0.9375rem;
  line-height: 1.5;
  text-wrap: pretty;
}

.points { margin-top: var(--space-2); max-width: var(--measure-body); }

.points li {
  position: relative;
  padding-left: 1.375rem;
  font-size: 0.9375rem;
  line-height: 1.5;
}

.points li + li { margin-top: 0.5rem; }

/* A short rule, not a dash character. */
.points li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.72em;
  width: 0.625rem;
  height: 1px;
  background: var(--blue);
}

/* On a phone the media runs the full column width. The 19rem cap is the
   desktop media column and is applied at that breakpoint, not here. */
.feature-media { width: 100%; }

/* The clips ship cropped to 1104x460, the band the dogs actually move through.
   Uncropped they were 4:3 with the animal in the middle third, which on a phone
   was a tall box of empty cyclorama. The well matches the crop exactly, so
   nothing is cropped again or letterboxed here. White rather than a tinted
   well, because the footage itself is white to its edges. */
.slot {
  width: 100%;
  aspect-ratio: 1104 / 460;
  border: var(--border);
  border-radius: var(--radius);
  background: var(--n0);
  object-fit: cover;
}

/* ---------- qualification mockup ---------- */

.qual-card {
  border: var(--border);
  border-radius: var(--radius);
  background: var(--n0);
  padding: 1rem 1.125rem;
}

.qual-step {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  font-size: 0.875rem;
  line-height: 1.45;
}

.qual-step + .qual-step { margin-top: 0.625rem; }

.qual-icon { flex: none; width: 1rem; height: 1rem; margin-top: 0.14em; }
.qual-icon svg { width: 100%; height: 100%; overflow: visible; }

.qual-ring {
  fill: none;
  stroke: var(--green);
  stroke-width: 1.5;
}

.qual-check {
  fill: none;
  stroke: var(--green);
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.qual-result {
  margin: 0.875rem 0 0;
  padding-top: 0.875rem;
  border-top: var(--border);
  color: var(--blue);
  font-size: 0.8125rem;
  font-weight: 600;
}

/* Armed by script.js, and only when motion is welcome. Everything above is the
   finished state, so a card that never gets armed still reads correctly. */
.qual-card.is-armed .qual-step { color: var(--n400); }
.qual-card.is-armed .qual-ring { stroke: var(--n200); }
.qual-card.is-armed .qual-check { stroke-dasharray: 10; stroke-dashoffset: 10; }

.qual-card.is-armed .qual-result {
  opacity: 0;
  transform: translateY(4px);
}

.qual-card.is-armed .qual-step.is-active .qual-ring {
  stroke: var(--green);
  stroke-dasharray: 9 28.7;   /* r=6, so the circumference is ~37.7 */
  transform-origin: 50% 50%;
  animation: qual-spin 0.85s linear infinite;
}

.qual-card.is-armed .qual-step.is-done { color: var(--n900); }
.qual-card.is-armed .qual-step.is-done .qual-ring { stroke: var(--green); }

.qual-card.is-armed .qual-step.is-done .qual-check {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 300ms var(--ease);
}

.qual-card.is-armed.is-resolved .qual-result {
  opacity: 1;
  transform: none;
  transition: opacity var(--t-base) var(--ease), transform var(--t-base) var(--ease);
}

@keyframes qual-spin {
  to { transform: rotate(360deg); }
}

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

.section-title {
  font-size: 1.625rem;
  letter-spacing: -0.03em;
  line-height: 1.15;
  max-width: var(--measure-title);
}

.section-sub {
  margin: var(--space-1) 0 0;
  max-width: var(--measure-body);
  color: var(--n500);
  font-size: 1rem;
  line-height: 1.55;
  text-wrap: pretty;
}

/* ---------- generic forms (dark) ---------- */

.rule {
  height: 0;
  margin: var(--space-3) 0;
  border: 0;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.generic-body p {
  margin: 0;
  max-width: var(--measure-body);
  font-size: 1rem;
  line-height: 1.6;
  text-wrap: pretty;
}

.generic-body .emphasis {
  margin-top: var(--space-2);
  color: var(--n0);
  font-weight: 600;
}

/* ---------- replaces the stack ---------- */

/* Sources, wires, target. The wire column is the only flexible one, so the
   pills keep a readable width on a phone and the curves just get shorter. */
.flow {
  display: grid;
  /* Content-width columns packed at the start, so the widest pill's left edge
     lands on the shell edge and the diagram shares a left margin with the
     heading above it. Sized as a fraction of the shell it floated in the
     middle of the band with nothing to line up against. */
  grid-template-columns: auto clamp(2.75rem, 12vw, 7rem) auto;
  justify-content: start;
  align-items: center;
  margin-top: var(--space-4);
}

.flow-sources { display: grid; gap: var(--flow-gap); }

/* Height and gap here set where the four wires start. The path origins in the
   markup are (i * (height + gap) + height / 2) / total, mapped onto a 0-200
   viewBox. Both are in rem, so the alignment survives the fluid root size. */
:root { --flow-pill: 2.25rem; --flow-gap: 0.5rem; }

.flow-source {
  display: flex;
  align-items: center;
  min-height: var(--flow-pill);
  padding: 0 0.875rem;
  border: var(--border);
  border-radius: 999px;
  background: var(--n0);
  color: var(--n400);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.3;
  text-decoration: line-through;
  /* Hug the label and sit against the wires, so every pill's right edge is a
     wire origin. Stretched full-width they just read as empty input fields. */
  justify-self: end;
  /* Keeps every pill exactly --flow-pill tall, which is what the wire origins
     below are calculated from. A wrapped label would break the alignment. */
  white-space: nowrap;
}

/* Height is stated rather than stretched. Letting the grid size this cell put
   it 8px out of step with the pill column, which showed as four wires leaving
   from just above each pill instead of out of its middle. */
.flow-wires {
  width: 100%;
  height: calc(4 * var(--flow-pill) + 3 * var(--flow-gap));
}

.wire-base path {
  fill: none;
  stroke: var(--n200);
  stroke-width: 1.25;
  vector-effect: non-scaling-stroke;
}

/* Hidden until the diagram is on screen, so the page is not animating a
   section nobody is looking at. */
.wire-pulse path {
  fill: none;
  stroke: var(--blue);
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-dasharray: 22 178;   /* pathLength is normalised to 200 in the markup */
  stroke-dashoffset: 200;
  opacity: 0;
  vector-effect: non-scaling-stroke;
}

.flow.is-running .wire-pulse path {
  opacity: 1;
  animation: wire-run 2.4s linear infinite;
}

@keyframes wire-run {
  to { stroke-dashoffset: 0; }
}

.flow-target {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  justify-self: start;
  padding: 0.875rem 1.125rem;
  border-radius: 10px;
  border: var(--border);
  background: var(--n0);
}

/* The wordmark is dark on transparent and stays that way. Reversed out white
   it lost the counters in the mark and read as a smudge at this size. */
.flow-target img {
  width: auto;
  height: 1.25rem;
}

.flow-ring {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid var(--blue);
  opacity: 0;
}

.flow.is-running .flow-ring { animation: node-pulse 2.4s var(--ease) infinite; }

@keyframes node-pulse {
  0%       { transform: scale(1);    opacity: 0.55; }
  70%,100% { transform: scale(1.16); opacity: 0; }
}

.stack-body {
  margin: var(--space-4) 0 0;
  max-width: var(--measure-body);
  font-size: 1rem;
  line-height: 1.6;
  text-wrap: pretty;
}

/* ---------- close ---------- */

.close .shell { text-align: center; }
.close .section-title { max-width: 20ch; margin: 0 auto; }
.close .section-sub { margin-left: auto; margin-right: auto; }

/* ---------- form ---------- */

/* On the band scale like every other section. It was the one block still
   carrying its own hand-picked padding, which is why it sat tighter under the
   dark band than any other section transition on the page. */
.form-section { padding: var(--band-y) 0; scroll-margin-top: 3.5rem; }

/* Capped in px, not rem. The embed picks its own inset from the width it is
   given, so letting the fluid root widen this on a 4K display would move the
   card's edge out from under the label. A form has one good width anyway. */
.form-inner {
  width: 100%;
  max-width: 704px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.form-title {
  padding-left: var(--card-inset);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.35;
  margin-bottom: 0.875rem;
  text-wrap: pretty;
}

/* The slot reserves the embed's height up front so nothing reflows on load. */
.form-slot {
  position: relative;
  min-height: 500px;
  transition: min-height var(--t-slow) var(--ease);
}

.form-slot.is-loaded { min-height: 0; }

.form-frame {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  border: 0;
  min-height: 500px;
  opacity: 0;
  transition: opacity var(--t-base) var(--ease);
}

.form-slot.is-loaded .form-frame { opacity: 1; }

/* Sits behind the iframe. This is what a prospect sees on venue wifi if the
   embed is slow, and everything it can show is static HTML. */
.form-standby {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  transition: opacity var(--t-base) var(--ease);
}

.form-slot.is-loaded .form-standby { opacity: 0; pointer-events: none; }

.standby-loading {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--n400);
  font-size: 0.9375rem;
}

.standby-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--n200);
  animation: pulse 1.4s var(--ease) infinite;
}

@keyframes pulse {
  0%, 100% { background: var(--n200); }
  50%      { background: var(--n400); }
}

.standby-fallback { text-align: center; }

.standby-fallback-text {
  margin: 0 0 0.875rem;
  color: var(--n500);
  font-size: 0.9375rem;
}

.form-note {
  margin: 0.75rem 0 0;
  padding-left: var(--card-inset);
  color: var(--n400);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.form-direct {
  margin: 0.5rem 0 0;
  padding-left: var(--card-inset);
  color: var(--n400);
  font-size: 0.8125rem;
}

.form-direct a { color: var(--n500); }

/* ---------- footer ---------- */

.footer {
  border-top: var(--border);
  padding: 1.5rem 0 calc(1.5rem + env(safe-area-inset-bottom));
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
}

.footer p { margin: 0; color: var(--n400); font-size: 0.8125rem; }

/* ---------- wider screens ---------- */

@media (min-width: 34rem) {
  :root { --gutter: 2rem; --band-y: 5.5rem; --card-inset: 24px; }

  .hero { padding-top: 4.5rem; padding-bottom: 4.5rem; }
  .hero h1 { font-size: 3rem; }
  .lede { font-size: 1.125rem; }

  .stat-figure { font-size: 4.5rem; }
  .stat-body { font-size: 1.125rem; }

  .section-title { font-size: 2rem; }
  .feature h2 { font-size: 1.4375rem; }
  .form-title { font-size: 1.125rem; }

  .int-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; }
  .int-cell { padding: 1.5rem 0.75rem; }
}

@media (min-width: 56rem) {
  :root { --band-y: 7rem; --card-inset: 32px; }

  .hero { padding-top: 6rem; padding-bottom: 5.5rem; }
  .hero h1 { font-size: 3.5rem; letter-spacing: -0.04em; }
  /* Two lines at this size, as in the FigJam. The 34ch measure that reads well
     on a phone breaks it into three and the block stops sitting under the h1. */
  .lede { max-width: var(--measure-body); }

  .stat-figure { font-size: 5rem; }

  .section-title { font-size: 2.25rem; }

  /* Media left, copy right, matching the FigJam. The columns are asymmetric so
     the body copy keeps a readable measure rather than stretching to half. */
  .feature {
    grid-template-columns: 19rem minmax(0, 1fr);
    gap: 2.5rem;
    align-items: start;
    padding: 4rem 0;
  }

  .feature h2 { font-size: 1.5rem; max-width: var(--measure-subtitle); }

  /* The mockup card and both clip wells share this width, so all three rows
     start on the same left edge and read as one column. */
  .feature-media { max-width: 19rem; }
}

/* ---------- reduced motion ----------
   Everything above degrades to its finished state. script.js reads the same
   query and skips arming the mockup and the counter. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  .js [data-reveal],
  .js .int-grid .int-cell {
    opacity: 1;
    transform: none;
  }
}
