/* =============================================================================
   PRISM II — VOICES. The attribution, the portrait, the carousel, and the
   featured band. Four rules sit under this component and each one was arrived at
   rather than chosen, so each is written down beside the declarations it explains.

   A NAME IS NOT DATA. Every attribution on the previous mock was 13px uppercase
   mono, which is the voice this site reserves for measurement — and uppercasing
   destroys real information: "Dr. Ben Condon" loses its title, "cargo.one" loses
   the lower-case that IS the brand, and "O'Brien" comes back as a shout. A name is
   the human register, so it is set in Newsreader at sentence case. Company and
   role stay in mono, because those two genuinely are metadata about a person.

   PHOTOGRAPHS ARE ROUND AND GREYSCALE (reversed 2026-09-01, Jason — round like a
   social-media profile picture). This is a deliberate exception to "rounded is
   reserved for actions": a portrait reads as a person regardless, so the usual
   risk of a radius reading as a button doesn't apply here the way it would on a
   card or a tile. Greyscale because thirteen portraits carry thirteen different
   colour casts, and colour on thirteen faces at once would be the loudest thing
   on a page that allows exactly one accent per position. The source files are
   100px-250px, so the strip renders them at 48 and the featured band caps at 78;
   anything larger is visibly soft.

   ONE TREATMENT, EVERY COUNT. Three quotes and twelve take the same strip at the
   same cell width. Only --dur changes, so a page carrying more voices reads as a
   longer loop rather than as a different component. The single exception is a page
   carrying exactly one quote, which takes the warm band instead, because a
   carousel of one looks broken rather than restrained.

   MOTION YIELDS COMPLETELY. This carousel is the only authored motion on the whole
   site, so under reduced motion it does not merely freeze in place — it becomes a
   native overflow-x snap scroller with the duplicated set hidden, which is the
   same content under a thumb instead of under a timer. site_00's global
   `animation: none` stops the track; the block at the end of this file is what
   makes the stopped strip usable.

   THE PORTRAITS ARE FILES, NOT DATA URIs. The mockup carried twelve ~4,700
   character base64 JPEGs inline, which is roughly 50KB of CSS on every page load
   that no browser can cache separately from the stylesheet. They are twelve
   100px files under app/assets/images/voices now, referenced relatively so
   Propshaft rewrites each one to its digested path.
   ============================================================================= */

/* The section heading is one fixed sentence everywhere now (see the shared
   partial), and it's short enough to read as one line rather than the
   balanced two-line wrap site_00's `.shead h2 { max-width: 22ch }` rule was
   built for longer headings. 22ch would split this 39-character line; this
   override widens the cap so normal desktop widths render it on one line,
   with a narrow phone still free to wrap if it must. */
.voices-h2 { max-width: 40ch; }

/* -----------------------------------------------------------------------------
   A. THE ATTRIBUTION
   -------------------------------------------------------------------------- */
/* THE NAME IS NOT A QUOTATION (2026-09-03), so it is not set in the human
   voice. site_00 declares four homes for Newsreader — the triad's human
   reading, every quotation, About's opening line, and .fd-analysis-quote — and
   the scoping is the entire reason the face means "a person is speaking". A
   speaker's name is attribution: it says who spoke, which is a different job
   from speech, and setting it in the same face made the serif mean "something
   to do with a person" instead. That is a fifth use, and a fifth use is what
   spends the other four. The quote above it keeps Newsreader; the name takes
   the display face at the weight an attribution is read at. */
.tq-name {
  display: block;
  font-family: var(--display);
  font-size: 16px;
  font-weight: 500;
  letter-spacing: -.008em;
  line-height: 1.25;
  color: var(--ink);
}
.tq-role {
  display: block;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .02em;
  color: var(--ink3);
  margin-top: 4px;
}
.tq-co {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--clay);
}

/* The portrait. background-size: cover with the focal point above centre, because
   a head sits in the top third of a square crop before the radius rounds it off.
   --bg3 shows while the file loads and stands in if one is ever missing, so a
   failed image leaves a warm circle rather than a hole. */
.av {
  display: block;
  flex: none;
  border-radius: 50%;
  background-size: cover;
  background-position: 50% 22%;
  background-color: var(--bg3);
  border: 1px solid var(--warm-line);
  filter: grayscale(1) contrast(1.03);
}

