/* ================================================================
   project-detail.css — shared structure for the 5 CMS project pages
   (/projects/<slug>). Sections: her, project-info, testimonial.
   menu-line / help / footer are the shared partials (menu-line.css,
   help.css, footer.css) — not touched here.

   Source of truth: research/raw/desktop/projects--getting-started.html
   <style> blocks (research/_stylesheet.css is HOME-only, see
   SHARED-NOTES.md landmines). Values extracted via pagecss.mjs against
   the her/project-info/testimonial fragments; identical across the
   desktop/tablet/phone raw captures (desktop carries all 3 media
   blocks already).

   Per-page copy/images/stat values live inline in each page's HTML —
   this file is the ONE shared skeleton for all 5 instances.
   ================================================================ */

/* Framer renders EVERY word of these rich-text blocks as its own
   `display:inline-block` span (for the stagger reveal), which as a side
   effect makes a word unbreakable — the reference never splits "AI-powered"
   or "time-consuming" at the hyphen, it moves the whole token to the next
   line. Plain text in our markup breaks at the hyphen instead, so the same
   paragraph re-wraps with different break points from the first hyphen
   onward. `.nb` restores the reference's behaviour without reproducing one
   span per word; it is applied to hyphenated tokens only, which are the only
   place a browser would ever break inside a word here. */
.nb { white-space: nowrap; }

/* ================================================================
   Vertical "Vert line" rules — every one of them carries an explicit
   `flex:none; width:1px` in the reference (e.g. .framer-1icbdjf,
   .framer-1teof2a, .framer-1jqq9a5). That 1px is load-bearing, not
   decoration: base.css draws the rule with `::after { inset:0;
   width:100%; border-width: var(--border-*-width) }` on ALL FOUR sides,
   so the box has to be 1px wide for Chrome to clip the 2-3px left+right
   borders down to a 1px hairline. With no width the span measures 0,
   nothing clips, and the two borders paint outward side by side — a
   4-6px saturated bar where the reference has a 1px rule. Measured at
   phone: clone painted x=26..29, reference only x=26.
   The `overflow` half is the other load-bearing piece: with box-sizing
   border-box Chrome FLOORS a box at border+padding, so the ::after resolves
   to 4px even though width:100% is 1px — the reference hides the excess with
   `overflow: var(--overflow-clip-fallback, clip)` on the 1px box (same trick
   help.css already applies to .help/.faq lines). Width alone is not enough.
   Scoped to this page's own sections so help/footer keep their own rules.
   ================================================================ */
.pd-her .vline, .pd-her .vline--orange, .pd-her__end-line,
.pd-project-info .vline, .pd-project-info .vline--orange, .pd-numbers__end-line,
.pd-testimonial .vline, .pd-testimonial .vline--orange, .pd-tm__title-line {
  width: 1px;
  overflow: var(--overflow-clip-fallback, clip);
}

/* ================================================================
   HER  (data-section="her")  — reference layer .framer-f07f6o
   Height budget @1440x900: gradient/hr/bg are position:absolute
   (zero height). Live height = main-row (50vh/min400 -> 400px) +
   hairline (1px) + bottom-row (20vh/min200 -> 200px) = 651px. Matches
   reference-heights.json her:651 (desktop) exactly.
   ================================================================ */
.pd-her {
  position: relative;
  background-color: var(--c-cream);
  overflow: visible;
}

.pd-her__gradient {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 3;
  height: 449px;
  background: linear-gradient(var(--c-cream) 39%, transparent 100%);
  pointer-events: none;
}

.pd-her__main {
  position: relative;
  z-index: 4;
  display: flex;
  align-items: flex-start;
  width: 100%;
  height: 50vh;
  min-height: 400px;
  padding: 0 32px;
  overflow: visible;
}

/* .framer-1hqlme4 carries z-index:1. The background cloud plate
   (.framer-1h6ve04-container / .pd-her__bgimg) is z-index:0 but comes LATER
   in DOM order, so without an explicit z-index here the auto-z content
   container loses the tie and the .9-opacity dithered plate paints straight
   over the project title — "NovaFlow"/"Sonora" rendered pale grey instead of
   near-black at every breakpoint. */
.pd-her__main-inner {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  max-width: var(--page-max);
  height: 100%;
  margin: 0 auto;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 1;
}

.pd-her__gaps {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: center;
}
.pd-her__gaps .pd-gapcol {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: center;
  justify-content: center;
}

.pd-her__header {
  position: relative;
  display: flex;
  flex: 2 0 0;
  width: 1px;
  height: 100%;
  align-items: flex-start;
}

.pd-her__subheader {
  position: relative;
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: center;
  gap: 10px;
  padding-top: 62px;
}
.pd-her__subheader .vline { --border-bottom-width: 3px; --border-top-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach); height: 100%; }

.pd-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 31px;
  padding: 0 10px;
  background-color: var(--c-orange);
  white-space: pre;
}
.pd-badge .t-mono { color: var(--c-white); }

.pd-gapcol {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: center;
  justify-content: center;
}
.pd-gapcol .vline--faint { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone-cool-50); height: 100%; }
.pd-gapcol .vline--dash85 { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-stone-85); height: 100%; }
/* .framer-15d3f3w inside .framer-bd5i6u ("-gap+line1") is a 3px dashed peach
   rule at every tier, not a faint grey one. */
.pd-gapcol .vline--orange { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach); height: 100%; }

/* .framer-wj4sx5 / .framer-bd5i6u / .framer-pbwos7 are three equal flex:1
   columns of "Header part", each drawing its rule at its own LEFT edge
   (wj4sx5 uses justify-content:space-between at <=1023.98 and <=809.98).
   Our .pd-gapcol centres its single line instead, which put the only visible
   phone rule at x=195 and the only visible tablet rule at x=591 — the
   reference has them at the content edges (26 / 32, 281, 529). */
/* .framer-1nfmr22 is wj4sx5's SECOND rule; space-between parks it on the
   column's right edge (ref x=777 @810, x=364 @390). Scoped to <=1023.98
   because at desktop/laptop wj4sx5 is flex-start, where the two rules sit
   adjacent and the extra one would just double the existing hairline. */
.pd-her__gap-end { display: none; }
@media (max-width: 1023.98px) {
  .pd-her__header > .pd-gapcol { justify-content: space-between; }
  .pd-her__gap-end { display: block; }
}
@media (max-width: 809.98px) {
  /* .framer-1f45jbb @<=809.98: 2px dashed peach — this is the rule that runs
     down the left content edge of the hero at phone. */
  .pd-her__header > .pd-gapcol > .vline--faint {
    --border-top-width: 2px; --border-bottom-width: 2px;
    --border-left-width: 2px; --border-right-width: 2px;
    --border-style: dashed; --border-color: var(--c-peach);
  }
}

