/* Vetfluence · motion.css · v3
   Motion that argues. Every block here either makes a point the page is
   already making, or removes a reason to leave.

   Rules this file holds itself to:
   · transform and opacity wherever possible, so work stays off the main thread
   · scroll-driven CSS in preference to JS, with a plain fallback
   · every effect has a designed reduced-motion state, not an off switch
   · nothing moves that a 55-year-old practice owner has to wait for */

/* ------------------------------------------------- scroll-driven reveals
   Used by every component built in v3. Where the browser lacks
   animation-timeline the content simply renders, which is the correct
   failure: visible. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    [data-sd] {
      animation: sd-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 22%;
    }
    [data-sd="fade"] { animation-name: sd-fade; }
    [data-sd="wipe"] { animation-name: sd-wipe; }
    [data-sd="scale"] { animation-name: sd-scale; }
    [data-sd-stagger] > * {
      animation: sd-rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 18%;
    }
  }
}
@keyframes sd-rise { from { opacity: 0; transform: translateY(12px); } }
@keyframes sd-fade { from { opacity: 0; } }
@keyframes sd-scale { from { opacity: 0; transform: scale(.965); } }
@keyframes sd-wipe { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 -2% 0); } }

/* ------------------------------------------------- headlines that set themselves
   Archivo is already loaded as a variable font across wdth 62..125 and
   wght 400..900, and the site uses almost none of that range. A display
   line condenses to its final width and settles its weight on entry.
   Changed 2026-10-28: animating the width axis re-wrapped the headline after
   first paint (layout shift ~0.2, Google's "poor"). It now rises and fades
   only, which moves pixels without moving anything around it. */
/* waits off screen already faded, so it never blinks from full to faint */
@media (prefers-reduced-motion: no-preference) { .set-type:not(.is-set) { opacity: 0; } }
.set-type.is-set {
  animation: type-set .4s cubic-bezier(.2, .8, .2, 1) both;
}
@keyframes type-set {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* lines of a big statement arriving one at a time, each from behind its own
   edge. The line boxes are built by js/upgrade.js. */
.lines__l { display: block; overflow: hidden; }
.lines__i { display: block; transform: translateY(102%); }
.lines.is-in .lines__i {
  animation: line-up .42s cubic-bezier(.2, .8, .2, 1) both;
  animation-delay: calc(min(var(--i, 0), 8) * 28ms);
}
@keyframes line-up { to { transform: translateY(0); } }

/* ------------------------------------------------- the heartbeat, as progress
   The journey section already draws an ECG. This runs the same trace down
   the full page as a reading indicator: the site's own motif doing a job. */
.ecgbar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  z-index: 120;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s;
}
.ecgbar.is-on { opacity: 1; }
.ecgbar svg { width: 100%; height: 100%; display: block; }
.ecgbar .trace {
  fill: none;
  stroke: var(--slate);
  stroke-width: 2.5;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transform-origin: left;
}
@supports (animation-timeline: scroll()) {
  .ecgbar .trace {
    animation: ecg-draw linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes ecg-draw { to { stroke-dashoffset: 0; } }
.ecgbar .beat { fill: var(--slate-deep); opacity: 0; }
.ecgbar.is-beating .beat { animation: ecg-beat .5s var(--ease); }
@keyframes ecg-beat { 0% { opacity: 0; r: 2; } 40% { opacity: 1; r: 5; } 100% { opacity: 0; r: 2; } }

/* ------------------------------------------------- sticky action bar
   Appears once the hero has had its say. One action, never two.
   This exists because checklist item 04 asks whether a visitor can book
   without scrolling, and without it this site fails its own test. */
.stickybar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 130;
  display: none;
  gap: 10px;
  align-items: center;
  padding: 10px var(--gut-r) calc(10px + env(safe-area-inset-bottom, 0px)) var(--gut);
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-top: 1px solid var(--line);
  transform: translateY(102%);
  transition: transform .34s var(--ease);
}
.stickybar.is-on { transform: none; }
.stickybar__t { display: grid; gap: 1px; min-width: 0; }
.stickybar__t b { font: 700 14px/1.2 var(--f-body); }
.stickybar__t span { font: 500 12.5px/1.2 var(--f-mono); color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stickybar .btn { margin-left: auto; min-height: 44px; padding: 0 18px; font-size: 15px; white-space: nowrap; }
@media (max-width: 980px) { .stickybar { display: flex; } }
@media (prefers-reduced-motion: reduce) { .stickybar { transition: none; } }

/* ------------------------------------------------- nav previews
   Hovering a nav item says what is behind it, as a miniature of the page's
   own shape rather than a screenshot that would go stale. */
.navprev {
  position: fixed;
  z-index: 190;
  width: 232px;
  padding: 12px;
  border-radius: var(--r);
  background: var(--raise);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-2);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px) scale(.97);
  transform-origin: top left;
  transition: opacity .18s, transform .18s var(--ease), visibility 0s .18s;
  pointer-events: none;
}
.navprev.is-on { opacity: 1; visibility: visible; transform: none; transition: opacity .18s, transform .18s var(--ease); }
.navprev__wire { display: grid; gap: 4px; padding: 9px; border-radius: 8px; background: var(--paper-2); margin-bottom: 9px; }
.navprev__wire i { display: block; height: 5px; border-radius: 2px; background: var(--line); }
.navprev__wire i.k { background: var(--slate); height: 8px; }
.navprev__wire .row { display: flex; gap: 4px; }
.navprev__wire .row i { flex: 1; height: 15px; }
.navprev b { display: block; font: 700 14px/1.2 var(--f-body); margin-bottom: 2px; }
.navprev span { font: 500 12.5px/1.35 var(--f-body); color: var(--ink-soft); }
@media (max-width: 980px), (pointer: coarse) { .navprev { display: none; } }

