/* ==========================================================================
   TPB testimonial component
   --------------------------------------------------------------------------
   Styles for the markup rendered by /assets/js/testimonials.js.

   Everything is scoped under .tpb-tst on purpose. Several event pages still
   carry hand-written .v-card / .v-modal blocks with their own page-level
   overrides, and a global rule here would fight them mid-migration. Scoping
   means a page can run the component and its own legacy section side by side.

   var() fallbacks are supplied throughout so the component also renders on a
   page that does not link assets/design-system.css.
   ========================================================================== */

.tpb-tst { --tst-ink: var(--lt-ink, #0B1B2B); --tst-muted: var(--lt-muted, #5A6B7C); --tst-line: var(--lt-line, #E3E9EF); --tst-card: var(--lt-bg-2, #fff); --tst-accent: var(--cyan-ink, #015AFF); --tst-radius: var(--radius, 14px); }

/* Hide anything the author left inside the container until the data lands, so a
   placeholder never flashes. Keyed off the attribute, not .tpb-tst: the class is
   added by the JS at render time, alongside the content it styles. */
[data-tpb-testimonials]:not(.is-ready) > * { display: none; }

/* ----- grid ----- */
.tpb-tst .tst-grid { display: grid; gap: 20px; grid-template-columns: repeat(var(--tst-cols, 4), minmax(0, 1fr)); }
@media (max-width: 1000px) { .tpb-tst .tst-grid { grid-template-columns: repeat(min(var(--tst-cols, 4), 2), minmax(0, 1fr)); } }
@media (max-width: 560px) { .tpb-tst .tst-grid { grid-template-columns: 1fr; max-width: 340px; margin: 0 auto; } }

/* ----- card ----- */
.tpb-tst .tst-card { position: relative; background: var(--tst-card); border: 1px solid var(--tst-line); border-radius: var(--tst-radius); box-shadow: var(--shadow-lt, 0 18px 40px -24px rgba(15, 40, 70, .28)); overflow: hidden; display: flex; flex-direction: column; cursor: pointer; transition: transform .18s ease, box-shadow .2s ease; }
.tpb-tst .tst-card:hover, .tpb-tst .tst-card:focus-visible { transform: translateY(-4px); box-shadow: 0 26px 54px -22px rgba(15, 40, 70, .32); outline: none; }
.tpb-tst .tst-card:focus-visible { box-shadow: 0 0 0 3px var(--tst-accent); }

/* A card the geo pass promoted. Subtle by design: the point is to raise the
   nearest builder's profile, not to announce that we geolocated anyone. */
.tpb-tst .tst-card.is-near { border-color: color-mix(in srgb, var(--tst-accent) 45%, var(--tst-line)); }

/* ----- thumbnail -----
   One aspect ratio for every card in a container, whatever shape the video
   behind it is. Sizing the box per video is what produced the ragged grid on
   the apply pages: a portrait Summit still made its card taller than the three
   landscape ones beside it, and the short cards were padded out with whitespace
   to match. Square is the middle ground between the two source shapes, so
   neither a 16:9 banner nor a 9:16 Summit still is cropped as hard as it would
   be by a box that matched the other one. data-ratio overrides per container.

   This is the thumbnail only. The video still plays at its own orientation:
   that comes off data-orientation on the card, which the modal reads. */
.tpb-tst .tst-thumb { position: relative; overflow: hidden; background: linear-gradient(135deg, #13293F, #0A1726); aspect-ratio: var(--tst-ratio, 1 / 1); }
/* Two ways to fit an image into that box, chosen by what the image is, not by
   which way up it is - uploaded artwork comes at 16:9 and at 9:16, and both
   have to survive.

   An uploaded thumb is designed: a headline, the logo, the builder's name. Crop
   one and you cut the words - a square centre crop of the 16:9 banners turns
   "TPB" into "B" and "KEN LONGSHAW" into "NGSHAW" - so it is fitted whole. It
   boxes against the same navy the artwork is built on, which reads as one panel
   rather than as bars, and a square export fills the box with no rule change.

   A thumb still hotlinked from Vimeo is just a frame someone's video landed on,
   so it crops like the photograph it is. Only the portrait ones need the lift:
   a 9:16 still cropped square keeps a bit over half its height, and centred
   that band lands on the chest rather than the face. */
.tpb-tst .tst-thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.tpb-tst .tst-thumb.is-still img { object-fit: cover; }
.tpb-tst .tst-thumb.is-still.is-portrait img { object-position: center 30%; }
.tpb-tst .tst-thumb::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(10, 23, 38, 0) 45%, rgba(10, 23, 38, .8) 100%); pointer-events: none; }

.tpb-tst .tst-dur { position: absolute; top: 12px; right: 12px; z-index: 2; font-size: .72rem; font-weight: 800; color: #fff; background: rgba(10, 23, 38, .72); padding: 4px 9px; border-radius: 999px; }

.tpb-tst .tst-playpill { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; display: inline-flex; align-items: center; gap: 7px; padding: 8px 14px 8px 11px; border-radius: 999px; background: rgba(255, 255, 255, .94); color: #0B1B2B; font-size: .8rem; font-weight: 800; box-shadow: 0 8px 24px rgba(0, 0, 0, .35); transition: transform .18s ease, background .18s ease; pointer-events: none; }
.tpb-tst .tst-card:hover .tst-playpill { transform: translate(-50%, -50%) scale(1.06); background: #fff; }
.tpb-tst .tst-playpill svg { width: 16px; height: 16px; fill: var(--tst-accent); flex-shrink: 0; }
/* Touch devices get "Tap", pointer devices get "Click". Chosen in CSS rather
   than by sniffing the user agent in JS. */
.tpb-tst .tst-playpill .on-tap { display: none; }
@media (hover: none) { .tpb-tst .tst-playpill .on-click { display: none; } .tpb-tst .tst-playpill .on-tap { display: inline; } }

/* Muted looping preview, layered over the still on hover / in-view. */
.tpb-tst .tst-preview { position: absolute; inset: 0; z-index: 1; opacity: 0; transition: opacity .3s ease; pointer-events: none; }
.tpb-tst .tst-preview.is-on { opacity: 1; }
.tpb-tst .tst-preview iframe { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; height: 178%; border: 0; }
.tpb-tst .tst-preview.is-landscape iframe { width: 178%; height: 100%; }

/* ----- body ----- */
.tpb-tst .tst-body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 9px; flex: 1; }
.tpb-tst .tst-stars { font-size: .95rem; color: #F5A623; letter-spacing: 1px; line-height: 1; }
.tpb-tst .tst-quote { margin: 0; font-size: .9rem; font-weight: 600; line-height: 1.45; color: var(--tst-ink); }
.tpb-tst .tst-story { display: flex; flex-direction: column; gap: 8px; }
.tpb-tst .tst-story p { margin: 0; font-size: .9rem; line-height: 1.55; color: var(--tst-ink); }
/* Story copy bolds its money line and spikes the number inside it with <u>.
   The default underline sits on the baseline and clips descenders, which reads
   as an artefact rather than as emphasis. */
.tpb-tst .tst-story u { text-decoration-thickness: 2px; text-underline-offset: 3px; text-decoration-skip-ink: auto; }
.tpb-tst .tst-attr { margin-top: auto; padding-top: 4px; font-size: .82rem; line-height: 1.35; }
.tpb-tst .tst-attr b { display: block; font-size: .92rem; font-weight: 800; color: var(--tst-ink); }
.tpb-tst .tst-attr span { display: block; font-weight: 600; color: var(--tst-muted); }
.tpb-tst .tst-attr .tst-loc { color: var(--tst-accent); }

/* ----- row layout (data-layout="rows") ----- */
.tpb-tst[data-layout="rows"] .tst-grid { --tst-cols: 1; gap: 26px; max-width: 940px; margin: 0 auto; }
.tpb-tst[data-layout="rows"] .tst-card { flex-direction: row; align-items: stretch; cursor: default; }
/* aspect-ratio:auto lets a designed landscape banner stretch to whatever height
   the story copy beside it needs, which is what makes a story row read as one
   panel. A PORTRAIT thumb cannot be left on auto: at 42% of the 940 grid the
   image is ~395px wide, so a 9:16 still contributes ~700px of natural height,
   the row inherits it, and a short quote then floats in 700px of white. Giving
   it a square box caps that contribution at ~395px; the row still grows past it
   when the copy is longer, because the item is stretched after the fact. The
   image is a Vimeo still (.is-still), so it crops to the box rather than
   letterboxing, and .is-still.is-portrait already pulls the crop up to 30% to
   keep the speaker's face in frame. */
.tpb-tst[data-layout="rows"] .tst-thumb { flex: 0 0 42%; aspect-ratio: auto; cursor: pointer; }
.tpb-tst[data-layout="rows"] .tst-thumb.is-portrait { aspect-ratio: 1 / 1; }
.tpb-tst[data-layout="rows"] .tst-body { padding: 20px 22px; }
/* Rows go one-per-row on a phone, and a square banner on top of the copy eats
   most of the screen before a word of it is read. The row layout keeps 16:9
   there whatever the grid ratio is - including the portrait case above, which
   has to be repeated at the same specificity to beat it (.is-portrait carries a
   class the bare rule does not, so source order alone would not do it). */
@media (max-width: 700px) {
  .tpb-tst[data-layout="rows"] .tst-card { flex-direction: column; }
  .tpb-tst[data-layout="rows"] .tst-thumb { flex: none; aspect-ratio: 16 / 9; }
  .tpb-tst[data-layout="rows"] .tst-thumb.is-portrait { aspect-ratio: 16 / 9; }
}

/* ----- collapse + reveal (data-collapse) -----
   The rows past the cut are display:none via the `hidden` attribute, so the
   fade sits over the bottom of the last VISIBLE row rather than over a
   half-drawn one. That is what signals there is more without paying for a
   partially rendered card that reflows when the button is pressed.

   --tst-fade-to is the page's own background. It defaults to white because
   every page running this today is white behind the section; a section on a
   tint sets it alongside the container. */
/* The collapsed rows have to be hidden HERE, in the author sheet. collapseExtras
   sets the `hidden` attribute, and the only rule behind that is the UA sheet's
   [hidden]{display:none} - which an author `display` on the same element beats
   regardless of specificity, because author origin wins over UA origin. .tst-card
   sets display:flex, so without this line every collapsed row stayed on screen
   and the button revealed nothing: the fade and the button rendered over a wall
   that was already fully visible. Attribute selector, so it also wins over the
   rows layout's own .tst-card rule without !important. */
.tpb-tst .tst-card[hidden] { display: none; }
.tpb-tst.is-collapsed .tst-grid { position: relative; }
.tpb-tst.is-collapsed .tst-grid::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 190px;
  background: linear-gradient(180deg, transparent, var(--tst-fade-to, #fff) 82%);
  pointer-events: none;                /* the covered row is still clickable */
}
.tpb-tst .tst-more { display: flex; justify-content: center; margin-top: 18px; }
.tpb-tst.is-collapsed .tst-more { margin-top: -34px; position: relative; z-index: 1; }
.tpb-tst .tst-more-btn {
  font: inherit; font-size: 16px; font-weight: 700; line-height: 1.3;
  color: #0A1726; background: #fff; border: 2px solid #0A1726; border-radius: 999px;
  padding: 13px 26px; cursor: pointer; text-align: center;
  transition: background .15s ease, color .15s ease, transform .15s ease;
}
.tpb-tst .tst-more-btn:hover { background: #0A1726; color: #fff; }
.tpb-tst .tst-more-btn:active { transform: translateY(1px); }
.tpb-tst .tst-more-btn:focus-visible { outline: 3px solid #015AFF; outline-offset: 3px; }
/* The label is a sentence, not a word, so it has to be allowed to wrap on a
   phone rather than run off the side. */
@media (max-width: 560px) {
  .tpb-tst .tst-more-btn { width: 100%; max-width: 420px; padding: 13px 18px; font-size: 15px; }
}
@media (prefers-reduced-motion: reduce) {
  .tpb-tst .tst-more-btn { transition: none; }
}

/* ----- modal ----- */
.tpb-tst-modal { position: fixed; inset: 0; z-index: 200; display: none; align-items: center; justify-content: center; padding: 24px; }
.tpb-tst-modal.is-open { display: flex; }
.tpb-tst-modal .m-bg { position: absolute; inset: 0; background: rgba(6, 14, 24, .87); backdrop-filter: blur(6px); }
/* The frame is the shape of the video, whatever that is. --m-ar is the video's
   own ratio as a number, set by the JS when the modal opens; the width is then
   whichever of three limits bites first - the viewport, a readable maximum, or
   the height budget converted to a width by that ratio. One rule covers 9:16,
   1:1 and 16:9 without a class per shape, and nothing letterboxes inside the
   frame, which is what left the close button stranded away from the picture. */
.tpb-tst-modal .m-inner { position: relative; z-index: 1; aspect-ratio: var(--m-ar, 0.5625); width: min(100%, 1000px, calc(min(86vh, 760px) * var(--m-ar, 0.5625))); height: auto; }
/* The rounding and the clipping live on the frame, not on .m-inner. The close
   button is a child of .m-inner and sits deliberately outside its top right
   corner, so an overflow:hidden up there cut the button in half - which is what
   it was doing. .m-inner is now just the box that gets sized. */
.tpb-tst-modal .m-frame { border-radius: 18px; overflow: hidden; background: #000; box-shadow: 0 40px 90px -30px rgba(0, 0, 0, .8); }
.tpb-tst-modal .m-frame, .tpb-tst-modal .m-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.tpb-tst-modal .m-close { position: absolute; top: -14px; right: -14px; z-index: 3; width: 42px; height: 42px; border-radius: 50%; background: #fff; border: 0; cursor: pointer; font-size: 1.5rem; line-height: 1; color: #0B1B2B; display: grid; place-items: center; box-shadow: 0 6px 18px rgba(0, 0, 0, .4); -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
/* The visual circle only takes clicks inside its radius, so taps on the corners
   of the button box fell through to the video underneath. This pads the hit
   area out past the circle and keeps those taps on the close button. */
.tpb-tst-modal .m-close::before { content: ""; position: absolute; inset: -10px; border-radius: 50%; }
@media (max-width: 560px) { .tpb-tst-modal { padding: 14px; } .tpb-tst-modal .m-close { top: 6px; right: 6px; } }

/* ----- mobile body-copy floor -----
   The quote is the only thing on a card a visitor reads, so it goes to 18px on
   a phone. The duration pill, the play pill and the attribution line stay put:
   they are chrome, and the card is already one-per-row at this width. */
@media (max-width: 820px) {
  .tpb-tst .tst-quote, .tpb-tst .tst-story p { font-size: 1.125rem; }
}