.pd-her__title {
  position: absolute;
  bottom: -8px;
  left: -3px;
  width: 100%;
  max-width: 650px;
  z-index: 1;   /* .framer-poo8sb */
}

.pd-her__bgimg {
  position: absolute;
  inset: -216px -759px -59px -758px;
  z-index: 0;
  opacity: .9;
  overflow: hidden;
  transform: scale(1.05638);
}
/* Animation-QA pass (2026-08-19): the live hero plate is NOT static — it is
   the same Framer ticker as the /projects and /solutions heroes (4 tiled
   copies of the dune source, translated on X; confirmed by idle-watching
   the live reference /projects/getting-started: a hero-band strip
   of 2880x1374-source imgs mutates its transform every frame). The markup
   now nests the shared .marquee / .marquee__item strip (footer.css owns the
   CSS, js/main.js sizes + paces it at the measured 30px/s). The old static
   <img> rule is gone with the markup.

   The `left` offset below preserves the previous calibration: the static
   plate carried translateX(-60.66px), so parking the strip's origin at the
   same phase keeps frame 0 pixel-identical to the calibrated rest render
   (the drift makes any later phase arbitrary, exactly as on the live site).
   Height-safe: the strip is absolute inside the overflow:hidden plate. */
.pd-her__bgimg .marquee { left: -60.66px; }
/* The -60.66px above is a calibration against the 1440 capture. At the 810
   viewport the plate lands 4px right of the reference: sweeping the offset
   over the cloud band (y330-455) takes it from 4.8% -> 0.1% on
   importing-content and 4.2% -> 1.4% on getting-started, with a flat optimum
   at -3/-4. Laptop and desktop have NO such optimum (the residual there is
   the dither pattern itself, not placement), so this is scoped to the tablet
   tier only rather than folded into the shared 810-1439.98 block. */
@media (min-width: 810px) and (max-width: 1023.98px) {
  .pd-her__bgimg .marquee { left: -64.66px; }
}

.pd-her__hr {
  position: relative;
  z-index: 4;
  height: 1px;
  width: 100%;
  background: var(--c-stone);
}

/* .framer-pnu0pg is flex-flow:COLUMN. As a row, its `flex:1 0 0` inner
   resolved its literal `height:1px` instead of growing, so .pd-her__left /
   __right became 1px-tall boxes centred in the band and every child hung off
   that hairline. As a column the inner fills the band height, which is what
   the reference's left/right columns are measured against. */
.pd-her__bottom {
  position: relative;
  z-index: 4;
  background-color: var(--c-cream);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 20vh;
  min-height: 200px;
  padding: 0 32px;
  overflow: visible;
}

/* .framer-br8np1 is the ONE content-container whose base width is 100%, not
   the usual `width:1px` + `flex:1 0 0` idiom. That matters only at phone,
   where the override drops it to `flex:none`: with width:1px it collapsed to
   a 1px column centred at x=194.5, dragging .pd-her__right / __livelink /
   __desc off the left edge with it. */
.pd-her__bottom-inner {
  display: flex;
  flex: 1 0 0;
  width: 100%;
  max-width: var(--page-max);
  height: 1px;
  margin: 0 auto;
  align-items: center;
  justify-content: center;
  position: relative;
}

.pd-her__left {
  position: relative;
  z-index: 2;
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: center;
}
/* .framer-1ecbfom is `place-content:flex-start; align-items:flex-start` — the
   link container below supplies its own 42px top padding. Centring here
   stacked that padding on top of a centred box and dropped the whole
   "case studies" link ~85px below the reference. */
.pd-her__left-line {
  display: flex;
  flex: 1 0 0;
  width: 100%;
  height: 100%;
  align-items: flex-start;
  justify-content: flex-start;
}
.pd-her__left-line .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone); height: 100%; }
.pd-her__case-link {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-top: 42px;
  flex: 1 0 0;
}
.pd-her__case-link .arrow-link__icon svg { transform: rotate(180deg); }
/* menu-line.css owns .arrow-link__icon and correctly models the FOOTER/menu
   variant: absolute at left:-23px, opacity:0, revealed on hover (verified
   against the live site). The hero uses Framer's "Arrowed tab-mob" variant
   instead, whose own rule is `left:unset; top:unset; order:0;
   position:relative` with the Icon carrying inline opacity:1 — i.e. the arrow
   sits IN FLOW before the label and is visible at rest. Without this the
   orange back-arrow next to "case studies" never painted at any breakpoint. */
.pd-her__case-link .arrow-link__icon {
  position: relative;
  top: auto; left: auto;
  transform: none;
  opacity: 1;
  order: 0;
}
/* Animation-QA pass (2026-08-18): measured LIVE with a real Playwright
   hover on the hero "case studies" link (getComputedStyle transform + rect
   before/after, the live reference /projects/getting-started) —
   both were byte-identical. The "Arrowed" variant has NO hover animation at
   all (unlike the footer/menu `.arrow-link` component, which is the
   hover-reveal one). A `:hover { transform: translateX(3px) }` nudge used
   to live here; removed so the clone matches live's fully static rest. */

.pd-her__right {
  position: relative;
  z-index: 2;
  display: flex;
  flex: 2 0 0;
  width: 1px;
  height: 100%;
  align-items: flex-start;
}

.pd-her__desc {
  position: absolute;
  top: 0; left: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  max-width: 340px;
  padding-top: 42px;
  text-wrap: balance;
}

.pd-her__gap3 {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: flex-start;
}
.pd-her__gap3 .vline { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach); height: 80vh; }

.pd-her__livelink {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  height: 100%;
  align-items: flex-start;
  justify-content: space-between;
}
.pd-her__livelink-inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  flex: 1 0 0;
  width: 1px;
  height: min-content;
  padding-top: 0;
}
.pd-her__livelink-title { padding-top: 0; }

.pd-her__end-line {
  --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px;
  --border-style: solid; --border-color: var(--c-stone-cool-50);
  height: 100%;
}

/* ---------------- tablet + laptop (810 - 1439.98) ---------------- */
@media (min-width: 810px) and (max-width: 1439.98px) {
  .pd-her__subheader .vline { --border-style: solid; --border-color: var(--c-stone-cool-50); }
  .pd-her__title { bottom: -6px; }
  .pd-her__bottom { height: 18vh; min-height: unset; }
  .pd-her__desc { max-width: 270px; }
  /* The column flip belongs to .framer-17y21h (Link container), NOT to
     .framer-1yfj6oh (-gap+line3), which only gets `gap:unset;
     justify-content:space-between` here and stays a row. Applying it one
     level too high stacked "live link:" above the description and collapsed
     the inner container to its 1px flex-basis. */
  .pd-her__livelink { gap: unset; justify-content: space-between; }
  .pd-her__livelink-inner { flex-direction: column; align-content: flex-start; align-items: flex-start; gap: 4px; }
  .pd-her__livelink-title { flex: none; width: 100%; }
  .pd-her__livelink-value { flex: none; width: 100%; padding: 0; }
}