.av[data-p="tim"]      { background-image: url("/assets/voices/tim-aaa509e7.jpg"); }
.av[data-p="ruth"]     { background-image: url("/assets/voices/ruth-e46b043a.jpg"); }
.av[data-p="kaitlin"]  { background-image: url("/assets/voices/kaitlin-251e046e.jpg"); }
.av[data-p="josh"]     { background-image: url("/assets/voices/josh-1fd93387.jpg"); }
.av[data-p="alex"]     { background-image: url("/assets/voices/alex-af300654.jpg"); }
.av[data-p="ben"]      { background-image: url("/assets/voices/ben-17657909.jpg"); }
.av[data-p="suen"]     { background-image: url("/assets/voices/suen-ccdbf7a6.jpg"); }
.av[data-p="shirley"]  { background-image: url("/assets/voices/shirley-5c6ed47c.jpg"); }
.av[data-p="danielle"] { background-image: url("/assets/voices/danielle-ede11183.jpg"); }
.av[data-p="sophie"]   { background-image: url("/assets/voices/sophie-5ac3e648.jpg"); }
.av[data-p="hugo"]     { background-image: url("/assets/voices/hugo-7a86c459.jpg"); }
.av[data-p="daniel"]   { background-image: url("/assets/voices/daniel-d5c2e1b7.jpg"); }
.av[data-p="esther"]   { background-image: url("/assets/voices/esther-6cbf034b.jpg"); }
.av[data-p="christie"] { background-image: url("/assets/voices/christie-4b8e3113.jpg"); }

/* -----------------------------------------------------------------------------
   B. THE CAROUSEL, for any page carrying more than one quote.

   The cells run continuously left, pausing on hover and on keyboard focus. The
   set is duplicated once so the loop is seamless, and the duplicate is
   aria-hidden, so a screen reader is read twelve people rather than twenty-four.
   The edges are a mask rather than a painted fade, because three different
   grounds sit behind this component and a painted fade could only match one.

   The keyframe travels half the track plus half the gap, which is what makes the
   duplicated set land exactly where the original started.
   -------------------------------------------------------------------------- */
/* THE EDGE FADE IS 12%, NOT 5% (2026-09-04), AND THE NUMBER IS THE CARD WIDTH.
   A marquee always has a partly-visible cell at each end; the mask is what
   decides whether that reads as a card continuing past the frame or as a card
   sliced through. At 5% the fade was 54px against a 420px cell, so a quotation
   ran at full contrast to within half a word of the edge and then stopped dead
   mid-word — which on a strip of real people's words reads as a rendering
   fault, and it is the first thing the eye lands on because it is the leftmost
   thing in the section. 12% is 129px, a little under a third of a cell, which
   is enough travel for the type to be gone before the frame is. Measured
   against the 420px cap; re-check this if .tcard's clamp changes. */
.tmq {
  position: relative;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
}
.tmq-track {
  display: flex;
  gap: 20px;
  width: max-content;
  animation: tmq-run var(--dur, 48s) linear infinite;
}
@keyframes tmq-run {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(-50% - 10px), 0, 0); }
}
.tmq:hover .tmq-track,
.tmq:focus-within .tmq-track { animation-play-state: paused; }
/* The button's pause is the hover's pause — same property, same place, so the
   two cannot drift into different behaviours. Holding the animation rather than
   a scroll position is what makes the strip stop exactly where it is. */
.tmq.is-paused .tmq-track { animation-play-state: paused; }

/* THE CONTROL SITS ABOVE THE STRIP AND OUT OF THE WAY. It is the data voice,
   because it reports a state rather than asking for the sale — every button on
   this site that wants something is a filled pill, and this one must not read as
   one three lines under a heading. It still clears 44px like every other action.
   Right-aligned so it is adjacent to the motion without interrupting the
   heading's left edge, which the eyebrow, the h2 and the note all share. */
.voices-strip { display: flex; flex-direction: column; align-items: flex-end; }
.voices-strip > .tmq { width: 100%; }
.voices-pause {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 14px;
  margin-left: auto;
  margin-bottom: 4px;
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink3);
  cursor: pointer;
  transition: color .2s, border-color .2s;
}
.voices-pause:hover { color: var(--ink); border-color: var(--ink); }
.voices-pause[hidden] { display: none; }