/* ------------------------------------------------- images that never pop
   The portfolio screens fade from their own paper, so nothing snaps in. */
.frame__view img, .shot img { opacity: 0; transition: opacity .5s var(--ease); }
.frame__view img.is-loaded, .shot img.is-loaded { opacity: 1; }
.frame__view, .shot {
  background-image: linear-gradient(100deg, var(--paper-2) 30%, var(--paper-3) 50%, var(--paper-2) 70%);
  background-size: 220% 100%;
}
.frame__view.is-waiting, .shot.is-waiting { animation: shimmer 1.5s linear infinite; }
@keyframes shimmer { to { background-position: -120% 0; } }
@media (prefers-reduced-motion: reduce) {
  .frame__view.is-waiting, .shot.is-waiting { animation: none; }
  .frame__view img, .shot img { transition: none; }
}

/* ------------------------------------------------- the decode table, moving
   The consequence slides in as the row arrives. The promise itself used to
   be struck through with a red rule, which read as damage rather than as
   argument; the two columns already make the point. */
.decode__row.is-in dd { animation: dd-in .5s var(--ease) .22s both; }
@keyframes dd-in { from { opacity: 0; transform: translateX(-10px); } }
@media (prefers-reduced-motion: reduce) {
  .decode__row.is-in dd { animation: none; }
}

/* ------------------------------------------------- species cycle
   "Care for every pet in the family" is a claim the page can simply show. */
.species { position: relative; display: inline-grid; vertical-align: middle; width: 1.25em; height: 1.25em; }
.species svg { grid-area: 1 / 1; width: 100%; height: 100%; opacity: 0; transform: translateY(26%) scale(.84); transition: opacity .42s var(--ease), transform .42s var(--ease); }
.species svg.is-on { opacity: 1; transform: none; }
.species svg.is-out { opacity: 0; transform: translateY(-26%) scale(.84); }
@media (prefers-reduced-motion: reduce) {
  .species svg { transition: none; }
  .species svg:first-child { opacity: 1; transform: none; }
  .species svg:not(:first-child) { display: none; }
}

/* ------------------------------------------------- the wordmark, once
   Drawn on the first visit of a session and never again, because a logo
   animation on the fourth page view is an imposition, not a flourish. */