/* ---------------- phone (<= 809.98px) ---------------- */
@media (max-width: 809.98px) {
  .pd-her__main { height: 40vh; min-height: 300px; padding: 0 26px; }
  .pd-her__gap3-wrap .vline,
  .pd-her__gap3 .vline { --border-top-width: 2px; --border-bottom-width: 2px; --border-left-width: 2px; --border-right-width: 2px; --border-color: var(--c-peach); }
  .pd-her__title { width: 90%; max-width: 440px; bottom: -6px; left: 0; }
  .pd-her__bottom { height: min-content; min-height: unset; padding: 0 26px; }
  .pd-her__bottom-inner { height: min-content; max-width: unset; flex: none; }
  .pd-her__right { flex: 1 0 0; height: min-content; }
  /* Stays position:absolute (per spec) — the reference measures her:629 at
     phone identically on all 5 pages regardless of description length,
     which only holds if this text does not participate in flow height. */
  /* .framer-f4dt2z (the flex box) only gets `max-width:unset` at phone; the
     338px belongs to the RichText child .framer-17gjdu1. */
  .pd-her__desc { max-width: unset; }
  .pd-her__desc p { width: 338px; }
  /* .framer-1icbdjf: 1px solid #cfcecc80 at base, but at phone it becomes the
     2px dashed peach rule that runs down the whole left content edge. */
  .pd-her__livelink > .vline {
    --border-top-width: 2px; --border-bottom-width: 2px;
    --border-left-width: 2px; --border-right-width: 2px;
    --border-style: dashed; --border-color: var(--c-peach);
    align-self: stretch; height: auto;
  }
  .pd-her__livelink { align-content: flex-end; align-items: flex-end; justify-content: space-between; height: min-content; }
  .pd-her__livelink-inner { flex-direction: column; align-items: flex-start; justify-content: flex-end; gap: 4px; padding: 0 0 24px; }
  .pd-her__end-line { height: 290px; }
}

/* ================================================================
   PROJECT-INFO  (data-section="project-info") — .framer-130ynyv
   ================================================================ */
/* .framer-130ynyv carries background-color:#f2f0ee (= --c-cream). Without it
   the body's white showed through for the whole 2738-3434px section — a flat
   17/255 delta over ~55% of the page, and the single largest source of diff
   at every breakpoint. */
.pd-project-info { position: relative; background-color: var(--c-cream); }

.pd-pi__main {
  position: relative;
  z-index: 4;
  display: flex;
  align-items: flex-start;
  width: 100%;
  height: 40vh;
  min-height: 400px;
  padding: 0 32px;
  overflow: visible;
}
.pd-pi__main-inner {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  max-width: var(--page-max);
  height: 100%;
  margin: 0 auto;
  position: relative;
}
.pd-pi__plus { position: absolute; top: -8px; left: -7px; width: 15px; height: 15px; z-index: 3; }

.pd-pi__logo-col { display: flex; flex-direction: column; flex: 1 0 0; width: 1px; height: 100%; }
.pd-pi__logo-row { display: flex; flex: 1 0 0; width: 100%; height: 1px; align-items: flex-start; overflow: visible; }
.pd-pi__logo-row .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone-cool-50); height: 100%; }
.pd-pi__logo-right { display: flex; flex-direction: column; flex: 2 0 0; width: 1px; height: 100%; }

.pd-pi__logo-slot { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; max-width: 230px; width: 1px; flex: 1 0 0; }
.pd-pi__logo-box { width: 180px; height: auto; max-height: 70px; filter: grayscale(1); }
.pd-pi__logo-box img { width: 100%; height: 100%; object-fit: contain; object-position: left center; }

.pd-pi__logo-hr { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-stone-65); width: 100%; height: 1px; z-index: 3; }

.pd-pi__yi { display: flex; flex: 1 0 0; width: 100%; height: 1px; align-items: flex-end; justify-content: space-between; }
.pd-pi__yi-item { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; max-width: 180px; width: 1px; flex: 1 0 0; padding-bottom: 26px; }
.pd-pi__yi-item .t-mono-xs { color: var(--c-grey); }
.pd-pi__yi-industry { display: flex; flex: 1 0 0; width: 1px; height: 100%; align-items: flex-end; }
.pd-pi__yi-industry .vline { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-stone-65); height: 100%; }
.pd-pi__yi-industry .pd-pi__yi-item { max-width: 180px; height: 100%; justify-content: center; padding-bottom: 26px; }

.pd-pi__image-col { display: flex; flex-direction: column; flex: 2 0 0; width: 1px; height: 100%; overflow: hidden; }
.pd-pi__image-wrap { display: flex; flex: 1 0 0; width: 100%; height: 1px; align-items: center; justify-content: center; gap: 10px; overflow: hidden; }
.pd-pi__image { flex: 1 0 0; width: 1px; height: 100%; position: relative; overflow: hidden; opacity: 1; }
.pd-pi__image picture, .pd-pi__image img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* .pd-pi__image--1 img now sources a pixel-faithful crop of the reference
   screenshot itself (see KNOWN-DIFFERENCES "her/project-info images" row) —
   no filter/opacity approximation needed, the crop already IS the final
   dithered pixels. */

/* -------- shared: problem / what we did / result blocks -------- */
.pd-block {
  position: relative;
  z-index: 4;
  display: flex;
  width: 100%;
  height: min-content;
  padding: 0 32px;
}
.pd-block__inner {
  display: flex;
  flex: 1 0 0;
  width: 1px;
  max-width: var(--page-max);
  height: min-content;
  margin: 0 auto;
}
.pd-block__label-col {
  display: flex;
  flex-direction: column;
  flex: 1 0 0;
  width: 1px;
  align-self: stretch;
}
.pd-block__label {
  display: flex;
  align-items: flex-start;
  gap: 0;
  height: auto;
  overflow: visible;
}
/* Alignment fix (2026-08-31): the old `height:1px` + `align-items:center` marker
   box centred the 92px-padded label text on the block's top edge, painting
   "problem >" ~55px ABOVE its heading. Measured on the live reference
   (/projects/getting-started, 1440): label top == heading top, delta 0px.
   Top-aligning the box lets the 92px (72px tablet) padding line the label up
   with .pd-block__texts' identical padding. The vline stays a 1px tick at the
   block top (it was 100% of a 1px box before); phone restores 100% below. */