/* IT RENDERS ONLY WHERE THERE IS MOTION TO STOP. Below 640px and under reduced
   motion the strip is a native snap scroller with no animation running, so a
   pause button would claim to do something it cannot. Hidden in both, matching
   the two blocks below exactly. */
@media (prefers-reduced-motion: reduce) {
  .voices-pause { display: none; }
}
@media (max-width: 640px) {
  .voices-pause { display: none; }
}

.tcard {
  flex: 0 0 clamp(268px, 72vw, 420px);
  border: 1px solid var(--warm-line);
  background: var(--bg2);
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-width: 0;
}
/* 19px Newsreader, on --ink. The previous treatment set the quote at 17px on
   --ink2, which is byte for byte the .body role: a human being rendered at
   exactly the size and ink the studio narrates itself in. */
.tcard q {
  display: block;
  font-family: var(--human);
  font-size: 19px;
  line-height: 1.5;
  color: var(--ink);
  quotes: "\201C" "\201D";
  text-indent: -.42em;
}
.tcard blockquote { margin: 0; }
/* margin-top: auto pins the attribution to the bottom of the cell, so twelve
   cards of uneven quote length still line their faces up along one baseline. */
.tcard .who {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: auto;
}
/* THE PORTRAIT PINS TO THE NAME, AND THE PAIR OF DECLARATIONS IS CONDITIONAL
   BEHAVIOUR WITHOUT A QUERY (2026-09-04, Jason). `align-items: center` is right
   while the text is shorter than the 48px portrait and wrong the moment it is
   taller: a title that wraps makes the text block the tall item, so the face
   floats at its middle and the name sits above the top of the head. Keeping
   `center` on .who and giving the portrait `align-self: flex-start` resolves
   both cases with no breakpoint — when the portrait is the tall item it defines
   the line box and flex-start is indistinguishable from center, so the
   single-line attribution renders exactly as it did; when the text is taller the
   portrait stops floating and meets the name.

   Most of the wrapping was duplication rather than length and is fixed in
   TestimonialsHelper instead — the company was printed twice, as the card's
   eyebrow and again at the end of the role. What remains is genuinely long
   (Shirley Gwynn's), and it is carried here rather than truncated or shrunk: a
   real person's title is not something to clip, and 13px is one step off the
   type floor. */
.tcard .av { width: 48px; height: 48px; align-self: flex-start; }
/* A flex item's default min-width is its content, so a long unbroken title would
   push the card wider than its own basis rather than wrapping. */
.tcard .who > span { min-width: 0; }

/* Motion is one authored moment on this site and it is this one, so it yields
   completely: site_00 stops the track, and these four rules turn the stopped
   strip into a native scroller with snap points, which is the same content under
   a thumb instead of a timer. The mask goes with the animation — an edge fade on
   a scroller hides the thing a reader is about to drag into view. */
@media (prefers-reduced-motion: reduce) {
  .tmq {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-mask-image: none;
    mask-image: none;
    padding-bottom: 4px;
  }
  .tmq-track { animation: none; width: auto; }
  .tmq-track [aria-hidden="true"].tcard { display: none; }
  .tcard { scroll-snap-align: start; }
}

/* THE PHONE TAKES THE SAME PATH, AND FOR THE SAME REASON (2026-09-03).

   A marquee needs room either side of the card in view or the cards beside it
   are cut, and at 390px there is none: the track ran past both viewport edges
   with the first and last cards clipped mid-word, and because it never stops,
   no card was ever both stationary and complete. Measured at 390px, the two
   outer cards were sliced through the middle of a sentence — on the section
   whose whole job is trust, on a site whose pitch is that its claims are
   checkable, a testimonial you can see is cut in half reads as a broken page.

   The fix already existed. Reduced motion turns this strip into a native snap
   scroller with the duplicate set hidden, which is the same twelve people under
   a thumb instead of a timer — and a thumb is what a phone has. So the phone
   gets that path unconditionally, one full-width card per snap point, rather
   than a second treatment nobody maintains.

   The rules are repeated rather than merged into one selector list: a media
   query cannot be combined with a preference query without `and`, and writing
   `(prefers-reduced-motion: reduce), (max-width: 640px)` as one block would
   apply the mask removal at every width on a reduced-motion desktop, which is
   correct, and the full-bleed card sizing too, which is not. */