.logo-wrap { display: inline-block; }
.logo-wrap.is-drawing .logo {
  -webkit-mask-image: linear-gradient(90deg, #000 0 33%, transparent 66%);
  mask-image: linear-gradient(90deg, #000 0 33%, transparent 66%);
  -webkit-mask-size: 320% 100%;
  mask-size: 320% 100%;
  animation: logo-wipe .85s cubic-bezier(.3, .9, .3, 1) both;
}
@keyframes logo-wipe { from { -webkit-mask-position: 100% 0; mask-position: 100% 0; } to { -webkit-mask-position: 0 0; mask-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .logo-wrap.is-drawing .logo { animation: none; -webkit-mask-image: none; mask-image: none; } }

/* ------------------------------------------------- stacking section headers
   On a long page you should always know which of Found / Trusted / Booked
   you are standing in. */
/* the step number used to stick to the top of the window and travel with
   the reader; it belongs to its step, not to the viewport */
.jstep__n { position: static; }

/* ------------------------------------------------- the gantt, sideways
   The week-by-week grid is unreadable squeezed into 360px. On a phone it
   becomes a scroll-snapped strip instead. */
@media (max-width: 760px) {
  .gantt { overflow-x: auto; scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
  .gantt__in { min-width: 680px; }
  .g-row > .g-bar { scroll-snap-align: start; }
  .gantt::-webkit-scrollbar { height: 6px; }
  .gantt::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
}

/* ------------------------------------------------- keyboard shortcuts
   Thirty lines. The one visitor who finds it remembers the site. */
.shortcuts {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: 20px;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  backdrop-filter: blur(3px);
  opacity: 0;
  visibility: hidden;
  transition: opacity .2s, visibility 0s .2s;
}
.shortcuts.is-on { opacity: 1; visibility: visible; transition: opacity .2s; }
.shortcuts__in {
  width: min(460px, 100%);
  background: var(--raise);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 26px;
  box-shadow: var(--shadow-2);
  transform: translateY(10px) scale(.98);
  transition: transform .24s var(--ease);
}
.shortcuts.is-on .shortcuts__in { transform: none; }
.shortcuts h2 { font: 800 21px/1.2 var(--f-display); margin: 0 0 16px; }
.shortcuts dl { display: grid; grid-template-columns: auto 1fr; gap: 10px 16px; margin: 0; align-items: center; }
.shortcuts dt { display: flex; gap: 4px; }
.shortcuts dd { margin: 0; font-size: 14.5px; color: var(--ink-soft); }
.shortcuts kbd {
  font: 600 12px/1 var(--f-mono);
  padding: 5px 7px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--paper-2);
}
.shortcuts__close { margin-top: 18px; background: none; border: 0; text-decoration: underline; font: 600 14px/1 var(--f-body); color: var(--ink-soft); cursor: pointer; }
@media (pointer: coarse) { .shortcuts { display: none; } }

/* ------------------------------------------------- scrub: template vs built
   The site argues against templates on four pages and never once shows one. */
.scrub { position: relative; border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--line); background: var(--paper-2); touch-action: pan-y; }
.scrub__pane { position: relative; display: grid; }
.scrub__a, .scrub__b { grid-area: 1 / 1; }
.scrub__b { clip-path: inset(0 0 0 var(--x, 50%)); }
.scrub__lbl {
  position: absolute;
  top: 14px;
  z-index: 3;
  font: 600 11px/1 var(--f-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 7px 11px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter: blur(6px);
  border: 1px solid var(--line);
}
.scrub__lbl--a { left: 14px; }
.scrub__lbl--b { right: 14px; }
.scrub__bar {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--x, 50%);
  width: 2px;
  background: var(--paper);
  z-index: 4;
  transform: translateX(-1px);
  box-shadow: 0 0 0 1px var(--ink-soft);
}
.scrub__grip {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  border-radius: 999px;
  background: var(--paper);
  border: 2px solid var(--ink);
  display: grid;
  place-items: center;
  cursor: ew-resize;
  box-shadow: var(--shadow-1);
}
.scrub__grip::before { content: "⟨ ⟩"; font: 700 13px/1 var(--f-mono); letter-spacing: -1px; }
.scrub input[type="range"] { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: ew-resize; z-index: 5; margin: 0; }
.scrub input[type="range"]:focus-visible ~ .scrub__bar .scrub__grip { outline: 3px solid var(--focus); outline-offset: 3px; }
.scrub__hint { padding: 12px 16px; font: 500 13px/1.4 var(--f-mono); color: var(--ink-soft); border-top: 1px solid var(--line); text-align: center; }

/* ------------------------------------------------- the score, as a diagnosis
   The ring already existed and was well made. What it lacked was a verdict:
   a count out of ten is flat, a banded result is a diagnosis. These extend
   the existing markup rather than replacing it. */
.ring .fg { transition: stroke-dashoffset .6s var(--ease), stroke .45s; }
.score[data-band="low"] .ring .fg { stroke: #e08177; }
.score[data-band="mid"] .ring .fg { stroke: #e0ad6a; }
.score[data-band="high"] .ring .fg { stroke: #8fd0b6; }
.score[data-band="full"] .ring .fg { stroke: #8fd0b6; }
.score__band {
  font: 600 11px/1 var(--f-mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--invert-line);
}
.score[data-band="low"] .score__band { color: #e08177; border-color: #e0817744; }
.score[data-band="mid"] .score__band { color: #e0ad6a; border-color: #e0ad6a44; }
.score[data-band="high"] .score__band,
.score[data-band="full"] .score__band { color: #8fd0b6; border-color: #8fd0b644; }
.ring .num.is-tick { animation: tick .34s var(--ease); }
@keyframes tick { 40% { transform: scale(1.14); } }
.score__copy {
  background: none;
  border: 0;
  color: var(--muted-ink);
  text-decoration: underline;
  cursor: pointer;
  font: 500 14px/1 var(--f-body);
  padding: 2px;
}
.score__copy.is-done { color: #8fd0b6; text-decoration: none; }
@media (prefers-reduced-motion: reduce) {
  .ring .fg { transition: none; }
  .ring .num.is-tick { animation: none; }
}

/* the fix that appears under an item its site did not pass */
.ck__fix {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .32s var(--ease);
}
.ck__item.is-missed .ck__fix { grid-template-rows: 1fr; }
.ck__fix > div { overflow: hidden; }
.ck__fix p {
  margin: 12px 0 0;
  padding: 12px 14px;
  border-left: 3px solid var(--no);
  background: var(--paper-2);
  font-size: 14.5px;
  border-radius: 0 8px 8px 0;
}
@media (prefers-reduced-motion: reduce) { .ck__fix { transition: none; } }

/* ------------------------------------------------- a blanket safety net
   Anything added in v3 that was missed above still stops moving here. */
@media (prefers-reduced-motion: reduce) {
  [data-sd], [data-sd-stagger] > *, .set-type.is-set, .lines.is-in .lines__i {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
  .ecgbar .trace { animation: none; stroke-dashoffset: 0; }
  .navprev, .shortcuts, .shortcuts__in { transition-duration: .01ms; }
}

/* ------------------------------------------------- the hand, not the machine
   Every illustration on this site is built from perfect circles and
   ellipses, which is exactly what generated art looks like. A low-frequency
   displacement filter puts a slight waver back into every edge, the way a
   drawn line waves. It costs one filter, applies to artwork already on the
   page, and it is the single clearest signal that a person made this.

   Applied only where the drawing is illustrative. The clinic UI mock-ups
   stay crisp, because a wobbly interface reads as broken rather than drawn. */
.rough { filter: url(#vf-rough); }

/* An ink line around every form. A waver alone is too quiet to notice at
   real size; an outline is what actually reads as "someone drew this",
   the way clinic signage and children's books do. Non-scaling so the line
   keeps its weight whatever size the drawing is used at. */
.rough :is(path, circle, ellipse, rect) {
  stroke: #2a211c;
  stroke-width: 1.15;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
/* the backdrop planes are scenery, not subjects: they keep a lighter line */
.rough > rect:first-child,
.rough rect { stroke-width: .7; stroke: rgba(42, 33, 28, .35); }

/* a second pass for the bigger drawings, which can carry more waver */
.rough--more { filter: url(#vf-rough-2); }

@media (prefers-reduced-motion: reduce) {
  /* the filter is static, so it stays: it is drawing, not motion */
}

/* paper-grain warmth behind illustrations, so they sit on a surface
   rather than floating on a flat fill */
.inked { position: relative; }
.inked::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .14;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='90' height='90' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* ------------------------------------------------- looping animations rest off-screen
   site.js marks a section .is-parked while it is out of view. Only animations that
   loop forever are listed: a one-off entrance is never paused, so nothing can be
   caught half-finished when it scrolls back in. */
.is-parked :is(.pmap__ring, .frame__view.is-waiting, .shot.is-waiting, .badge .dot, .caret, .map__ring, .cta__pulse path, .g-now, .target .spin, .target .stamp, .artv.is-play .a-pulse),
.is-parked .g-now::before { animation-play-state: paused; }