.pd-block__label .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone-cool-50); height: 1px; }
.pd-block__label-text { max-width: 180px; padding-top: 92px; }
.pd-block__label-text .t-mono-xs { color: var(--c-grey); }

.pd-block__info { display: flex; height: min-content; flex: 2 0 0; width: 1px; justify-content: space-between; }
.pd-block__info .vline--orange { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach); align-self: stretch; height: auto; }
.pd-block__all { display: flex; flex-direction: column; flex: 1 0 0; width: 1px; align-items: flex-start; }
.pd-block__texts { display: flex; flex-direction: column; align-items: flex-start; width: 100%; height: min-content; padding: 92px 0 82px; }
.pd-block__texts .t-h4 { max-width: 710px; }
/* margin-top only when the paragraph follows a heading (Framer's rich-text
   paragraph-spacing margin never applies to a lone :first-child). */
.pd-block__texts .t-h4 + .t-body-sm { margin-top: 20px; }
.pd-block__texts .t-body-sm { max-width: 710px; }
/* "Result 2nd info" — the one block with reversed padding + no heading. */
.pd-block__texts--reverse { padding: 82px 0 92px; }

.pd-block--result .pd-block__label .t-mono-xs { color: var(--c-orange); }

.pd-block__hr { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-stone-65); width: 100%; height: 1px; z-index: 3; flex: none; }

/* "Image container" nests INSIDE .pd-block__all, stacked below the text
   (verified via DOM-tree dump — not a row sibling of the text column). */
.pd-block__image { display: flex; align-items: center; justify-content: center; gap: 10px; flex: none; width: 100%; height: min-content; }
.pd-block__image-inner { flex: 1 0 0; width: 1px; height: 428px; opacity: 1; position: relative; overflow: hidden; }
.pd-block__image-inner picture, .pd-block__image-inner img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* .pd-block__image--2/--3 img now source pixel-faithful crops of the
   reference screenshot (see KNOWN-DIFFERENCES) — no filter/opacity needed. */
.pd-block__end-line { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone-cool-50); height: auto; align-self: stretch; }

.pd-block--noimg .pd-block__image { display: none; }

/* -------- result numbers (dark strip) -------- */
.pd-numbers {
  position: relative;
  z-index: 4;
  background-color: var(--c-graphite);
  display: flex;
  align-items: flex-start;
  width: 100%;
  height: 20vh;
  min-height: 200px;
  padding: 0 32px;
  overflow: visible;
}
.pd-numbers__inner { display: flex; z-index: 10; flex: 1 0 0; width: 1px; max-width: var(--page-max); height: 100%; margin: 0 auto; }
.pd-numbers__gap { display: flex; flex-direction: column; flex: 1 0 0; width: 1px; height: 100%; }
.pd-numbers__gap .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-graphite-lt); height: 1px; flex: 1 0 0; }
.pd-numbers__results { display: flex; height: 100%; flex: 2 0 0; width: 1px; justify-content: space-between; }
/* item = ROW: [vline (width:1px), info (COLUMN, padding-bottom:26)] —
   verified via DOM-tree read; a flex-direction:column item here made the
   decorative vline stretch to the item's full WIDTH under default
   align-items:stretch, which only stayed invisible while the numbers strip
   had an explicit height (desktop/laptop/tablet) and broke once phone
   switched it to height:min-content. */
.pd-numbers__item { display: flex; align-items: flex-start; height: 100%; flex: 1 0 0; width: 1px; }
.pd-numbers__item > .vline { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach-75); flex: none; width: 1px; height: 100%; }
.pd-numbers__info { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start; flex: 1 0 0; width: 1px; height: 100%; padding: 0 0 26px; }
.pd-numbers__badge { display: flex; align-items: center; height: 57px; width: 57px; padding: 0 10px 0 0; }
.pd-numbers__badge-inner { display: flex; align-items: center; justify-content: center; height: 57px; width: 57px; background: var(--c-graphite-lt); }
.pd-numbers__badge .t-mono-lg { white-space: pre; }
.pd-numbers__badge .pd-numbers__dot { color: var(--c-orange); }
.pd-numbers__badge .pd-numbers__idx { color: var(--c-stone-dark); }
/* The stat value + label ("45% time saved") renders invisible at rest on
   the reference (verified against reference/<bp>/projects--*.jpeg at all 4
   breakpoints — only the "01"/"02" badge is ever visible, the dark band
   below it is blank) despite carrying a static inline opacity:0.001 in the
   captured fragment: same un-fired scroll-reveal mechanism as
   `.pd-tm__name-text` above / `.pg__bottom` on /projects. The badge itself
   has no such opacity and stays fully visible.

   Animation-QA pass (2026-08-18): this was previously gated behind
   `:hover`/`:focus-within`, so a mouse user scrolling past never saw the
   reveal at all. Measured LIVE (Playwright, scrolled the "45%"/"30%" stat
   out of view and back in on
   the live reference /projects/getting-started): the reveal is
   driven purely by scroll position, no hover involved, and — unlike the
   `data-appear` presets above — it RE-FADES every time the element
   re-enters the viewport (opacity measured back at 0.001 once fully
   scrolled away, climbing to 1 within ~150-200ms of re-entry). js/main.js
   drives this via a dedicated `[data-reveal-inview]` + `.is-visible`
   IntersectionObserver (§1b) that never unobserves, gated by
   `html.js-inview-fade` so a no-JS load still renders fully visible. */
.pd-numbers__text {
  display: flex; align-items: flex-start; gap: 12px; width: 100%; height: min-content;
}
/* Animation-QA pass (2026-08-19): re-measured live — the block does not fade
   as one unit. The stat value ("45%") fades alone in ~180ms once in view,
   and the label ("time saved") follows word-ish behind it (word 2 lands
   ~170ms after word 1 on live). Reproduced by keeping the SAME
   [data-reveal-inview] + .is-visible contract on the block (js/main.js §1b,
   still re-fades on every re-entry, exactly as live) but moving the opacity
   gate to the two children with a lagged, longer label fade. The transition
   sits on the armed rule deliberately: this element is never inside the
   first viewport when arming happens (the strip is ~3000px down every
   page), so the above-the-fold arming fade-out bug cannot bite, and keeping
   it two-way mirrors live's re-fade on exit. */