@media (max-width: 640px) {
  /* THE SCROLLER STAYS ON THE COLUMN. A full-bleed version was tried first, so
     the strip could run to the viewport edges: margin-inline: calc(50% - 50vw)
     with matching padding, the same arithmetic .essay uses. It measured wrong.
     scroll-snap-align: start aligns a card to the scrollport's edge, and
     scroll-padding did not hold it off that edge, so the resting card sat flush
     against the screen while the heading above it stayed on the 24px gutter —
     one card visibly out of line with everything around it. On the column the
     card IS the column, so it cannot be out of line with anything. */
  .tmq {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-mask-image: none;
    mask-image: none;
    padding-bottom: 4px;
  }
  .tmq-track { animation: none; width: auto; gap: 14px; }
  .tmq-track [aria-hidden="true"].tcard { display: none; }
  .tcard {
    scroll-snap-align: start;
    /* THE NEXT CARD PEEKS, AND THE 16% IT IS HELD BACK BY IS THE AFFORDANCE
       (2026-09-04, Jason, from a phone).

       This was `flex: 0 0 100%` — one card filling the column, "so nothing is
       ever half-read". That was the right correction to the marquee running
       past both viewport edges, and it overshot: a full-width card with nothing
       beside it, and the pause control hidden at this width, leaves a visitor
       with no signal that thirteen more people are a thumb away. Read from a
       phone it is not a carousel, it is a single testimonial.

       The distinction that matters is between a card CUT and a card WAITING.
       What the 2026-09-03 note describes is a sentence sliced mid-word by a
       viewport edge, which reads as a broken page. A card deliberately held
       back at the column's edge, its ground and border visible, reads as the
       next one — so this reserves 16% of the column, of which 14px is the gap:

         320px viewport → 272px column → 228px card, 30px of the next visible
         390px             342px          287px       41px
         430px             382px          321px       47px

       THE SPACER BELOW IS LOAD-BEARING, and without it the LAST testimonial is
       the one that breaks. `scroll-snap-align: start` needs the fourteenth card
       to be able to reach the left edge, and once the cards no longer fill the
       column it cannot: at 390px the track is 4,200px inside a 342px
       scrollport, so scrolling stops at 3,858px while that card's snap point
       sits at 3,913px. A mandatory scroller resolves an unreachable snap point
       by snapping back, which can leave the last person permanently unread. The
       spacer adds the missing 16% of scroll range after the last card.

       It is a pseudo-element rather than a markup spacer on purpose: an empty
       div in the partial would be counted by the `.tmq .tcard` assertions and
       would land in the accessibility tree.

       TWO THINGS THIS DOES NOT TOUCH. The reduced-motion scroller above already
       peeks at every width, because it inherits .tcard's base
       clamp(268px, 72vw, 420px) rather than this rule. And the 12% edge mask is
       `none` here, so the note at the head of section B — re-check the mask if
       .tcard's clamp changes — does not apply to this number. */
    flex: 0 0 84%;
  }
  .tmq-track::after { content: ""; flex: 0 0 16%; }
  /* No scrollbar furniture under a thumb; the snap points are the affordance. */
  .tmq { scrollbar-width: none; }
  .tmq::-webkit-scrollbar { display: none; }
}

/* -----------------------------------------------------------------------------
   C. THE FEATURED VOICE, for a page carrying exactly one.

   A single quote has to be a moment or it is an orphan, so it takes the warm band
   the Front Desk uses, at the same metrics — these are modifiers on .band, never
   a second copy of it. The photograph sits in the 130px cell where home puts the
   orb: the page has already taught that a warm round thing lives there, and a
   square photograph in the same slot reads as the human counterpart to the
   machine's. Rendered at 78px inside that cell, because the source is 100px and
   anything larger is visibly soft.
   -------------------------------------------------------------------------- */
.tband { align-items: center; }
.tband .av { width: 78px; height: 78px; margin: 0 auto; }
.tband q {
  display: block;
  font-family: var(--human);
  font-size: clamp(23px, 2.6vw, 30px);
  line-height: 1.32;
  color: var(--ink);
  max-width: 34ch;
  quotes: "\201C" "\201D";
  text-indent: -.42em;
  margin: 14px 0 18px;
}
.tband .who { display: flex; flex-direction: column; gap: 2px; }