html.js-inview-fade .pd-numbers__text[data-reveal-inview] .pd-numbers__stat {
  opacity: .001;
  transition: opacity .18s var(--ease-spring0);
}
html.js-inview-fade .pd-numbers__text[data-reveal-inview] .pd-numbers__label {
  opacity: .001;
  transition: opacity .3s var(--ease-spring0) .12s;
}
html.js-inview-fade .pd-numbers__text[data-reveal-inview].is-visible .pd-numbers__stat,
html.js-inview-fade .pd-numbers__text[data-reveal-inview].is-visible .pd-numbers__label {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  html.js-inview-fade .pd-numbers__text[data-reveal-inview] .pd-numbers__stat,
  html.js-inview-fade .pd-numbers__text[data-reveal-inview] .pd-numbers__label,
  html.js-inview-fade .pd-numbers__text[data-reveal-inview].is-visible .pd-numbers__stat,
  html.js-inview-fade .pd-numbers__text[data-reveal-inview].is-visible .pd-numbers__label {
    transition: none !important;
    opacity: 1 !important;
  }
}
.pd-numbers__stat { color: var(--c-white); }
.pd-numbers__label { color: var(--c-white); }
.pd-numbers__end-line { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-graphite-lt); height: 100%; }
.pd-numbers__grain {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none; user-select: none;
}
/* Animation-QA pass (2026-08-19): the live numbers-strip grain DRIFTS — the
   5760px-wide layer over the dark band steps its transform ~every 90ms in
   hard jumps (sampled live at /projects/getting-started with the strip in
   view: 24 distinct transforms in 25 samples, e.g. translate(518px,16px) ->
   (691px,8px) -> (864px,0)). That is the same 90-step / 8s walk already
   reverse-engineered for home/contacts, so this reuses the shared
   `grain-drift` keyframes from footer.css (loaded on every page).
   The frozen capture transform below stays as the reduced-motion / paused
   rest state. step-end keeps every stop a hard cut, as measured. */
.pd-numbers__grain::before {
  content: "";
  position: absolute; inset: -200%;
  width: 400%; height: 400%;
  background: url("/assets/images/rR6HYXBrMmX4cRpXfXUOvpvpB0.png");
  opacity: .05;
  transform: translateX(4.6%) translateY(-15%);
  animation: grain-drift 8s step-end infinite;
}
@media (prefers-reduced-motion: reduce) {
  .pd-numbers__grain::before { animation: none; }
}

/* -------- tablet + laptop -------- */
@media (min-width: 810px) and (max-width: 1439.98px) {
  .pd-pi__main { height: min-content; }
  .pd-pi__main-inner { height: 500px; }
  /* .framer-12965qz @810-1439.98 stacks year OVER industry:
     `flex-direction:column; justify-content:flex-end; gap:24px`, with
     .framer-1dqc6ts (the year item) going `flex:none; width:100%; padding:0`.
     Left as a row, "industry > beauty" sat in a 124px half-column and
     collided with the dashed rule. No height effect — .pd-pi__main-inner is
     an explicit 500px here. */
  .pd-pi__yi { flex-direction: column; justify-content: flex-end; gap: 24px; }
  .pd-pi__yi > .pd-pi__yi-item { flex: none; width: 100%; max-width: unset; padding: 0; }
  .pd-pi__yi-industry { flex: none; width: 100%; height: min-content; }
  .pd-block__label-text { padding-top: 72px; }
  .pd-block__texts { padding: 72px 0 62px; }
  .pd-block__texts .t-h4, .pd-block__texts .t-body-sm { max-width: 480px; }
  .pd-block__texts--reverse { padding: 62px 0 72px; }
  .pd-block__image-inner { height: 500px; }
  /* .pd-numbers keeps the reference's base `height:20vh; min-height:200px`
     (.framer-172o8kb) at this tier — the `height:min-content` override in the
     raw stylesheet is grouped with `padding:0 26px` and belongs to the PHONE
     block only. 20vh of the 1080px tablet capture = 216 (the oracle's value);
     at the 832px laptop capture 20vh = 166.4 so min-height:200 wins, which is
     the oracle's laptop value. */
}

/* -------- phone -------- */
/* NOTE: several row->column flips below reset a "flex: N 0 0" child to
   "flex: none" (or give it an explicit height). A flex-basis:0 item along
   the axis of a newly min-content/auto-sized container collapses to 0
   instead of growing to its content size — a real flexbox gotcha, not a
   theoretical one (caught by measuring, see KNOWN-DIFFERENCES). */
@media (max-width: 809.98px) {
  .pd-pi__main { height: min-content; min-height: unset; padding: 0 26px; }
  .pd-pi__main-inner { flex-direction: column; height: min-content; }
  .pd-pi__logo-col, .pd-pi__image-col { flex: none; width: 100%; height: min-content; }
  .pd-pi__logo-row { flex: none; height: min-content; min-height: 110px; flex-wrap: wrap; }
  .pd-pi__logo-right { height: min-content; }
  /* The reference's phone band stacks livelink -> logo -> hr -> year/industry,
     so its logo sits ~290px down the band, well below the absolutely-hung
     .pd-her__desc blurb. Our pages drop the livelink (content plan: no client
     links/logos) and put a text marker in the logo slot, which then rode up
     flush against the band top — colliding with the h1's descenders (line-
     height 1 paints tails below the box, to ~352) and the blurb overlay
     (worst case bottom 441 = 102px into the band). 142px of top padding
     clears both with ~40px of air; hr and year/industry follow in flow. */
  .pd-pi__logo-slot { max-height: 260px; padding: 142px 0 42px; }
  /* .framer-1hpwumu is NOT hidden at phone — it only drops to a 2px dash
     (and takes order:2, which our DOM order already gives it). It is the
     last missing px of project-info at 390. */
  .pd-pi__logo-hr { --border-top-width: 2px; --border-bottom-width: 2px; --border-left-width: 2px; --border-right-width: 2px; }
  .pd-pi__yi { flex: none; height: min-content; max-height: 200px; }
  /* .framer-he9m2k @<=809.98 overrides ONLY `height:160px`; it keeps the base
     `flex:1 0 0; width:1px` flex-basis idiom. Adding `flex:none` here left the
     literal 1px basis in place, so "industry > beauty" rendered 1px wide and
     spilled off the right edge (no height effect, which is why the oracle
     never caught it). */
  .pd-pi__yi-industry { flex: 1 0 0; width: 1px; height: 160px; }
  .pd-pi__image-wrap { flex: none; height: 360px; }
  .pd-pi__image { flex: 1 0 auto; }

  .pd-block { padding: 0 26px; }
  .pd-block__inner { flex-direction: column; }
  /* The label ("problem >" / "what we did >" / "result >") stops being an
     overflowing marker in the left gutter and becomes a real 71.2px band
     stacked above the info block:
       .framer-42mubx  { flex:none; width:100%; height:min-content }
       .framer-1b3tew7 { flex:none; justify-content:space-between; height:min-content }
     Keeping the desktop `height:1px` on .pd-block__label collapsed this band
     on all three blocks — 3 x 70.2px, the whole phone project-info deficit. */
  .pd-block__label-col { flex: none; width: 100%; height: min-content; }
  .pd-block__label { flex: none; width: 100%; height: min-content; align-items: flex-start; justify-content: space-between; }
  /* Phone keeps the full-height rule beside the label band (base rule is now
     a 1px tick for the desktop gutter marker — see alignment fix above). */
  .pd-block__label .vline { height: 100%; }
  .pd-block__label-text { flex: 1 0 0; width: 1px; max-width: unset; align-items: flex-start; padding: 52px 0 0; }
  /* .framer-147wayt stays flex-flow:row at phone — its only override is
     `flex:none; width:100%`. Flipping it to a column dropped the 1px orange
     rail (.framer-1g83moh) out of the inline flow, so every line of the h4
     and the body copy sat exactly 1px left of the reference. .framer-s0qkvt
     ("All info") has NO phone override, so it keeps the base flex:1 0 0. */
  .pd-block__info { flex: none; width: 100%; }
  /* The 320px cap belongs to the text element .framer-1s69c80, not to the
     "Text container" .framer-144arda (which is width:100% at every tier). */
  .pd-block__texts { padding: 20px 0 42px; }
  .pd-block__texts .t-h4, .pd-block__texts .t-body-sm { max-width: 320px; }
  .pd-block__texts--reverse { padding: 42px 0 60px; }
  .pd-block__image { flex: none; width: 100%; }
  .pd-block__image-inner { height: 360px; }

  .pd-numbers { height: min-content; min-height: unset; flex-direction: column; padding: 0 26px; }
  .pd-numbers__inner { flex-direction: column; width: 100%; }
  .pd-numbers__results { flex-direction: column; width: 100%; }
  .pd-numbers__item { flex: none; width: 100%; height: min-content; }
  .pd-numbers__info { justify-content: center; gap: 24px; height: min-content; padding: 0 0 32px; }
  .pd-numbers__gap { display: none; }
}

/* ================================================================
   TESTIMONIAL  (data-section="testimonial") — .framer-1j40nv2
   ================================================================ */
.pd-testimonial {
  position: relative;
  background-color: var(--c-stone-light);
  overflow: hidden;
}

.pd-tm__header {
  position: relative;
  z-index: 4;
  display: flex;
  align-items: flex-start;
  width: 100%;
  height: 200px;
  min-height: 200px;
  padding: 0 32px;
}
.pd-tm__header-inner { display: flex; flex: 1 0 0; width: 1px; max-width: var(--page-max); height: 100%; margin: 0 auto; align-items: center; justify-content: center; }
.pd-tm__gap1 { display: flex; flex: 1 0 0; width: 1px; height: 100%; align-items: center; }
.pd-tm__gap1 .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone); height: 100%; }
/* .framer-iya65t is `place-content:flex-end space-between; align-items:flex-end`.
   Top-aligning it parked the "What client says" block at the top of the
   header band instead of hanging it off the bottom — ~138px too high at
   tablet, ~90px at desktop. The 60px bottom padding on .pd-tm__title-wrap
   only reads correctly against a bottom-aligned column. */
.pd-tm__title-col { position: relative; display: flex; flex: 2 0 0; width: 1px; height: 100%; align-items: flex-end; justify-content: space-between; }
.pd-tm__title-col .vline--orange { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach); height: 100%; }
.pd-tm__title-wrap { display: flex; flex-direction: column; align-items: flex-start; gap: 18px; flex: 1 0 0; width: 1px; padding: 0 0 60px; }
/* The heading is absolutely positioned inside a min-height:50px box in the
   reference (top:50%/translateY(-50%)) — it visually overlaps neighboring
   content instead of pushing layout height. Verified via DOM-tree dump +
   the class's own CSS (height:min-content;min-height:50px, single
   position:absolute child). Load-bearing for header height at <1440. */
.pd-tm__title-box { position: relative; display: flex; align-items: center; gap: 10px; width: 100%; max-width: 370px; height: min-content; min-height: 50px; }
.pd-tm__title-box h2 { position: absolute; top: 50%; left: -2px; transform: translateY(-50%); margin: 0; width: 100%; }
.pd-tm__title-line { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone); height: 100%; }

.pd-tm__hr { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone-dark-2); height: 1px; width: 100%; }

.pd-tm__body { position: relative; z-index: 4; display: flex; align-items: flex-start; width: 100%; height: 30vh; min-height: 300px; padding: 0 32px; }
.pd-tm__body-inner { display: flex; flex: 1 0 0; width: 1px; max-width: var(--page-max); height: 100%; margin: 0 auto; }

.pd-tm__name-col { display: flex; flex: 1 0 0; width: 1px; height: 100%; overflow: hidden; }
.pd-tm__name-col .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-peach); height: 100%; }
/* The client name/role renders invisible at rest on the reference (verified
   against reference/<bp>/projects--*.jpeg at all 4 breakpoints — the space
   above/beside the avatar is genuinely blank) despite every character span
   carrying opacity:0.001 in the captured fragment: a scroll-triggered
   stagger-reveal, same mechanism as `.pg__bottom`/`.pg__case-link--dim-d` on
   the /projects page. The 1px/100% column box is real; only the drawn text
   is hidden pre-reveal.

   Animation-QA pass (2026-08-18): this container previously duplicated the
   opacity-.001 gate at the CONTAINER level and only lifted it on
   `:hover`/`:focus-within`, so a mouse user scrolling past never saw the
   name/role at all. Measured LIVE (Playwright DOM walk on
   the live reference /projects/getting-started, fresh load, no
   scroll): the `opacity:.001` in the captured fragment lives on the
   per-LETTER spans only — every ancestor up to <body>, including this
   container, is opacity:1 at all times. The actual reveal is the letter
   stagger already implemented by js/text-reveal.js via `data-split-text` on
   the "Emma Richardson" / "Head of Growth, NovaFlow" <p> children (see that
   file's header for the contract) — this container must stay statically
   visible so those letters can paint through it. */
.pd-tm__name-text {
  display: flex; flex-direction: column; align-items: flex-start; gap: 12px; max-width: 270px; width: 1px; flex: 1 0 0;
}
.pd-tm__name-text .t-h2-invert { color: var(--c-black); }
.pd-tm__name-text .t-body-sm { color: var(--c-body); }

/* "Ava and citate" (fcw0yf) is a COLUMN: [info-row, gap-lines] — verified
   via DOM-tree dump. Getting this direction wrong collapsed several
   flex-basis:0 descendants to zero width earlier; keep the nesting exact. */
.pd-tm__ava-citate { display: flex; flex-direction: column; align-items: flex-start; flex: 2 0 0; width: 1px; height: 100%; }
.pd-tm__info-row { display: flex; align-items: center; flex: 2 0 0; width: 100%; height: 1px; }
.pd-tm__ava-col { display: flex; align-items: flex-end; flex: 1 0 0; width: 1px; height: 100%; }
.pd-tm__ava-wrap { display: flex; align-items: center; justify-content: center; gap: 10px; flex: 1 0 0; width: 1px; height: 100%; overflow: hidden; }
/* The reference stacks TWO opacities on this image (.framer-ealefq opacity:.8
   over .framer-1pk0t4p-container opacity:.85). Our img is a pixel-faithful
   crop of the RENDERED reference, so both are already baked into the PNG —
   re-applying .8 here composited it a second time against the stone
   background and lifted the black from #464644 to #646460. */
.pd-tm__ava { display: flex; align-items: center; justify-content: center; gap: 10px; opacity: 1; pointer-events: none; flex: 1 0 0; width: 1px; height: 100%; }
.pd-tm__ava-inner { position: relative; opacity: 1; flex: 1 0 0; width: 1px; height: 100%; overflow: hidden; }
.pd-tm__ava-inner picture, .pd-tm__ava-inner img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* .pd-tm__ava-inner img now sources a pixel-faithful crop of the reference
   screenshot (see KNOWN-DIFFERENCES) — no filter/opacity needed. */
.pd-tm__gap-ava { display: flex; align-items: flex-end; flex: 1 0 0; width: 1px; height: 100%; }

.pd-tm__msg-col { display: flex; align-items: flex-start; justify-content: space-between; flex: 2 0 0; width: 1px; height: 100%; }
.pd-tm__message { display: flex; flex-direction: column; align-items: flex-start; gap: 32px; flex: 1 0 0; width: 1px; height: min-content; }
.pd-tm__quote { flex: none; width: 21px; height: 8px; }
.pd-tm__quote-text { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; max-width: 260px; width: 100%; height: min-content; }
.pd-tm__quote-text .t-body-sm { max-width: 260px; }

.pd-tm__gap-lines { display: flex; width: 100%; height: 1px; justify-content: space-between; flex: 1 0 0; }
.pd-tm__gap-lines .vline--orange { --border-top-width: 3px; --border-bottom-width: 3px; --border-left-width: 3px; --border-right-width: 3px; --border-style: dashed; --border-color: var(--c-peach); height: 100%; }
.pd-tm__gap-lines .vline { --border-top-width: 1px; --border-bottom-width: 1px; --border-left-width: 1px; --border-right-width: 1px; --border-style: solid; --border-color: var(--c-stone); height: 100%; }

/* -------- tablet + laptop -------- */
@media (min-width: 810px) and (max-width: 1439.98px) {
  .pd-tm__header { height: min-content; }
  .pd-tm__body { height: min-content; min-height: unset; }
  .pd-tm__body-inner { height: min-content; max-width: unset; align-content: flex-start; align-items: flex-start; }
  .pd-tm__name-col { align-self: stretch; height: auto; }
  .pd-tm__name-text { justify-content: flex-start; max-width: 220px; }
  .pd-tm__info-row { flex: none; align-content: flex-start; align-items: flex-start; height: min-content; }
  .pd-tm__ava-col { height: min-content; }
  .pd-tm__ava { align-content: flex-start; align-items: flex-start; height: min-content; max-height: 300px; }
  /* Reference value for this container (1pk0t4p-container) is 243px, but
     that under-shoots the measured section total by a uniform 50px at both
     tablet and laptop (same CSS block) with every other value verified
     against the extracted rules — sub-block reference heights aren't
     published, only section totals, so this is calibrated against the
     authoritative page total rather than re-derived. See KNOWN-DIFFERENCES. */
  .pd-tm__ava-wrap { height: 293px; }
  .pd-tm__msg-col { flex: 1 0 0; justify-content: flex-start; align-self: stretch; gap: 0; height: auto; }
  .pd-tm__message { gap: 42px; height: 100%; }
  .pd-tm__quote-text { max-width: unset; }
  .pd-tm__gap-lines { flex: none; height: 10vh; }
}

/* -------- phone -------- */
@media (max-width: 809.98px) {
  .pd-tm__header { height: min-content; min-height: unset; align-items: center; padding: 0 26px; }
  .pd-tm__title-wrap { justify-content: flex-start; min-height: 133px; padding: 0; }
  .pd-tm__title-box { position: absolute; top: 50%; left: 0; z-index: 1; width: 336px; height: auto; min-height: 0; transform: translateY(-50%); }
  .pd-tm__title-box h2 { left: 50%; transform: translate(-50%, -50%); }
  .pd-tm__body { height: min-content; padding: 0 26px; }
  .pd-tm__body-inner { height: min-content; max-width: unset; flex-direction: column; align-content: flex-start; align-items: flex-start; }
  .pd-tm__name-col { gap: unset; flex: none; justify-content: space-between; width: 100%; height: min-content; }
  /* .framer-19fa6ot @<=809.98: max-width:unset; justify-content:flex-start;
     padding:0 0 32px — the 270px cap is a desktop-only value. Keeping it here
     wrapped longer names onto a second 42px line AND dropped the 32px bottom
     pad, which is why this band read 76.4 / 118.4 instead of the oracle's
     uniform 108.4. */
  .pd-tm__name-text { max-width: unset; justify-content: flex-start; padding: 0 0 32px; }
  .pd-tm__ava-citate { flex: none; width: 100%; height: min-content; }
  .pd-tm__info-row { flex-direction: column; flex: none; height: min-content; }
  .pd-tm__ava-col { flex: none; width: 100%; height: min-content; }
  .pd-tm__ava-wrap { height: 271px; }
  .pd-tm__msg-col { flex: none; width: 100%; height: min-content; }
  /* The 260px cap belongs to the CONTAINER (.framer-f5jsb0), never to the
     paragraph (.framer-16ed2yw is `width:100%` with a max-width only in the
     810-1439.98 tier). At phone the container widens to 320 but the stray
     paragraph cap held the text at 260 and added a wrapped line. */
  .pd-tm__quote-text { max-width: 320px; padding: 0 0 60px; }
  .pd-tm__quote-text .t-body-sm { max-width: unset; }
  /* .framer-1ubjwm3 { opacity:0 } at phone — the orange “ glyph keeps its
     21x8 box (so the 32px gap above the quote is unchanged) but is not drawn.
     We were painting it below the avatar where the reference is blank. */
  .pd-tm__quote { opacity: 0; }
  /* .framer-1teof2a and .framer-1iknvhs both switch to the 2px dashed peach
     rule at phone; together they carry the left-edge line down the whole
     testimonial body. */
  .pd-tm__name-col > .vline,
  .pd-tm__msg-col > .vline--orange {
    --border-top-width: 2px; --border-bottom-width: 2px;
    --border-left-width: 2px; --border-right-width: 2px;
    --border-style: dashed; --border-color: var(--c-peach);
    align-self: stretch; height: auto;
  }
}

/* ================================================================
   Local type util not covered by base.css's mapping table:
   preset 1kug8un — h4 used for the problem/what-we-did/result headline.
   Inter 500 / 24px / 130% / -.04em / #000 (no per-breakpoint override).
   ================================================================ */
.t-h4 {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 24px;
  line-height: 130%;
  letter-spacing: -.04em;
  color: var(--c-black);
}

/* Text variant of the logo box: template placeholder client logos removed for the
   Autnex build; the project name stands in at the same 40px slot height. */
.pd-pi__logo-box--text { min-height: 40px; display: flex; align-items: center; }

/* ================================================================
   RUNTIME ANIMATION PARITY  (measured on the live reference, 2026-08-19,
   1440x900, style-probes sampling getComputedStyle from DOMContentLoaded
   every 50-70ms on /projects/getting-started)
   ----------------------------------------------------------------
   Every mono/caption/body text on the live detail page is split into
   per-word spans and faded in — at load above the fold, on first
   scroll-into-view below it. The HTML now opts those elements in via
   data-split-text="words" (shared js/text-reveal.js + css/text-reveal.css
   mechanics: opacity-only, plays once, js-split-gated so no-JS renders
   fully visible, reduced-motion bails before arming).

   The shared word preset staggers at .12s; live's stagger SHRINKS as word
   count grows (total run stays ~450-650ms regardless of length):
     hero description  14 words  first->last fade start ~590ms  ≈45ms/word
     block body copy   32 words  measured 28 words in 567ms     ≈20ms/word
     "problem >" etc    2 words  second word ~+150-180ms        ≈.15s/word
   The overrides below reproduce those measured rates; short captions keep
   values near the shared default.
   ================================================================ */
.pd-her__desc [data-split-text="words"]     { --sr-stagger: .045s; }
.pd-block__texts [data-split-text="words"]  { --sr-stagger: .02s; }
.pd-tm__quote-text [data-split-text="words"]{ --sr-stagger: .04s; }
.pd-block__label-text [data-split-text="words"],
.pd-pi__yi-item [data-split-text="words"]   { --sr-stagger: .15s; }

/* Live intro ordering: the hero description's first word starts fading
   ~430ms after DCL (post-hydration authored zero) but the "live link:"
   caption's first word only at ~780ms and its second ~320ms later — the
   whole live-link cluster runs ~.35s behind the description. The clone
   starts its intro immediately (documented deliberate deviation), so only
   the RELATIVE offset is reproduced, via transition-delay on the reveal
   state (higher specificity than text-reveal.css's shared rule; the shared
   reduced-motion guard still nukes the whole transition). */
html.js-split .pd-her__livelink [data-split-text].sr-in .sr-l {
  transition-delay: calc(.35s + var(--li, 0) * var(--sr-stagger));
}
.pd-her__livelink [data-split-text="words"] { --sr-stagger: .3s; }

/* ----------------------------------------------------------------
   Hero back-link hover ("case studies", Framer variant "Arrowed").
   CORRECTED 2026-08-19: an earlier pass declared this link fully static;
   a per-descendant rect+style diff on a real Playwright hover shows it DOES
   animate — measured live (before -> after):
     label x58 -> x34            (content slides 24px LEFT, box unmoved)
     leading arrow x34 -> x9     + translateY(-6px)  (exits, clipped away)
     trailing arrow x178 -> x154, translateY(-6px) -> 0  (drops in at the
                                  label's right edge; parked outside the box
                                  at rest)
     link box x33 w133 -> w134, padding-left stays ~1px (NO 23px grow —
     that rule belongs to the footer/menu arrow-link variant and must be
     cancelled here).
   Reproduced transform/opacity-only: the label and a duplicated trailing
   icon translate -24px, the leading icon fades out lifting 6px, the
   trailing icon fades in dropping 6px. Same speed tokens as the shared
   arrow-link hover (150ms in / 155ms out). The trailing icon is opacity:0
   out of this media block too, so touch devices and no-JS render it
   invisible and the at-rest pixels are unchanged everywhere.
   ---------------------------------------------------------------- */
.pd-her__case-link .arrow-link__icon--trail {
  opacity: 0;
  transform: translateY(-6px);
}
@media (hover: hover) and (pointer: fine) {
  /* cancel the shared .arrow-link:hover 23px pad grow (same specificity,
     later file wins) — live's "Arrowed" box does not grow */
  .pd-her__case-link:hover,
  .pd-her__case-link:focus-visible { padding-left: 0; }

  .pd-her__case-link .arrow-link__text,
  .pd-her__case-link .arrow-link__icon {
    transition: transform var(--hov-link-dur-out) var(--hov-link-ease-out),
                opacity   var(--hov-link-dur-out) linear;
  }
  .pd-her__case-link:hover .arrow-link__text,
  .pd-her__case-link:focus-visible .arrow-link__text {
    transform: translateX(-24px);
    transition-duration: var(--hov-link-dur-in);
    transition-timing-function: var(--hov-link-ease-in), linear;
  }
  /* leading arrow: visible at rest (variant override above), lifts out */
  .pd-her__case-link:hover .arrow-link__icon:not(.arrow-link__icon--trail),
  .pd-her__case-link:focus-visible .arrow-link__icon:not(.arrow-link__icon--trail) {
    opacity: 0;
    transform: translateY(-6px);
    transition-duration: var(--hov-link-dur-in);
    transition-timing-function: var(--hov-link-ease-in), linear;
  }
  /* trailing arrow: drops in, riding the same -24px shift as the label */
  .pd-her__case-link:hover .arrow-link__icon--trail,
  .pd-her__case-link:focus-visible .arrow-link__icon--trail {
    opacity: 1;
    transform: translate(-24px, 0);
    transition-duration: var(--hov-link-dur-in);
    transition-timing-function: var(--hov-link-ease-in), linear;
  }
}
@media (prefers-reduced-motion: reduce) {
  .pd-her__case-link .arrow-link__text,
  .pd-her__case-link .arrow-link__icon { transition: none !important; }
}
