/**
 * KSM Review Collector Styles
 * Based on Trustindex widget styling
 */

/* Main Widget Container */
.ksm-widget {
  margin: 2rem 0;

}

.ksm-widget-container {
  background-color: #f9f9f9;
  padding: 2rem;
  border-radius: 8px;
}

/* Header Section */
.ksm-widget-header {
  margin-bottom: 2rem;
  background-color:#444;
}

/* Platform Tabs */
.ksm-platform-tabs {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1.5rem;
  border-bottom: 2px solid #e0e0e0;
}

.ksm-platform-tab-items {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.ksm-tab-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 1rem 1.5rem;
  cursor: pointer;
  border-bottom: 3px solid transparent;
  transition: all 0.3s ease;
  user-select: none;
}

.ksm-tab-item:hover {
  border-bottom-color: #ccc;
}

.ksm-tab-item.ksm-tab-active {
  border-bottom-color: #2196F3;
  font-weight: 600;
}

.ksm-platform-icon {
  width: 20px;
  height: 20px;
  object-fit: contain;
}

.ksm-item-label {
  font-size: 14px;
  color: #ffffff;
}

/* Header Content */
.ksm-header-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin-top: 1.5rem;
  padding-left:1rem;
  padding-right:1rem;
}

.ksm-header-rating {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.ksm-stars {
  display: flex;
  gap: 0.2rem;
}

.ksm-star {
  font-size: 30px;
  color: #ddd;
}

.ksm-star.f {
  color: #FFA500;
}

.ksm-header-rating-text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

.ksm-header-rating-text strong {
  font-size: 35px;
  color: #fff;
}

.ksm-header-rating-reviews {
  font-size: 20px;
  color: #ffffff;
}

/* Write Review Button */
.ksm-header-write-btn-container {
  position: relative;
}
.ksm-header-write-btn {
  color: #fff !important;
}
@media (min-width: 992px) {
.ksm-header-write-btn-container { margin-right:0; }
}
@media (max-width: 991px) {
.ksm-header-write-btn-container { margin-right:auto; margin-left:auto; margin-bottom:2rem; }
}


.ksm-header-write-btn {
  background-color: #2196F3;
  color: #fff;
  border: none;
  padding: 12px 24px;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.ksm-header-write-btn:hover {
  background-color: #1976D2;
}

.ksm-write-btn-dropdown {
  display: none;
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 0.5rem;
  background-color: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 100;
  min-width: 200px;
}

.ksm-write-btn-dropdown.ksm-dropdown-active {
  display: block;
}

.ksm-write-btn-dropdown-inner {
  padding: 0.5rem 0;
}

.ksm-write-btn-dropdown-item {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.8rem 1.2rem;
  text-decoration: none;
  color: #333;
  transition: background-color 0.2s ease;
}

.ksm-write-btn-dropdown-item:hover {
  background-color: #f5f5f5;
}

.ksm-write-btn-dropdown-item img {
  width: 16px;
  height: 16px;
}

/* Reviews Container */
.ksm-reviews-container-wrapper {
  position: relative;
  margin-bottom: 2rem;
}

.ksm-reviews-container {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.5rem;
  margin-bottom: 2rem;
}

.ksm-col-3 .ksm-reviews-container {
  grid-template-columns: repeat(3, 1fr);
}

.ksm-col-1 .ksm-reviews-container {
  grid-template-columns: 1fr;
}

/* Review Card */
.ksm-review-item {
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 1.5rem;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.ksm-review-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.ksm-rc-js .ksm-review-item[data-source]:not([data-source="all"]) {
  display: none;
}

/* Review Header */
.ksm-review-header {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 1rem;
  margin-bottom: 1rem;
  align-items: start;
}

.ksm-profile-img {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  overflow: hidden;
  background-color: #e0e0e0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ksm-profile-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ksm-profile-default {
  font-size: 20px;
  font-weight: 600;
  color: #666;
}

.ksm-profile-details {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.ksm-name {
  font-weight: 600;
  font-size: 15px;
  color: #333;
}

.ksm-profile-details .ksm-stars {
  font-size: 16px;
}

.ksm-verified-review {
  display: inline-block;
  color: #4CAF50;
  font-size: 14px;
  font-weight: 600;
}

.ksm-date {
  font-size: 12px;
  color: #999;
  text-align: right;
}

/* Review Content */
.ksm-review-content {
  margin-top: 1rem;
}

.ksm-review-text-container {
  position: relative;
}

.ksm-inner {
  line-height: 1.6;
  color: #555;
}

.ksm-inner p {
  margin: 0 0 0.5rem 0;
}

/* Item (2.20), cross-checked against the coordinator's captured
   before-values. Now a native <button type="button">, which arrives with
   UA chrome a <div> never had. color/font-size/font-weight/cursor/
   margin-top/display were already explicit before this stage and are
   UNCHANGED; only the appearance/background/border/padding/font-family/
   line-height/text-align group is new.
   line-height:inherit, NOT 'normal': measured, style 1's read-more is
   15px tall at 13px font (~1.15, i.e. already 'normal' there), but style
   2's is 21px tall at 14px font -- exactly 1.5, INHERITED from a style-2
   ancestor, not 'normal'. A UA button sets line-height:normal explicitly,
   which would leave style 1 unchanged but shrink style 2's read-more from
   21px to ~17px. inherit reproduces what the unstyled div did in BOTH
   styles at once -- do not "simplify" this to normal on the strength of
   style 1 alone measuring normal.
   text-align:inherit, not 'left': the capture did not record text-align,
   so a literal value would be an unverified guess, and style 2's
   read-more is a 429.5px-wide block -- if any ancestor centres text,
   forcing left would visibly move it while inherit cannot. Same
   principle as line-height: reproduce div behaviour, don't assert a
   value nothing confirmed. */
/* Item 3 (2.21), WCAG 2.5.8 target size, style 1: rendered height was
   15px (measured), width 67.2px already clears 24px so only height was
   short -- font-size is UNCHANGED, the extra height comes entirely from
   padding. 15px + 9px (4.5px top + 4.5px bottom) = 24px exactly.
   margin-top: THIS SITE'S 1rem IS 10px, NOT the browser-default 16px --
   confirmed by three independent measurements (the wrapper's padding
   measuring 22.5px against a declared 2.25rem; the pre-2.21 read-more
   margin measuring "5px 0 0" against a declared 0.5rem; .ksm-inner's
   101px = four 24px clamped lines (96) + a trailing 0.5rem paragraph
   margin, which only resolves at 10px, not 16px). Explicit px used here
   rather than rem for exactly that reason -- this wrong assumption has
   already caused more than one error on this project. 0.5rem is
   therefore 5px, not 8px: reduced by the 4.5px TOP padding added (not
   the full 9px, since only the top half pushes the text down), so the
   text's own vertical position is visually unchanged: 5 - 4.5 = 0.5px. */
/* Item 2 (2.23), AUTHORISED, DELIBERATE CHANGE TO STYLE 1: Shane ruled
   #2196F3 on "Read more" specifically -- fix it. #FFA500 and everything
   else about style 1 stays frozen; this is one colour, not blanket
   permission on the palette.
   BACKGROUND CONFIRMED INDEPENDENTLY, not assumed: this control sits
   inside .ksm-review-item, whose only background is #fff (:213) --
   .ksm-review-content/.ksm-review-text-container (its ancestors) set no
   background of their own, so nothing else is in play here. Re-derived
   the candidates with this plugin's own relative_luminance() formula
   rather than adopting the coordinator's figures: #2196F3 3.12:1 FAIL
   (today) / #1E88E5 3.68:1 FAIL / #1976D2 4.60:1 PASS / #1565C0 5.75:1
   PASS / #0D47A1 8.63:1 PASS -- identical to five decimal places.
   #1976D2 CHOSEN: the lightest value that clears 4.5:1, staying closest
   to the original blue's intent, and it ALREADY EXISTS in this codebase
   as .ksm-view-all-reviews's own :hover colour (:516) -- a colour this
   plugin already uses, not a new one introduced.
   SCOPE: colour only, this rule only. Style 2 overrides .ksm-read-more's
   colour to #707070 at higher specificity (.ksm-style-2 .ksm-review-item
   .ksm-read-more) regardless of what this base rule says, so this
   change cannot reach style 2 -- confirmed by re-reading that rule's
   own selector, not assumed from memory. */
.ksm-read-more {
  color: #1976D2;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  margin-top: 0.5px;
  display: inline-block;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: none;
  padding: 4.5px 0;
  font-family: inherit;
  line-height: inherit;
  text-align: inherit;
}

.ksm-read-more:hover {
  text-decoration: underline;
}

/* Slider Controls */
.ksm-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  margin-top: 2rem;
}

/* Item (2.20), cross-checked against the coordinator's captured
   before-values. Now a native <button>; every other property below was
   already explicit before this stage and is UNCHANGED -- only
   appearance/-webkit-appearance and padding:0 are new (native button
   chrome/UA padding a div never had; the 40x40 box + centred ::before
   arrow already fill the visible area). No font-family/line-height
   reset: ::before is content:'' (pure geometry -- a rotated bordered
   box), not a text glyph, so no font-size-dependent rendering exists
   here for a UA button default to disturb -- confirmed against the
   actual ::before rule, not assumed. Margins (the -19px straddle,
   further down this file) are untouched by this rule, deliberately. */
.ksm-arrow-prev,
.ksm-arrow-next {
  width: 40px;
  height: 40px;
  background-color: #fff;
  border: 2px solid #e0e0e0;
  border-radius: 50%;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease;
  position: relative;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}

.ksm-arrow-prev::before,
.ksm-arrow-next::before {
  content: '';
  width: 10px;
  height: 10px;
  border-top: 2px solid #333;
  border-right: 2px solid #333;
  display: block;
}

.ksm-arrow-prev::before {
  transform: rotate(-135deg);
  margin-left: 3px;
}

.ksm-arrow-next::before {
  transform: rotate(45deg);
  margin-right: 3px;
}

.ksm-arrow-prev:hover,
.ksm-arrow-next:hover {
  background-color: #2196F3;
  border-color: #2196F3;
}

.ksm-arrow-prev:hover::before,
.ksm-arrow-next:hover::before {
  border-color: #fff;
}

/* Item (2.20): two-tone (white + near-black) focus ring, not one colour --
   these controls sit against both the white desktop panel and a
   THEME-DEFINED rail colour (--ksm-rc-rail-bg) that could be almost
   anything, and one fixed colour can't clear 3:1 against an arbitrary
   light AND dark background at once. Same "pick whichever extreme wins"
   principle as get_band_button_text_colour(), shown as two simultaneous
   rings since a focus ring (unlike text) can afford both.
   MEASURED against the round's own worked rail default #0073aa
   (L=0.15164): black vs white = 21:1; black vs rail = 4.03:1; white vs
   rail = 5.21:1 (same figure 2.14's band-text derivation already
   established) -- all clear WCAG 1.4.11's 3:1. White vs white = 1:1
   (invisible there), which is fine -- the black ring covers that case;
   only one ring needs to show at a time.
   :focus-visible (keyboard only, not mouse clicks), scoped to these four
   selectors -- nothing else on the page is touched. */
.ksm-arrow-prev:focus-visible,
.ksm-arrow-next:focus-visible,
.ksm-read-more:focus-visible,
.ksm-style2-track-clip:focus-visible,
.ksm-style-2 .ksm-review-item .ksm-inner:focus-visible {
  /* 2.22: .ksm-inner added -- when the expanded review text actually
     needs to scroll (item 3, JS-managed tabindex, same predicate/toggle
     mechanism as .ksm-style2-track-clip), it gets the SAME two-tone ring
     as every other newly-focusable control in this widget, not a
     separate treatment. NEWLY MEASURED for this selector's own
     background (the card's #f5f5f5 fill, not the rail or the desktop
     panel the 2.20 figures were computed against): black vs #f5f5f5 =
     19.26:1, white vs #f5f5f5 = 1.09:1 -- the white ring is close to
     invisible on this near-white fill, but the black ring alone clears
     3:1 by a wide margin, which is sufficient under the same "only one
     ring needs to be visible" principle 2.20 already established. */
  outline: none;
  box-shadow: 0 0 0 2px #fff, 0 0 0 4px #000;
}

.ksm-controls-line {
  display: flex;
  gap: 0.5rem;
}

.dot {
  width: 10px;
  height: 10px;
  background-color: #ddd;
  border-radius: 50%;
  cursor: pointer;
  transition: background-color 0.3s ease;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
  touch-action: manipulation;
}

.dot.ksm-active {
  background-color: #2196F3;
}

/* Verified Badge */
.ksm-verified-by {
  text-align: center;
  padding-top: 1rem;
  border-top: 1px solid #e0e0e0;
}

.ksm-verified-by-row {
  font-size: 12px;
  color: #999;
  margin-bottom: 0.5rem;
}

.ksm-view-all-reviews {
  display: inline-block;
  color: #2196F3;
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: color 0.3s ease;
}

.ksm-view-all-reviews:hover {
  color: #1976D2;
  text-decoration: underline;
}

/* Platform Review Badges */
.ksm-platform-badges {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 2rem 0;
}

.ksm-platform-badge {
  flex: 1 1 calc(33.333% - 1rem);
  min-width: 250px;
}

.ksm-platform-badge-link {
  display: block;
  text-decoration: none;
  transition: transform 0.2s ease;
}

.ksm-platform-badge-link:hover {
  transform: translateY(-2px);
}

.ksm-platform-badge-content {
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 1.5rem;
  text-align: center;
  transition: box-shadow 0.2s ease;
}

.ksm-platform-badge-link:hover .ksm-platform-badge-content {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.ksm-platform-badge-name {
  font-size: 18px;
  font-weight: 700;
  color: #333;
  margin-bottom: 0.75rem;
}

.ksm-platform-badge-stats {
  font-size: 14px;
  color: #666;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.ksm-platform-badge-count {
  font-weight: 600;
  color: #2196F3;
}

.ksm-platform-badge-separator {
  color: #ccc;
}

.ksm-platform-badge-rating {
  color: #666;
}

/* List View Styles */
.ksm-reviews-list-wrapper {
  margin: 2rem 0;
}

.ksm-list-header {
  background-color: #444444;
  padding: 2rem;
  border-radius: 8px;
  margin-bottom: 2rem;
}

.ksm-list-header-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1.5rem;
}

.ksm-reviews-list {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.ksm-review-list-item {
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 2rem;
}

.ksm-review-list-header {
  display: flex;
  gap: 1.5rem;
  margin-bottom: 1rem;
}

.ksm-review-list-avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  overflow: hidden;
  background-color: #e0e0e0;
  flex-shrink: 0;
}

.ksm-review-list-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ksm-avatar-default {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  font-weight: 600;
  color: #666;
}

.ksm-review-list-meta {
  flex: 1;
}

.ksm-review-author {
  margin: 0 0 0.5rem 0;
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.ksm-review-rating {
  display: flex;
  gap: 0.2rem;
  margin-bottom: 0.5rem;
}

.ksm-star {
  font-size: 18px;
  color: #ddd;
}

.ksm-star.filled {
  color: #FFA500;
}

.ksm-review-source {
  font-size: 13px;
  color: #999;
}

.ksm-review-list-content {
  font-size: 15px;
  line-height: 1.6;
  color: #555;
}

/* List View Pagination */
.ksm-review-hidden {
  display: none;
}

.ksm-load-more-container {
  text-align: center;
  margin-top: 2rem;
}

.ksm-load-more-btn {
  background-color: #2196F3;
  color: #fff;
  border: none;
  padding: 14px 32px;
  border-radius: 4px;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.ksm-load-more-btn:hover {
  background-color: #1976D2;
}

/* Reviews Summary Badge */
.ksm-reviews-summary-wrapper {
  margin: 2rem 0;
}

.ksm-summary-title {
  font-size: 24px;
  font-weight: 600;
  margin: 0 0 1rem 0;
  color: #333;
}

.ksm-summary-title a {
  color: #333;
  text-decoration: none;
  transition: color 0.3s ease;
}

.ksm-summary-title a:hover {
  color: #2196F3;
}

.ksm-reviews-summary-badge {
  display: inline-block;
}

.ksm-summary-link {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.8rem 1.2rem;
  background-color: #fff;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  text-decoration: none;
  transition: all 0.3s ease;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.ksm-summary-link:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  border-color: #2196F3;
}

.ksm-summary-stars {
  display: flex;
  gap: 0.15rem;
}

.ksm-summary-star {
  font-size: 20px;
  color: #ddd;
}

.ksm-summary-star.filled {
  color: #FFA500;
}

.ksm-summary-text {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 15px;
  color: #333;
}

.ksm-summary-rating {
  font-weight: 600;
  font-size: 18px;
}

.ksm-summary-separator {
  color: #999;
}

.ksm-summary-count {
  color: #666;
}

/* Responsive Styles */
@media (max-width: 992px) {
  .ksm-widget-container {
    padding: 1.5rem;
  }

  .ksm-reviews-container {
    grid-template-columns: 1fr;
  }

  .ksm-col-2 .ksm-reviews-container,
  .ksm-col-3 .ksm-reviews-container {
    grid-template-columns: 1fr;
  }

  .ksm-platform-tab-items {
    flex-wrap: nowrap;
    overflow-x: auto;
  }

  .ksm-header-content {
    flex-direction: column;
    align-items: flex-start;
  }
}

@media (max-width: 768px) {
  .ksm-widget-container {
    padding: 1rem;
  }

  .ksm-platform-badges {
    flex-direction: column;
  }

  .ksm-platform-badge {
    flex: 1 1 100%;
    min-width: 100%;
  }

  .ksm-review-header {
    grid-template-columns: auto 1fr;
    grid-template-rows: auto auto;
  }

  .ksm-date {
    grid-column: 1 / -1;
    text-align: left;
    margin-top: 0.5rem;
  }

  .ksm-review-list-header {
    flex-direction: column;
  }

  .ksm-review-list-avatar {
    width: 80px;
    height: 80px;
    margin: 0 auto;
  }

  .ksm-write-btn-dropdown {
    right: auto;
    left: 0;
    min-width: 100%;
  }
  .ksm-stars {
  display: block;
  text-align: center;
  width: 100%;
}
.ksm-header-rating {
  display: block;
  text-align: center;
  margin-left: auto;
  margin-right: auto;
}
}

/* ==========================================================================
   Style 2 — rail + cards (2.12)
   Everything below is scoped under .ksm-style-2 and touches nothing above
   this line. Palette reused as-is from the existing rules in this file:
   #2196F3 / #1976D2 accent, #333 / #555 / #666 text, #e0e0e0 / #ddd borders,
   #fff card surface, #f5f5f5 card fill (new, per §5), #FFA500 stars
   (unchanged -- see the note above the star rule below). No new custom
   properties beyond --ksm-rc-band-text (flagged below), no font-family
   change, no @import.
   ========================================================================== */

/* --- D2: cards stay visible with JS on ------------------------------------
   UNCHANGED FROM 2.11 -- do not edit this rule or its specificity without
   re-reading the knowledge note's specificity warning first.
   Without this, the offending ".ksm-rc-js .ksm-review-item[data-source]:not(
   [data-source="all"]) { display: none; }" rule above (:225, frozen region)
   hides every style-2 card outright once JS is running, because the shared
   JS keeps every item visible via a transform instead of calling .show(),
   so the .show()-writes-an-inline-style escape hatch style 1 relies on
   never fires for style 2. This is a functional requirement for the
   JS-enabled render, not only the no-JS fallback.
   Specificity check (re-verified this stage, Stage 6 gate 4): the gating
   rule at :225 is .ksm-rc-js (class) + .ksm-review-item (class) +
   [data-source] (attribute) + :not([data-source="all"]) (the :not()
   pseudo-class itself contributes nothing to specificity, but the
   attribute selector INSIDE it does) = 1 class + 1 class + 2 attribute
   selectors = (0,4,0). THIS rule below doubles its own class on purpose
   (.ksm-style-2.ksm-style-2 -- a selector may repeat the same class any
   number of times, each repetition still counts) to reach .ksm-style-2 +
   .ksm-style-2 + .ksm-reviews-container + .ksm-review-item + [data-source]
   = 2 classes + 2 classes... i.e. 4 classes + 1 attribute = (0,5,0),
   strictly higher than the gate's (0,4,0), so it wins outright regardless
   of source order -- not relying on the cascade tie-break. Nothing in this
   stage added a class to the :225 gate, so this comparison stands exactly
   as it did after Amendment 1; if a future stage ever adds a prefix to the
   :225 selector, this rule's doubling must be increased to match. */
.ksm-style-2.ksm-style-2 .ksm-reviews-container .ksm-review-item[data-source] {
  display: flex;
  flex-direction: column;
}

/* --- Track geometry (UNCHANGED FROM 2.11, NOW GATED BEHIND .ksm-rc-js) ----
   Stage 4 of 2.13 (criterion 11): the whole sliding-track mechanism only
   makes sense once JS is actually driving it -- the shared JS applies
   transform: translateX(-{currentPage * 100}%) to .ksm-reviews-container,
   and it is what this wrapper's overflow: hidden exists to clip. With NO
   JS, none of this ever applies, so it is gated behind .ksm-rc-js (added
   synchronously in <head>, Amendment 1 C1) rather than left ungated and
   inert: a JS-off browser now falls straight through to the BASE grid
   (:196-198, shared/frozen, display:grid; grid-template-columns:
   repeat(2,1fr)) -- which happens to be the exact two-up layout §5 already
   wants, so no fallback-specific styling is needed at all, only removal of
   what would otherwise override it. Gating .ksm-rc-js ahead of .ksm-style-2
   only ADDS a class to each selector -- it cannot break any existing
   override relationship between style-2's own rules, and is unrelated to
   the frozen style-1 visibility exemption below (different selectors
   entirely, unaffected either way). */
/* Item 1 (2.17): 2.16's fix for this same defect (overflow-clip-margin:
   content-box, directly below in the previous version of this rule) DID
   NOT WORK -- verified live in Chrome: the property computes to the
   string 'content-box' (proving the declaration PARSED) but the clip
   edge measurably stayed at the padding box regardless (proving the
   engine did not act on it). A computed-style read is evidence of
   parsing only, never of effect -- this is recorded here so nobody
   reaches for overflow-clip-margin on this element again on the strength
   of it "looking" supported.
   THE ACTUAL FIX now lives on the NEW .ksm-style2-track-clip element
   introduced this stage (class-renderer.php, style 2 only), which wraps
   ONLY the track and carries NO padding of its own -- so plain
   overflow:hidden's ordinary padding-box clip edge is already identical
   to the content-box edge there, with no experimental property and no
   keyword-support gamble. This element (.ksm-reviews-container-wrapper)
   goes back to having no overflow rule at all: it no longer does any
   clipping, only sizing/padding/positioning (below), so there is nothing
   left here to fall back on incorrectly. */

/* Item 1 (2.17): the actual clip, on the new inner element. No padding is
   ever set on this element (checked: nothing in this file targets
   .ksm-style2-track-clip except this rule), so overflow:hidden's default
   padding-box clip edge and its content-box edge are the SAME edge here
   by construction -- not a coincidence to maintain, a guarantee that
   holds as long as this rule stays the only one touching this class.
   Gated behind .ksm-rc-js for the same reason the old wrapper clip was:
   the sliding-track mechanism (and the mobile grid's analogous need to
   clip extra columns) only exists once JS is driving it; with no JS the
   base grid shows every review anyway. */
/* Item 4 (2.22): fixed clip height, desktop. A desktop "page" is one ROW
   of cards, side by side (the transform-driven slide, unchanged) -- one
   card's own fixed height (275px @1440, derived on
   .ksm-review-item[data-source] above), no addition needed since there
   is only one row here (contrast the mobile rule below, which stacks
   two). Gated .ksm-rc-js, matching this rule's EXISTING gate: without JS
   this element does no clipping at all at desktop width (the base grid
   shows every review, unchanged), so giving it a fixed height in that
   state would be wrong, not merely redundant -- it would cut off rows of
   the no-JS grid that are supposed to all be visible. */
.ksm-rc-js .ksm-style-2 .ksm-style2-track-clip {
  overflow: hidden;
  height: 275px;
}

.ksm-style-2 .ksm-reviews-container-wrapper {
  position: relative; /* controls positioning -- gated separately below,
                          this stays ungated since it is harmless on its own
                          (no effect unless something is absolutely
                          positioned against it) and the base grid needs
                          nothing from it either way. */
}

.ksm-rc-js .ksm-style-2 .ksm-reviews-container {
  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;
  width: 100%;
  gap: 0;
  margin-bottom: 0;
  transition: transform 0.4s ease;
}

/* Reduced motion: CSS only, no JS branch (there must not be one) -- paging
   becomes an instant swap.
   STAGE 8 FIX (2.13): this rule was WRONGLY described as "harmless to leave
   ungated" -- media queries contribute NO specificity, so before Stage 4
   this and the gated transition rule above were BOTH (0,2,0) and file
   order decided (this rule, later, won). Stage 4 raised the rule above to
   .ksm-rc-js .ksm-style-2 .ksm-reviews-container = (0,3,0) without raising
   this one to match, silently demoting it below a rule it could no longer
   beat on file order alone -- the transition stopped being overridden at
   all. Fixed by matching the SAME .ksm-rc-js prefix: now both are (0,3,0)
   again and file order (this rule is later) decides exactly as before.
   Matching the prefix is also semantically correct on its own terms, not
   just a specificity patch: the transition property it overrides ONLY
   exists on the gated rule -- with .ksm-rc-js absent there is nothing to
   suppress (the base grid has no transition at all), so requiring
   .ksm-rc-js here changes nothing about the no-JS path. */
@media (prefers-reduced-motion: reduce) {
  .ksm-rc-js .ksm-style-2 .ksm-reviews-container {
    transition: none;
  }

  /* STAGE 9 FIX (2.13): this was NOT losing to its own :hover rule (:1341
     declares only opacity/text-decoration, no transition, and is
     irrelevant here). The actual mechanism: the button's non-hover
     style-2 rule (:1329) ALSO sets transition: opacity 0.3s ease, at the
     SAME (0,2,0) specificity this override had -- a tie, broken by source
     order, and :1329 sits later in the file, so it silently won. Doubling
     the class (the same idiom already used and documented for the style-2
     visibility exemption above) raises this to (0,3,0), which beats
     :1329's (0,2,0) outright -- no reliance on source order this time, so
     a future edit anywhere else in the file cannot quietly take the win
     away again the way the reduced-motion track rule and the mobile
     controls rule both did earlier in this same job. */
  .ksm-style-2.ksm-style-2 .ksm-view-all-reviews {
    transition: none;
  }

  .ksm-style-2 .ksm-arrow-prev,
  .ksm-style-2 .ksm-arrow-next {
    transition: none;
  }
}

/* Slot width, one variant per ksm-col-N, GATED. Amendment A2 (Stage 2 of
   2.12) removed the style-2 three-column forcing rule, so style 2 now
   inherits shortcode_atts' own default of 2 -- .ksm-col-2 is what
   actually renders by default, matching §5's "two across". The col-1/
   col-3 variants are kept for completeness (an explicit columns=
   attribute can still request them). Gated for the same reason as the
   track above: flex/box-sizing/margin all only mean something while
   .ksm-reviews-container is itself flex; on the base grid fallback these
   selectors must not apply at all, or their margin would double up
   against the grid's own gap.
   Item 1 (2.16): THE INVARIANT is basis + (2 x margin) == 100/N% for a
   page step of exactly one track-width to land on the next N-card page
   with no surplus. 2.11 set this correctly at margin:0.5rem (basis
   100/N% - 1rem exactly cancels 2 x 0.5rem). 2.12 then widened the GAP
   to 0.75rem in a SEPARATE, later rule (deleted this stage -- see below)
   without touching the basis here, so the two slots secretly summed to
   100/N% + 0.5rem: over a whole page that surplus is exactly what let a
   sliver of the next card render inside the clip region (Shane's report,
   measured at 1440px: card 3 landing 5px into the visible area). FIXED
   HERE by consolidating BOTH values into these three rules directly --
   margin stays 0.75rem (2.12's intended visual gap, kept, not reverted),
   basis now subtracts 2 x 0.75rem = 1.5rem instead of 1rem, restoring the
   invariant at the WIDER gap rather than the original narrower one:
     col-1 (N=1): 100%    - 1.5rem
     col-2 (N=2): 50%     - 1.5rem
     col-3 (N=3): 33.3333% - 1.5rem (the 33.3333% approximation itself is
       UNCHANGED from 2.11 -- not introduced or altered this stage -- and
       carries the same negligible sub-pixel rounding tolerance it always
       has; not re-derived or newly assessed here).
   This also makes the old separate margin-override rule (2.12, previously
   immediately after the card-fill rules below) REDUNDANT: it existed only
   to raise 0.5rem to 0.75rem after the fact. With that value folded in
   here directly, keeping both would mean two rules that only stay correct
   by being maintained in sync by hand -- deleted rather than kept as a
   second rule that "mostly" still agrees. */
.ksm-rc-js .ksm-style-2 .ksm-col-3 .ksm-reviews-container > .ksm-review-item[data-source] {
  flex: 0 0 calc(33.3333% - 1.5rem);
  box-sizing: border-box;
  margin: 0 0.75rem;
}

.ksm-rc-js .ksm-style-2 .ksm-col-2 .ksm-reviews-container > .ksm-review-item[data-source] {
  flex: 0 0 calc(50% - 1.5rem);
  box-sizing: border-box;
  margin: 0 0.75rem;
}

.ksm-rc-js .ksm-style-2 .ksm-col-1 .ksm-reviews-container > .ksm-review-item[data-source] {
  flex: 0 0 calc(100% - 1.5rem);
  box-sizing: border-box;
  margin: 0 0.75rem;
}

/* Equal card heights: align-items: stretch (set above, matching the flex
   default) stretches every item in a row to the tallest sibling's height
   automatically -- combined with the four-line clamp further down (which
   bounds the single biggest source of height variance, the review text),
   the row reads as visually even without a hardcoded pixel height. */

/* ==========================================================================
   THE PANEL
   ========================================================================== */

/* The band colour itself is an inline style="background-color:...;" on
   .ksm-widget (Stage 4 PHP, R6) -- nothing to do here for the colour. This
   rule only removes the shared/frozen .ksm-widget-container's own grey fill
   (:12-16, #f9f9f9) for style 2, so an unset band shows "whatever the page
   provides" (R6's default) and a set band shows through uniformly as the
   frame around the white panel, rather than a grey stripe on top of it.
   "Full-bleed" is read here as filling .ksm-widget's OWN box edge-to-edge --
   no viewport-width CSS breakout is attempted, since that would assume
   things about the surrounding theme this job is not authorised to touch. */
.ksm-style-2 .ksm-widget-container {
  background-color: transparent;
  border-radius: 0;
}

/* Item (2.18): overflow:hidden REMOVED from this rule -- audited (this
   stage's report has the full list) and it was clipping more than its own
   rounded corners needed. This element's OWN background-color/border
   already clip to its OWN border-radius regardless of overflow (a
   default of how border-radius works, not something overflow:hidden was
   providing) -- ONLY a descendant painting its own background/shadow to
   the panel's edge needed the panel to clip it, and the rail (below) is
   the sole one, now given its own border-radius instead. The straddling
   arrows are DESCENDANTS of this panel too (nested inside
   .ksm-reviews-container-wrapper, itself a direct child of this
   element) -- they were being clipped by this same declaration whenever
   they overhung the panel's edge, which is exactly the straddle Shane
   asked to keep. Removing this rule is what lets that overhang paint in
   full; it is not a side effect to guard against, it is the fix. */
/* Item 4 (2.22), PANEL/RAIL AT DESKTOP -- deliberately NOT given their
   own explicit height. MEASURED, not assumed: the rail DOES currently
   move at this width too (330px webfont -> 321px fallback, -9px) -- so
   "stretch makes the rail immune" is NOT true as a general claim.
   align-items:stretch (already here, unchanged) does not grant
   immunity; it makes the rail passively TRACK whatever the wrapper
   needs, and the wrapper itself was font-dependent before this stage
   (measured 310px -> 301px, the SAME -9px, no unaccounted term). This
   is an EXPECTATION FROM MECHANISM, NOT YET RE-MEASURED, so it is
   written as one rather than as an established fact: once the wrapper
   becomes deterministic (fixed card height -> fixed clip height ->
   wrapper = 275 clip + 35 padding = 310px, none of it font-dependent
   any more), the rail's stretch TARGET stops moving, and the rail
   should become immune as a side effect of that, not because stretch
   itself ever provided immunity. If re-measurement after sync shows the
   rail still moves at this width, that expectation was wrong and this
   needs the same explicit-height treatment as the mobile rule above --
   adding a literal duplicate number (310px = 275 clip + 35 wrapper
   padding) here now, before that confirmation, would be a second place
   to keep in sync for a claim not yet verified; the number is recorded
   here for that purpose if it turns out to be needed. */
.ksm-style-2 .ksm-style2-panel {
  display: flex;
  align-items: stretch;
  background-color: #fff;
  border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

/* --- Rail: ~26-28% (:27%), centred text, vertically centred -------------
   FOUR children, not three: rating word, stars, count, source line -- the
   uniform gap below spaces all four the same, deliberately, now that the
   star row's box height is no longer squeezed by a foreign stylesheet (see
   the .ksm-stars rules below) -- see this stage's report for why widening
   this gap would only have masked the actual defect rather than fixed it. */
/* Item 2, Branch A/B (2.14, §3): background is the measured brand primary
   when Stage 2's PHP could measure one (--ksm-rc-rail-bg), else falls
   through to the theme's own --primary, else the plugin's own hardcoded
   default -- every custom-property reference here carries a hardcoded
   fallback so this still looks right on a theme defining nothing.
   Declared EXPLICITLY on this rule (not inherited) so a band colour set on
   the ancestor .ksm-widget cannot show through: the rail always opaquely
   paints its own box, matching Stage 2's "band paints the wrapper, rail
   paints its own smaller box on top" analysis -- confirmed here in CSS,
   not just assumed from the PHP.
   The box-shadow does two jobs in one declaration (no duplicated rule):
   - the first layer is a 4px inset accent on the rail's leading edge,
     using --secondary where it genuinely helps (§3) -- a hardcoded
     fallback of #333 keeps today's rail-count colour if nothing defines
     --secondary;
   - the second layer is the Branch B scrim: --ksm-rc-rail-scrim is
     'transparent' on Branch A (Stage 2 emits it explicitly) so this layer
     disables itself there, and falls back to rgba(0,0,0,0.55) -- the
     alpha this stage derived (white needs background L <= 0.18333, which
     needs alpha >= 0.5347 against a worst-case pure-white primary; 0.55
     rounds up safely, 4.76:1 at that worst case) -- whenever the property
     is absent, i.e. Branch B/unmeasurable. One rule: unmeasured -> scrim,
     measured -> transparent, no second copy of this logic anywhere. */
.ksm-style-2 .ksm-style2-rail {
  flex: 0 0 27%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 2rem 1.5rem;
  gap: 0.6rem;
  background: var(--ksm-rc-rail-bg, var(--primary, #0073aa));
  box-shadow:
    inset 4px 0 0 0 var(--secondary, #333),
    inset 0 0 0 999px var(--ksm-rc-rail-scrim, rgba(0, 0, 0, 0.55));
  /* Item (2.18): the panel's own overflow:hidden used to clip this
     element's square corners into the panel's 12px radius -- now that
     that rule is gone (see .ksm-style2-panel above), this element rounds
     its OWN left corners itself. Desktop only: the rail sits on the LEFT
     here, so only top-left/bottom-left round; the mobile override below
     replaces this with top corners instead, matching the rail's own
     desktop-vs-mobile position. This affects the background and BOTH
     inset box-shadows (the --secondary stripe and the scrim) identically,
     since border-radius clips everything painted inside an element's own
     box, inset shadows included. */
  border-radius: 12px 0 0 12px;
}

/* Bold uppercase per §5's visual description; the STRING stays sentence
   case (get_rating_word(), PHP) so it stays correctly translatable and so
   screen readers announce it as a normal word rather than spelling out
   capital letters -- text-transform is presentation only, decided this
   stage per the coordinator's ruling on the Stage 5 i18n question.
   Colour (2.14, §3): the rail is no longer always white, so the word is
   driven from --ksm-rc-rail-text (set by the SAME PHP helper that already
   guarantees >=4.58:1 for the band button -- see class-renderer.php) with
   a hardcoded #fff fallback for a theme defining nothing. Full-strength
   colour, no opacity reduction -- this is the rail's most prominent
   element. */
/* Item 4 (2.22): capped to one line, same reason and same treatment as
   .ksm-name (item 1) -- CHECKED, NOT ASSUMED SAFE THE WAY THIS PLUGIN
   HAS BEEN WRONG ABOUT FOUR TIMES BEFORE: this is NOT reliably "a single
   short word from a fixed set". get_rating_word() (class-renderer.php)
   falls back to the automatic Excellent/Great/Good/Fair/Poor set only
   when ksm_rc_rating_word_override is blank -- when set, that option is
   sanitised (admin/class-settings.php's sanitize_rating_word_override())
   to allow up to 40 CHARACTERS of admin-supplied text, not a single
   word. At 26px bold uppercase in a ~27%-wide column, a 40-character
   override would almost certainly wrap. That makes this the FIRST
   candidate to check, not the safest -- confirmed by reading the actual
   sanitiser, not inferred from how the field is described elsewhere. */
.ksm-style-2 .ksm-style2-rail-word {
  font-size: 26px;
  font-weight: 700;
  color: var(--ksm-rc-rail-text, #fff);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* CAUSE (2.13, measured in Chrome, not predicted): a DIFFERENT active
   plugin -- wp-content/plugins/reviews-pro/assets/css/
   ksm-testimonials.min.css -- defines ".ksm-stars{height:18px;width:18px;
   color:gold;font-size:18px}" at the SAME (0,1,0) specificity as our own
   bare .ksm-stars (:88) and loads LATER, so on that tie it wins the
   height/width/color/font-size properties (display/gap are ours alone, no
   conflict). Our star glyphs still render at their own font-size (set
   directly on .ksm-star, which overrides the inherited font-size on its
   own element regardless of the parent's), but the CONTAINER they sit in
   is forced to an 18x18px box -- and since box overflow defaults to
   visible, the oversized glyphs escape that box and paint over whatever is
   above/below. This rule out-specifies the foreign one (three classes,
   (0,3,0), beats its (0,1,0)) and declares height/width EXPLICITLY rather
   than relying on inheriting out of the problem, which does not work here
   because our own rules never declared these properties at all -- the
   foreign rule was free to set them uncontested.
   STATUS (2.15, §2): reviews-pro has since been REMOVED from every local
   site -- this guard defends against nothing present locally any more.
   It is KEPT DELIBERATELY, not as unnoticed dead weight: the collision it
   defends against still occurs on any LIVE site still running that
   plugin, and this rule costs nothing to leave in place on a site where
   reviews-pro is absent. Do not remove it as dead code, and do not write
   any rule anywhere that assumes reviews-pro's absence -- see this
   plugin's own A3/A4 rulings, unchanged, for the full reasoning. */
.ksm-style-2 .ksm-style2-rail .ksm-stars {
  justify-content: center;
  height: auto;
  width: auto;
}

.ksm-style-2 .ksm-style2-rail .ksm-star {
  font-size: 34px;
  /* Row height no longer depends on inherited/theme line-height (measured
     "normal" in this font stack resolves to ~1.5x -- 34px * 1.5 = 51px,
     which is what researcher-george measured as the escaping glyph's
     scrollHeight). line-height:1 sets the line box to exactly 34px.
     VERIFIED this contains the glyph rather than assumed: measured via
     Canvas 2D text metrics on the live page's actual computed font stack
     (Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)
     -- the star glyph's real ink bounds (actualBoundingBoxAscent +
     actualBoundingBoxDescent) are 26px at font-size:34px, comfortably
     inside a 34px line box (8px to spare). Measured in Chrome; other
     engines/fonts are not re-verified here, and this is a text-metrics
     check, not the visual confirmation Stage 7 still owes. */
  line-height: 1;
}

/* Empty-star colour (2.14, A4): the filled/empty distinction is now carried
   by GLYPH (renderer.php emits the hollow ☆ for an unfilled star, solid ★
   for filled) rather than colour, so this colour is freed to be chosen for
   rail visibility alone -- .f (filled) keeps #FFA500 untouched, no override
   added for it here or anywhere in this file (see the frozen-colour note
   below the card-rating rules).
   var(--ksm-rc-rail-text, #fff) so the empty star goes dark on a light
   rail and light on a dark one, same source of truth as the rail's text.
   opacity 0.75 is MEASURED, not guessed: at 0.55 the default KSM rail
   (#0073aa) gives 2.64:1 against the empty star, below the 3:1 WCAG 1.4.11
   graphical-object threshold; 0.75 gives 3.63:1 there and clears 3:1 on a
   light and a dark rail too (see this stage's report for the full table).
   Filled star's own visibility against the rail is a SEPARATE, unresolved
   problem -- reported, not fixed here (colour is frozen). */
.ksm-style-2 .ksm-style2-rail .ksm-star:not(.f) {
  color: var(--ksm-rc-rail-text, #fff);
  opacity: 0.75;
}

/* Item 4 (2.22): checked, not assumed -- this renders "Based on <strong>N
   reviews</strong>", where N is $stats['total'] (an integer review
   count from get_review_stats(), class-renderer.php), interpolated into
   FIXED English text, not admin-supplied like the rail-word above. Genuinely
   lower risk: bounded by digit count, not an arbitrary string length.
   Still capped to one line for the same reason and at the same low
   cost -- a pathologically large count is unlikely but not impossible,
   and the alternative (leaving it unguarded on the strength of "probably
   fine") is exactly the assumption this plugin has been wrong about
   before. */
.ksm-style-2 .ksm-style2-rail-count {
  font-size: 16px;
  color: var(--ksm-rc-rail-text, #fff);
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ksm-style-2 .ksm-style2-rail-count strong {
  font-weight: 700;
}

/* Source line (2.14, §3): tried a de-emphasised opacity tier from the same
   --ksm-rc-rail-text property (as done for the empty star above) and
   measured it -- at 0.75 it gives only 3.63:1 on the default rail for this
   14px TEXT, below the 4.5:1 AA text threshold (text and graphical objects
   have different WCAG bars; the star's 3:1 graphical threshold does not
   apply here). Rather than reintroduce a fixed grey that would go invisible
   on a dark rail, full-strength var(--ksm-rc-rail-text, #fff) is used --
   still AA-safe on every rail this stage measured -- and the visual
   de-emphasis this line needs comes from its smaller size (14px vs the
   word's 26px) and regular weight instead. */
.ksm-style-2 .ksm-style2-rail-source {
  font-size: 14px;
  color: var(--ksm-rc-rail-text, #fff);
  /* 2.15, §3B: this now pairs an icon with the name (was text-only
     before this job) -- flex row so the two sit inline rather than the
     block's default stacking. justify-content:center keeps it centred
     to match the rail's other text lines above, which rely on
     text-align:center inherited from the rail wrapper -- a flex
     container needs that stated explicitly since text-align does not
     govern a flex child's own layout. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  max-width: 100%;
}

/* Item 4 (2.22): the source NAME's own truncation, same reason as
   .ksm-name and rail-word above -- a source name is admin/plugin-
   registration-controlled (KSM_RC_Source_Manager::register_source(),
   any get_name() a third-party integration supplies), so it is exactly
   as free a variable as the others fixed this stage. Truncating this
   directly on the FLEX CONTAINER (.ksm-style2-rail-source itself) would
   not work correctly -- text-overflow:ellipsis needs a single text box
   to act on, and this container also holds the fixed-size icon as a
   sibling; wrapping the name in its own span (class-renderer.php) lets
   only the TEXT truncate while the icon stays 20x20. min-width:0
   overrides the flex item's default min-width:auto, the same fix this
   file already uses elsewhere for a flex child that must be allowed to
   shrink/truncate below its own content width. */
.ksm-style-2 .ksm-style2-rail-source-name {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  min-width: 0;
}

/* Same round-mask INGREDIENTS as .ksm-style2-card-source-icon and
   .ksm-style2-avatar-icon (§3.5: object-fit:contain, padding inside the
   circle, box-sizing:border-box, a neutral background) applied to a
   SINGLE element rather than a wrapper+child pair -- this <img> has no
   separate wrapper div (unlike the other two), so border-radius:50% and
   the background-color sit directly on it instead of on a parent; a
   replaced element like <img> clips to its own border-radius the same
   way any other element does, so this still masks to a circle correctly.
   Multi-source sites never reach this element at all (renderer.php
   leaves "Verified reviews" as plain text when count(sources) > 1), so
   it only ever needs to represent one resolved icon. */
.ksm-style-2 .ksm-style2-rail-source-icon {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  overflow: hidden;
  background-color: #fff;
  object-fit: contain;
  padding: 2px;
  box-sizing: border-box;
  flex-shrink: 0;
}

/* --- Cards area: ~72-73%, the existing slider infrastructure above still
   drives paging inside it; this just sizes it as the panel's second column
   and reuses the SAME position:relative set above for the straddling
   controls. */
.ksm-style-2 .ksm-style2-panel > .ksm-reviews-container-wrapper {
  flex: 1 1 73%;
  box-sizing: border-box;
  min-width: 0; /* flex children need this to shrink below content size */
  padding: 1.75rem 2.25rem;
}

/* --- Card fill: #f5f5f5, ~8px radius, ~22px padding, ~24px gap (the gap
   is the item's own margin, matched to the existing D9 track-geometry
   margin math above so the same-sum-to-100% invariant is undisturbed --
   0.75rem each side = 1.5rem total ~= 24px). No border on top of the fill
   (a filled card does not also want an outlined edge); the existing hover
   lift (:220, frozen, shared with style 1) is left as-is, unmentioned by
   §5 and harmless. */
/* Item 1 (2.22), THE CLS FIX: fixed card height, derived term by term and
   reconciled against live measurement (researcher-george, 20 items) to
   the exact decimal on both anchors -- not observed from whichever
   review happens to be tallest today. Every rem term on this plugin
   resolves at 10px, not the browser default 16px -- confirmed by
   several independent prior measurements (see the .ksm-read-more margin
   comment further down) -- so 0.6rem=6px, 0.75rem=7.5px, 0.2rem=2px
   throughout.
   .ksm-inner's OWN term is fixed at 96px, not left as a range: the
   measured set showed BOTH 96px (7 items that genuinely overflow -- the
   clamp's own overflow:hidden clips the trailing paragraph's 5px margin
   away) AND 101px (2 items that don't overflow, so nothing clips that
   margin) for the SAME 4-clamped-lines-at-24px-each content. Rather than
   carry a card height built on the taller 101px case, the 5px is
   normalised away below (.ksm-inner p:last-child) and .ksm-inner itself
   is given a fixed 96px height -- one deterministic number, not a range
   that depends on whether a given review happens to overflow.
   ANCHOR ROWS: derived from the cards with ZERO stretch residue at each
   width (Conor Martin at both; also Alice Connors for the even grid row
   at 390) -- picking any shorter card would under-derive, since today's
   height comes from align-items:stretch matching the row's tallest
   member, not each card's own content in isolation.
   @390 (mobile): 44 padding + (52.5 header + 7.5 mb) + (25 rating + 6 mb)
     + (22 topic + 6 mb) + 96 inner + (24 read-more + 3.5 mt) = 286.5px
     -- MEASURED 286.5, zero residue.
   @1440 (desktop): 44 padding + (48 header + 7.5 mb) + (18 rating + 6 mb)
     + (22 topic + 6 mb) + 96 inner + (24 read-more + 3.5 mt) = 275px
     -- MEASURED 275, zero residue.
   Header/rating genuinely differ per breakpoint (52.5/48, 25/18) --
   taken from live measurement rather than re-derived a second time
   from source, since a static read of this file's own rules did not by
   itself explain that difference and the measured, reconciled figures
   are the more trustworthy source here.
   CONDITIONAL ROWS (topic, read-more): a FIXED height must accommodate
   the tallest possible content or it clips, so both are included as
   PRESENT in this sum even though neither always is -- a review with no
   topic, or one short enough to need no "Read more", leaves whitespace
   inside the fixed card instead. The brief explicitly accepts this.
   THE HEADER'S OWN REMAINING RISK, addressed not just noted: this row's
   height is not simple "tallest child wins" (52.5px measured EXCEEDS
   both the 48px avatar and the ~25px text block), and a long author
   name wrapping the .ksm-name line would grow it past the anchor value,
   clipping the fixed card. .ksm-name below is set to a single,
   non-wrapping, ellipsis-truncated line specifically so the header's
   height can never exceed what was measured, regardless of name length
   -- "regardless of content" per the brief covers the author name too,
   not only the review body.
   THE CLS MECHANISM, stated explicitly: with the webfont swapped for a
   fallback, the SAME rows measured 18.5px shorter at 390 and 9px
   shorter at 1440 (.ksm-inner is font-independent, line-height-driven,
   so none of that shrink comes from it) -- a height fixed at the
   webfont-loaded value ABSORBS that swap by construction, since the
   rows simply have room to shrink inside a box that does not itself
   move. That absorption, not merely "a number", is what eliminates the
   layout shift.
   box-sizing:border-box makes each total figure INCLUSIVE of this
   rule's own 22px padding (content-box, the default, would add padding
   on top and overshoot) -- this rule had no box-sizing before, added
   here specifically so the height figures mean what this derivation
   says they mean. Breakpoint-specific height set via the existing
   mobile media query, not duplicated here. */
.ksm-style-2 .ksm-review-item[data-source] {
  background-color: #f5f5f5;
  border: none;
  border-radius: 8px;
  padding: 22px;
  box-sizing: border-box;
  height: 275px;
  overflow: hidden; /* belt-and-braces against the derivation being wrong
                        somewhere -- content that somehow still exceeds
                        the fixed height is clipped rather than pushing
                        the card taller and reopening the CLS this stage
                        exists to close; .ksm-inner's own scroll (below)
                        is the intended escape hatch for genuinely long
                        text. */
  position: relative; /* 2.15, §3B: anchors the new top-right source icon
                          below -- this rule adds nothing visible on its
                          own, it only gives that absolutely-positioned
                          child a containing block. */
}

/* 2.15, §0/§3B: NEW markup -- the per-card top-right source icon did not
   exist before this job (grep for this class returned 0 hits in both
   this file and the renderer). Style-2 only, mirrors the SAME masked-
   circle technique .ksm-style2-avatar-icon already uses below rather
   than inventing a second one: a rounded, overflow-hidden wrapper with a
   neutral background clips to a circle and backs a transparent asset,
   the <img> itself uses object-fit:contain so a wide wordmark is
   scaled to fit rather than stretched or cropped mid-word (§3.5's round
   mask requirement). */
.ksm-style-2 .ksm-style2-card-source-icon {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  overflow: hidden;
  background-color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ksm-style-2 .ksm-style2-card-source-icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 3px;
  box-sizing: border-box;
}

/* Item 1 (2.16): the separate 0.75rem margin-override rule that used to
   sit here (added Stage 4 of 2.13, winning over the track-geometry
   block's own 0.5rem purely by coming later in the cascade at equal
   specificity) has been DELETED. Its value is now folded directly into
   the col-1/col-2/col-3 rules above, alongside the basis fix those same
   rules needed anyway -- a single correct rule beats two that only agree
   because nobody has edited one without the other yet, which is exactly
   how this stage's own bug shipped in the first place. Nothing here
   still needs a second rule to win a cascade tie. */

/* --- Card header: avatar 48px | name (bold) above date (grey) beneath ----
   .ksm-review-header (:230, frozen/shared) is a 3-column, 1-row CSS grid
   for style 1 (avatar | name+stars+verified | date, all one row) -- that
   rule is NEVER edited, only overridden here at higher specificity
   (.ksm-style-2 .ksm-review-header vs the bare .ksm-review-header) with a
   different grid shape, so style 1's own row-based layout is untouched. */
.ksm-style-2 .ksm-review-header {
  display: grid;
  grid-template-columns: 48px 1fr;
  grid-template-rows: auto auto;
  column-gap: 0.75rem;
  row-gap: 0.15rem;
  align-items: center;
  margin-bottom: 0.75rem;
}

.ksm-style-2 .ksm-review-header .ksm-profile-img {
  grid-column: 1;
  grid-row: 1 / span 2;
  width: 48px;
  height: 48px;
}

/* A4 (Amendment A, Stage 5): the source-icon fallback must be CONTAINED,
   never cropped or squashed -- a wordmark wider than it is tall must not
   be cut mid-word (object-fit:cover, the base .ksm-profile-img img rule's
   behaviour for the PHOTO path, :249-253, frozen, left untouched) nor
   stretched to a bare width/height. .ksm-style2-avatar-icon is a class
   only ever put on the SOURCE-ICON <img> (never the photo one), so
   out-specifying the shared rule here cannot affect the photo path.
   box-sizing:border-box keeps the inset padding INSIDE the existing
   width:100%/height:100% from that shared rule rather than overflowing
   the circle. No background-color of its own -- the wrapper's existing
   #e0e0e0 (:242, frozen) already reads as a neutral backdrop behind a
   contained, padded, possibly-transparent logo, consistent with the
   initial-letter fallback's own circle. */
.ksm-style-2 .ksm-style2-avatar-icon {
  object-fit: contain;
  padding: 6px;
  box-sizing: border-box;
}

.ksm-style-2 .ksm-review-header .ksm-profile-details {
  grid-column: 2;
  grid-row: 1;
  display: block; /* override the base column-flex (:260) -- only .ksm-name
                      is shown here for style 2, see below, so the flex
                      column of three stacked children no longer applies */
  gap: 0;
}

/* Item 1 (2.22): the header row's own height is not simple
   "tallest-child-wins" (measured 52.5px @390, exceeding both the 48px
   avatar and the ~25px text block) and is not otherwise pinned by
   anything else in this rule set -- an unusually LONG author name
   wrapping onto a second line would grow this row past the measured
   figure the fixed card height above is built on, clipping it. Capped
   to a single non-wrapping, ellipsis-truncated line so the header's
   height can never exceed what was measured, regardless of name
   length -- "regardless of content" (the brief's own phrase) covers the
   author name, not only the review body. */
.ksm-style-2 .ksm-review-header .ksm-name {
  font-weight: 700;
  font-size: 16px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The header's own stars (style 1's inline rating, :273) are duplicated for
   style 2 in the dedicated .ksm-style2-card-rating block below the header
   instead, so hidden here to avoid showing the row twice. The header's own
   verified badge (:277, style 1's green tick) is ALSO hidden here, but for
   a different reason as of 2.13: style 2 shows no verified indicator at
   all (Shane asked for the blue tick removed, Stage 3), so this rule now
   exists purely to stop style 1's green tick leaking into style 2's card
   via the shared header markup -- not to avoid a duplicate. */
.ksm-style-2 .ksm-review-header .ksm-profile-details .ksm-stars,
.ksm-style-2 .ksm-review-header .ksm-verified-review {
  display: none;
}

.ksm-style-2 .ksm-review-header .ksm-date {
  grid-column: 2;
  grid-row: 2;
  font-size: 14px;
  color: #666; /* same AA-reasoning as the rail source line above; the base
                  .ksm-date rule's #999 fails AA (2.85:1) */
  text-align: left;
}

/* --- Card rating row: stars ~18px, below the header -----------------------
   Carried the verified tick until Stage 3 of 2.13 (Shane: remove the blue
   check mark) -- removed, markup and rule both, so this is now a single-
   child row. gap dropped below since it only ever spaced the stars against
   the tick and has nothing left to space. */
.ksm-style-2 .ksm-style2-card-rating {
  display: flex;
  align-items: center;
  margin-bottom: 0.6rem;
}

/* Item 2 (2.16): topic chip, restored (Shane: "kind of nice when it shows
   somewhere on each review"). PLACEMENT: a normal in-flow block sibling
   between .ksm-style2-card-rating and .ksm-review-content, not absolutely
   positioned anywhere -- I considered and cleared all four things this
   card must not collide with:
     - .ksm-style2-card-source-icon (2.15's absolute top:12/right:12,
       24x24) -- this chip is never positioned in that corner at all, it
       sits below the header/rating row in normal flow, so there is no
       shared coordinate space to collide in;
     - the avatar -- lives inside .ksm-review-header, which this chip
       comes entirely AFTER as a later sibling;
     - the star row -- .ksm-style2-card-rating is this chip's own
       preceding sibling, not a container it sits inside, so nothing
       overlaps it;
     - the "Read more" line -- lives inside .ksm-review-content, which
       this chip sits BEFORE as an earlier sibling in normal block flow,
       so it can only ever push that content down, never overlap it.
   ABSENT CHIP, NO RESERVED SPACE: renderer.php emits NO element at all
   (not a hidden/empty one) when a review has no topic -- there is
   nothing here to reserve space, by construction, not by a CSS rule
   suppressing an empty box. What align-items:stretch actually equalises
   is the CARD's own outer height across a row, not each card's internal
   layout -- a card with one fewer internal block (no chip, exactly like
   a card with a shorter review already has less content) simply has its
   own content start and end at different internal points than a
   neighbour's, while the row's shared height is still set by the
   tallest card present, same as it always was before this chip existed.
   Nothing about the card's WIDTH or its slot in the row (Item 1's own
   fix, above) depends on whether this chip is present.
   CLAMP/READ-MORE MEASUREMENT: unaffected -- checked initTruncation() in
   assets/js/ksm-review-collector.js (untouched this stage) directly
   rather than assumed: it measures ONLY .ksm-inner's own scrollHeight
   vs clientHeight, and this chip is a sibling of .ksm-review-content
   (which CONTAINS .ksm-inner), never inside it, so it cannot be part of
   what that measurement sees. */
.ksm-style-2 .ksm-style2-card-topic {
  display: inline-block;
  align-self: flex-start; /* Item 2 fix (2.17): .ksm-review-item is
    display:flex;flex-direction:column with default align-items:stretch,
    so EVERY direct flex-item child -- including a bare display:inline-
    block one -- is stretched to the column's full cross-axis width
    regardless of its own display value; verified live, this chip
    measured 429.5px wide (a full-card grey bar) with only
    display:inline-block set, disproving the assumption that
    inline-block alone would stop it stretching in a flex context. This
    is a FLEX ITEM property, not a display-mode one: align-self opts
    THIS child out of the parent's stretch and sizes it to its own
    content instead, which is what display:inline-block was actually
    expected to do and, in a flex column, cannot do alone. */
  font-size: 12px;
  font-weight: 600;
  color: #666; /* quiet/secondary per the brief -- same AA-safe grey
                  already used for the rail source line and card date
                  elsewhere in this file, not a new colour choice. */
  background-color: #eee;
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  margin-bottom: 0.6rem;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap; /* a single short label, not a wrapping sentence --
                           an unusually long term name is elided rather
                           than allowed to widen or wrap the card. */
}

/* Foreign .ksm-stars cascade defeat, same cause as the rail rule above (see
   that comment) -- this row measured 18px actual vs 27px escaping glyph
   before the Stage 2 fix (researcher-george). */
.ksm-style-2 .ksm-style2-card-rating .ksm-stars {
  height: auto;
  width: auto;
}

.ksm-style-2 .ksm-style2-card-rating .ksm-star {
  font-size: 18px;
  /* Verified the same way as the rail: at font-size:18px the glyph's real
     ink bounds measure 15px (Canvas actualBoundingBoxAscent/Descent on the
     live page's computed font), comfortably inside an 18px line box. */
  line-height: 1;
}

/* --- Review text: ~16px/1.5, clamped to four lines with an ellipsis, then
   "Read more" beneath in grey.
   GATED BY .ksm-rc-js (added synchronously in <head>, Amendment 1 C1) so a
   no-JS visitor gets the FULL, unclamped review text -- criterion 11's
   "readable static grid" -- rather than text permanently cut off with no
   script able to ever expand it.
   The clamp must be something the shared JS (Stage 7) can undo without a
   JS change THIS stage: max-height/overflow (as style 1 already uses) is
   directly writable by the existing click handler's inline
   $content.css('max-height','none'), but plain max-height gives a hard
   clip with no ellipsis glyph. -webkit-line-clamp DOES render a native
   ellipsis, but is not itself affected by max-height at all -- setting
   max-height:none would not release it. Resolved with :has(), reacting to
   a REAL state the unmodified click handler already sets (.ksm-read-more-
   active on the button, toggled the same way for style 1 today), not a
   fragile inline-style string match: */
.ksm-rc-js .ksm-style-2 .ksm-review-item .ksm-inner {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
  font-size: 16px;
  line-height: 1.5;
  height: 96px; /* Item 1 (2.22): 4 lines x 24px (16px font x 1.5
    line-height) = 96, made an EXPLICIT height rather than left to the
    clamp's own implicit sizing -- the measured set showed this element
    at either 96px (content that overflows, so the clamp's overflow:
    hidden clips the trailing .ksm-inner p margin away) or 101px
    (content that doesn't overflow, so nothing clips that margin), for
    otherwise-identical 4-line content. An explicit height removes that
    variance outright, and .ksm-inner p:last-child below removes the
    5px margin that caused it in the first place, so both the clamped
    and (2.22 item 3) expanded-scroll states agree on the SAME 96px
    figure -- not a range depending on whether a given review happens
    to overflow. */
  color: #555; /* inherited from the base .ksm-inner rule (:299) anyway --
                  restated here only because this rule's higher specificity
                  would otherwise need it re-asserted if that ever changes */
}

/* Item 1 (2.22): removes the trailing paragraph's 0.5rem (5px) bottom
   margin, style 2 only -- the source of the 96px-vs-101px variance
   above. Scoped narrowly (last-child only, under .ksm-rc-js .ksm-style-2
   .ksm-review-item, matching the clamp rule's own gate) so style 1's
   review text and any other .ksm-inner p spacing elsewhere is untouched. */
.ksm-rc-js .ksm-style-2 .ksm-review-item .ksm-inner p:last-child {
  margin-bottom: 0;
}

/* Item 3 (2.22): Shane chose SCROLL over GROW -- "Read more" no longer
   releases the clamp into overflow:visible (which grew the card and is
   exactly the layout shift this round exists to close); it now reveals
   the full text WITHIN the same fixed window instead. max-height:96px
   is not a new number -- it is the SAME .ksm-inner term fixed on the
   clamp rule above, reused rather than re-derived, because the space
   this element is allotted inside the fixed card is the same whether
   clamped or expanded -- only whether OVERFLOW past that budget clips
   or scrolls changes. overflow-y:auto (not scroll) so a review that
   fits within 96px even unclamped shows no scrollbar and is not
   spuriously made scrollable. */
.ksm-style-2 .ksm-review-item .ksm-review-text-container:has(.ksm-read-more-active) .ksm-inner {
  -webkit-line-clamp: unset;
  line-clamp: unset;
  display: block;
  max-height: 96px;
  overflow-y: auto;
  overflow-x: hidden;
}

/* Default hidden (matches the 2.11 rule's OWN specificity/scope exactly,
   only the reasoning changes): Stage 7 re-enables initTruncation() for
   style 2, which will call the SAME $readMore.show()/.hide() the shared JS
   already uses for style 1, based on actually measured overflow -- an
   inline-style .show() always outranks this class rule regardless of
   specificity, so this is simply the correct BEFORE-JS-runs default. With
   no JS at all it stays hidden permanently, which is deliberately BETTER
   than a dead, inert link -- there is nothing for it to do since the text
   above is already showing in full in that case.
   RESOLVED IN JS, NOT HERE (2.20 follow-up -- do not "fix" this again by
   adding display:block to this rule, it cannot work): this rule stays
   display:none unconditionally so a JS-off visitor never sees a dead
   control -- that is deliberate, see above. The 2.20 <div>-to-<button>
   change (accessibility) briefly shrank this element's shown width by
   ~75%, because $readMore.show() (js:ksm-review-collector.js) finds it
   hidden BY THIS RULE and falls back to jQuery's own guessed tag
   default -- block for the <div> it used to be, inline-block for the
   <button> it is now -- an inline value that outranks this class rule
   regardless of what it says. Style 1's OWN read-more is unaffected: its
   base rule is display:inline-block, not none, so .show() there merely
   clears an inline 'none' and the CSS value already wins -- the tag
   change cannot reach it. FIX: the JS now sets
   $readMore.css('display','block') explicitly for the style-2 overflow
   branch instead of calling .show(), removing jQuery's guess from the
   path entirely. That fixed the DISPLAY guess but NOT the width -- see
   width:100% below for the second, independent cause.
   THE REUSABLE LESSON: a <div> and a <button> given the IDENTICAL
   display value do not lay out identically -- a block-level button
   still sizes its width to its own content, a block-level div fills its
   containing block. Confirmed with a controlled side-by-side test (a
   plain block button collapsed to content width in a 400px parent while
   a control block div filled it), not inferred from this site alone --
   this is ordinary CSS behaviour for form controls. Converting a <div>
   to a <button> anywhere else in this plugin for accessibility can
   silently shrink its hit area the SAME way, past every visual check,
   for this same reason.
   width:100% (measured, not guessed): the card's own padding is 22px
   each side (:1317 area); 473.5px card - 44px = 429.5px at 1440,
   345px card - 44px = 301px at 390 -- both match the pre-2.20 baseline
   exactly. padding:0/border:none on this button (above) mean
   box-sizing cannot change that result either way.
   BOTH FIXES ARE REQUIRED TOGETHER, not alternatives: without the JS
   fix, jQuery's own inline-block guess would override display before
   width:100% could ever apply the same way; without width:100%, a
   correctly-block button still collapses to content width. Removing
   either one on the (incorrect) assumption it is now redundant with the
   other reopens this regression. */
/* Item 2 (2.21): #999 -> #707070, Shane's ruling ("Read more" specifically;
   #FFA500 stays frozen). Re-derived independently with this plugin's own
   relative_luminance() formula (class-renderer.php) rather than adopting
   the coordinator's numbers on trust -- confirmed identical:
     #999999  L=0.31855  white 2.85:1  #f5f5f5 2.61:1  (today, fails BOTH)
     #767676  L=0.18116  white 4.54:1  #f5f5f5 4.17:1  (passes white, FAILS the card)
     #717171  L=0.16513  white 4.88:1  #f5f5f5 4.48:1  (still just fails the card)
     #707070  L=0.16203  white 4.95:1  #f5f5f5 4.54:1  (PASSES BOTH -- lightest that does)
   The control sits on the white panel (style 1) and the #f5f5f5 card fill
   (style 2), so it must clear 4.5:1 on both -- #707070 is what's used.
   Still visibly secondary to the review text (#555, .ksm-inner) at this
   font-weight/size -- reads as grey, not as body copy. */
.ksm-style-2 .ksm-review-item .ksm-read-more {
  display: none;
  width: 100%;
  color: #707070;
  /* Item 3 (2.21), same target-size requirement as style 1 above, same
     font-size-unchanged constraint. Measured height was 21px (14px font
     at line-height:inherit=1.5); width already 301px/429.5px, far past
     24px, so again only height was short. 21px + 3px (1.5px top + 1.5px
     bottom) = 24px exactly.
     margin-top: explicit px, not rem -- see the base .ksm-read-more
     rule's comment for why (1rem is 10px on this site, not 16px, three
     independent measurements). This element never overrode margin-top
     before this stage, it simply inherited the shared base rule's
     0.5rem = 5px. Reduced by the 1.5px TOP padding just added:
     5 - 1.5 = 3.5px. Style 1's own reduction (0.5px, on the base rule)
     used the SAME 5px starting point with ITS OWN 4.5px top padding --
     the two styles need different compensation because they need
     different padding, not because they started from different
     margins. */
  margin-top: 3.5px;
  padding: 1.5px 0;
  font-size: 14px;
  font-weight: 400;
}

.ksm-style-2 .ksm-review-content {
  order: 2;
  margin-top: 0;
  flex-grow: 1;
}

/* --- R8: dots hidden in style 2, SCOPED CSS ONLY -- the JS still updates
   them harmlessly (untouched) and style 1 still needs the markup, so
   nothing is removed, only hidden. */
.ksm-style-2 .ksm-controls-line {
  display: none;
}

/* --- Controls: circular ~38px, straddling the cards-area edges ----------
   NOW GATED (Stage 4, 2.13): paging controls only make sense while there is
   something to page BETWEEN -- with no JS every review is already shown at
   once in the base grid, so there is no "next page" for these to mean
   anything, and left ungated this absolutely-positioned overlay would sit
   over an arbitrary row of the (now potentially multi-row) grid rather than
   the single-row track it was built for, looking broken rather than merely
   inert. Gated behind .ksm-rc-js so a JS-off visitor falls through to the
   BASE .ksm-controls rule (:321-327, shared/frozen) instead -- a normal,
   in-flow, centred row below the grid. Its arrow buttons still render but
   do nothing without JS either way; that is accepted (matches this
   plugin's existing no-JS precedent elsewhere, e.g. the Read More link)
   rather than removing the markup, which is out of scope this stage.
   .ksm-reviews-container-wrapper stays position:relative unconditionally
   (declared above) -- harmless when nothing is absolutely positioned
   against it, and still correctly needed once .ksm-rc-js IS present. */
.ksm-rc-js .ksm-style-2 .ksm-controls {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 0;
  padding: 0 1rem;
  pointer-events: none; /* re-enabled on the arrows themselves, below */
}

.ksm-rc-js .ksm-style-2 .ksm-arrow-prev,
.ksm-rc-js .ksm-style-2 .ksm-arrow-next {
  pointer-events: auto;
  width: 38px;
  height: 38px;
}

.ksm-rc-js .ksm-style-2 .ksm-arrow-prev {
  margin-left: -19px;
}

.ksm-rc-js .ksm-style-2 .ksm-arrow-next {
  margin-right: -19px;
}

/* --- Star colour: UNCHANGED, #FFA500, per explicit instruction not to
   "improve" it (it measures ~1.98:1 on #fff / ~1.9:1 on #f5f5f5, below AA;
   Shane has been told and has ruled it stays). No rule in this file
   overrides .ksm-star.f's colour for style 2 -- the base rule at :98
   applies as-is, exactly like style 1. */

/* --- Footer: band + button ------------------------------------------------
   .ksm-verified-by (:402, frozen/shared) draws a border-top divider,
   appropriate on style 1's own grey box but not wanted here, sitting
   directly on the (possibly coloured) band with the white panel already
   providing its own visual separation above it. */
/* Item 1 (2.23): closes the last 5px of the total-widget shift -- the
   "Read all reviews" pill (.ksm-view-all-reviews, below) measured 40px
   webfont-loaded -> 35px fallback, the entire remaining 921->916 delta
   at 390px.
   RESERVED ON THE CONTAINER, NOT THE CONTROL ITSELF, and this was a
   choice, not the only option: .ksm-view-all-reviews is display:
   inline-block with NO explicit line-height of its own, so it inherits
   the ambient "normal" keyword -- which, unlike a numeric multiplier
   (e.g. line-height:1.5, immune to font swap), genuinely resolves
   differently per font, which is WHY this control's height moves at
   all. Giving the LINK its own explicit height would need touching
   line-height/vertical-align together to avoid shifting where the text
   sits inside its own box relative to the border/padding around it --
   real risk of moving the very thing item 1 forbids moving. min-height
   on THIS container instead cannot alter the link's own rendered box in
   ANY way (padding, border, colour, text position all untouched) -- it
   only stops the OUTER wrapper from shrinking when the inner content
   does. NOT a hard height, so a legitimately longer translated string
   (this button's text is plugin-defined, not admin/user content, but IS
   translatable) still has room to grow rather than being clipped;
   there is no "free variable" risk here of the kind the review text/
   author name/rail fields needed a hard ceiling for.
   THE VALUE IS 55px, NOT THE LINK'S OWN 40px -- A REAL DEFECT, CAUGHT BY
   MEASUREMENT AFTER SHIPPING AS A NO-OP: this container's own height is
   padding-top (1.5rem=15px, fixed) PLUS the link's font-variable height,
   not the link's height alone. webfont: 15+40=55px. fallback: 15+35=50px.
   min-height:40px sat BELOW BOTH states (even the shorter, 50px,
   already exceeds it), so the declaration was live -- it genuinely
   computed to 40px -- but structurally inert, since a min-height only
   does work when it is at least as tall as the TALLEST state it is
   meant to hold. The widget's total height was measured completely
   unchanged (921->916 at 390) with this value in place: a computed
   value proved nothing, exactly the trap this round has been building
   toward avoiding. 55px is the webfont-loaded CONTAINER height (not the
   link's own 40px) -- same "hold the taller state" principle as every
   other fixed value this round, applied to the right box this time.
   DO NOT "correct" this back to 40px on the strength of it looking more
   conservative -- 40 is the link's figure, not the container's, and it
   is proven inert at this position. */
.ksm-style-2 .ksm-verified-by {
  border-top: none;
  padding-top: 1.5rem;
  min-height: 55px;
}

/* GAP, FLAGGED RATHER THAN INVENTED (see this stage's report): R7's
   get_band_button_text_colour() is defined (Stage 4) but its return value
   is never emitted anywhere in the Stage 5 markup -- no inline style, no
   data attribute, no custom property. This rule is written AGAINST the
   hook it WOULD need, --ksm-rc-band-text, set inline by a future PHP
   change alongside R6's background-color on .ksm-widget. Until that PHP
   change exists, the custom property is always undefined, so this always
   falls through to the "inherit" fallback -- i.e. every page currently
   gets the "no band" button styling (border/colour from the theme's own
   inherited colour) regardless of whether a band colour is actually set.
   This does NOT crash or misrender anything -- it degrades to the safe
   no-band look -- but it means criterion 9's "a set colour's button text
   passes AA by computation" is not yet reachable in the browser. */
.ksm-style-2 .ksm-view-all-reviews {
  display: inline-block;
  padding: 0.75rem 2rem;
  border: 2px solid currentColor;
  border-radius: 999px;
  color: var(--ksm-rc-band-text, inherit);
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  transition: opacity 0.3s ease;
}

.ksm-style-2 .ksm-view-all-reviews:hover {
  opacity: 0.8;
  text-decoration: none;
}

/* ==========================================================================
   Mobile: single column, rail ABOVE the cards
   ========================================================================== */
@media (max-width: 768px) {
  .ksm-style-2 .ksm-style2-panel {
    flex-direction: column;
  }

  .ksm-style-2 .ksm-style2-rail {
    flex: 1 1 auto;
    padding: 1.5rem 1.25rem;
    /* Item (2.18): at this width the panel is flex-direction:column and
       the rail sits ABOVE the cards at full width (:1611 area, this same
       media query) -- its desktop border-radius (12px 0 0 12px, rounding
       the LEFT side) would square off the panel's actual top corners
       here instead of rounding them. Overridden to round the TOP corners
       instead, matching where the rail actually sits at this width. */
    border-radius: 12px 12px 0 0;
    /* Item 4 (2.22): fixed rail height, mobile-only, and GENUINELY
       INDEPENDENT of the card fix above -- flex-direction:column at
       this width means the rail is NOT stretch-matched to the cards
       column at all (that mechanism only operates on the cross axis at
       flex-direction:row, i.e. desktop), so nothing about fixing the
       cards touches this element. Measured (webfont -> fallback
       swap): rail 169px -> 151px, decomposed with no residue across
       its four children:
         rail-word    39 -> 31  (-8)
         .ksm-stars   36 -> 34  (-2)
         rail-count   25 -> 18  (-7)
         rail-source  21 -> 20  (-1)
         total               -18, matching the rail's own measured delta
       exactly. Fixed at 169px -- the WEBFONT-LOADED (taller) value, same
       reasoning as the cards: fixing to the shorter fallback would let
       the taller webfont content overflow once it actually loads.
       overflow:hidden as the same belt-and-braces the card rule uses,
       for the same reason -- content that somehow still exceeds this
       is clipped rather than pushing the rail (and therefore the whole
       mobile panel) taller. */
    height: 169px;
    overflow: hidden;
  }

  .ksm-style-2 .ksm-style2-panel > .ksm-reviews-container-wrapper {
    padding: 1.25rem 1.25rem 1.75rem;
  }

  /* Item (2.19): finger-swipe paging, CSS scroll-snap. This is the SAME
     element 2.17 introduced with zero padding specifically so its
     padding-box and content-box clip edges coincide -- that property is
     exactly what makes it a trustworthy SCROLLPORT too: its visible
     scrolling viewport is precisely the track's own edge, no padding to
     create a gap for a sliver to hide in, the same guarantee reused for
     a second purpose rather than re-derived.
     UNGATED (no .ksm-rc-js anywhere in this selector), DELIBERATELY: this
     must work identically with or without JS, since native scroll+snap
     needs no script at all -- a JS-off visitor gets a genuinely
     swipeable/keyboard-scrollable stack instead of today's static one,
     which is strictly better, not a compromise. Doubling .ksm-style-2
     raises this to (0,3,0) -- matching (and, by file order, beating) the
     desktop-scope GATED rule of the same base selector
     (.ksm-rc-js .ksm-style-2 .ksm-style2-track-clip, also (0,3,0)) so
     this wins at mobile width regardless of whether .ksm-rc-js is
     present, the same idiom this file already uses elsewhere (the
     reduced-motion view-all-reviews fix, the visibility exemption) when
     a plain viewport-only selector would tie DESKTOP behaviour under JS
     rather than losing to it cleanly.
     overflow-y:hidden alongside overflow-x:auto -- this must scroll
     horizontally only; a vertical scrollbar was never wanted here and
     the row-gap between the two stacked cards is not a scroll axis.
     scroll-snap-type:x mandatory + overscroll-behavior-x:contain (so a
     swipe cannot trigger the browser's own back/forward navigation).
     scroll-behavior:smooth here, overridden to auto under
     prefers-reduced-motion in the compound query further down (mirrors
     exactly how the existing transform transition is handled: smooth by
     default, instant under reduced motion).
     Scrollbar hidden WITHOUT removing keyboard access: scrollbar-width/
     -ms-overflow-style/::-webkit-scrollbar only hide the SCROLLBAR UI,
     they do not affect whether the element can be focused or scrolled by
     keyboard -- that is what the tabindex="0" on this element in
     class-renderer.php is for (a plain scrollable div is not in the tab
     order by default; see that file's own comment on this same
     element). */
  /* Item 4 (2.22): fixed clip height, mobile. A mobile "page" is TWO
     cards STACKED (2.14's grid-auto-flow:column mechanism, unchanged),
     separated by the SAME row-gap:1rem (10px at this file's own
     established 1rem=10px) the grid rule below already declares --
     reused, not a new number. 286.5 (card @390, see
     .ksm-review-item[data-source]'s mobile override) + 10 (row-gap) +
     286.5 (card) = 583px. UNGATED, matching this rule's own existing
     scope (2.19 made the whole mobile scroll-snap mechanism
     JS-independent on purpose; the height fix stays consistent with
     that, not re-gated). */
  .ksm-style-2.ksm-style-2 .ksm-style2-track-clip {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scroll-behavior: smooth;
    scrollbar-width: none;
    -ms-overflow-style: none;
    height: 583px;
  }

  .ksm-style-2.ksm-style-2 .ksm-style2-track-clip::-webkit-scrollbar {
    display: none;
  }

  /* Item 1 (2.14, §2): TWO CARDS STACKED, paging two per click, via CSS
     ONLY -- no change anywhere to updateSlider()'s maths (js:127-174,
     unread/untouched this stage) or to how `columns` is read (js:33-34,
     from the ksm-col-N class, never the viewport). That maths already
     advances exactly `columns` items per translateX(-currentPage*100%)
     step; only the LAYOUT was wrong for mobile, not the paging.
     MECHANISM: display:grid with grid-auto-flow:column and exactly two
     explicit row tracks packs items column-major -- items 1-2 into
     column 1, 3-4 into column 2, and so on -- with NO DOM change and NO
     JS change, so the existing page index still lands on exactly the
     right pair.
     EXPLICIT vs IMPLICIT COLUMNS (Stage 7 fix -- a real defect, found in
     the browser: column 1 measured 44px instead of 100%, only on the
     FIRST page). grid-auto-columns only ever sizes IMPLICIT tracks --
     ones the grid creates itself because content overflows the explicit
     ones. Without grid-template-columns:none below, this element still
     inherits `grid-template-columns: 1fr` from the shared 992px
     breakpoint (:709-716) and `repeat(2, 1fr)` beneath that (:196-201) --
     ONE explicit `1fr` column plus nine implicit `100%` ones. `1fr` is
     `minmax(auto, 1fr)`; with the implicit columns overflowing the
     container the free space goes negative, so the explicit track
     collapses toward its min-content contribution instead of taking
     grid-auto-columns' value, which cannot reach an explicit track at
     all. grid-template-columns:none removes the explicit track entirely,
     so all ten columns are implicit and every one takes
     grid-auto-columns:100% uniformly -- BY CONSTRUCTION, not by
     arithmetic that happens to land on the right number. With that fix
     in place: grid-auto-columns:100% sizes every column at 100% of THIS
     element's own box -- the SAME box translateX(-100%) already shifts
     by (both percentages resolve against the grid/track element's own
     computed width, which stays width:100% of the wrapper,
     unconditionally, above) -- so a page step and a column width are
     numerically identical and the slide lands exactly on the next pair,
     pixel for pixel, the same guarantee the desktop row mechanism already
     relies on, rotated 90 degrees.
     WHY GRID OVER THE OBVIOUS flex-direction:column;flex-wrap:wrap
     ALTERNATIVE: that needs a DEFINITE height on the container to know
     where to wrap into a new column, and review length varies (the
     four-line clamp bounds it but does not equalise it) -- an unreliable
     foundation. Grid's explicit grid-template-rows: repeat(2, auto) needs
     no such height: it fixes the ROW COUNT instead, and the two row
     tracks size themselves from the tallest item CSS places in each row
     across the WHOLE grid (i.e. row 1's height is set by the tallest of
     items 1, 3, 5...) -- which also means every page renders at the same
     total height, no jump between pages, consistent with the
     align-items:stretch equal-height behaviour the desktop row already
     has (:975 area).
     column-gap MUST stay 0 -- exactly like the desktop track's own
     gap:0 -- for the same reason: any column-gap here would make a
     column's actual left edge land past the 100% translateX shifts to,
     breaking the pixel-exact alignment above. row-gap is free to be
     whatever reads well WITHIN a stacked pair, since it has no bearing on
     the horizontal step; 1rem is chosen to match this file's existing
     1rem/1.5rem rhythm, nothing measured about that number specifically.
     ROW COUNT PER ksm-col-N (Stage 4b correction): a "page" here is one
     grid COLUMN, and updateSlider() steps currentPage by exactly
     `columns` items regardless of viewport (js:33-34/141-142) -- so the
     row count below must equal `columns` for every N, not just the
     default. The relationship is simply rows == columns-per-page. This
     base rule sets the ksm-col-2 default (2 rows, §2's settled "two cards
     stacked"); the two variants immediately below raise specificity by
     one class (.ksm-col-1/.ksm-col-3, (0,4,0) vs this rule's (0,3,0)) to
     override the row count ONLY for an explicit columns=1/columns=3
     shortcode attribute, so paging stays aligned with however many items
     updateSlider() actually advances by. col-1 therefore shows one card
     per mobile page, col-3 shows three stacked -- taller than the
     two-card default, but correct paging beats a shorter, item-skipping
     page. Mirrors this file's own established precedent of keeping
     col-1/col-3 variants "for completeness using the exact same proven
     math" (the desktop :915-928 rules) -- not a new pattern, the same one
     applied at this breakpoint. */
  /* Item (2.19): UNGATED from .ksm-rc-js on purpose -- see the report for
     the full "redo the arithmetic" account this stage's brief demanded.
     Doubling .ksm-style-2 raises this to (0,3,0), tying (and, by later
     file order, beating) the desktop-scope GATED rule of the identical
     base selector (.ksm-rc-js .ksm-style-2 .ksm-reviews-container, also
     (0,3,0)) so this grid layout applies at mobile width in BOTH JS
     states -- previously it only existed at all when .ksm-rc-js was
     present, and a JS-off visitor fell through to the single-column base
     grid with nothing to swipe. Native scroll+snap needs no script, so
     that gate no longer has a reason to exist. */
  .ksm-style-2.ksm-style-2 .ksm-reviews-container {
    display: grid;
    grid-auto-flow: column;
    grid-template-columns: none; /* Stage 7 fix: removes the inherited
      explicit 1fr track (:196-201/:709-716) so every column is implicit
      and takes grid-auto-columns below uniformly -- see the mechanism
      comment above. NOT grid-template-columns:100%, which would leave
      one explicit track that is only coincidentally the right size. */
    grid-template-rows: repeat(2, auto);
    grid-auto-columns: 100%;
    column-gap: 0;
    row-gap: 1rem;
    /* transform: none !important (2.19) -- REINTRODUCED, and this is NOT
       a reflexive restoration of what 2.14 removed from this exact spot;
       read the reasoning before "fixing" it back either way.
       2.14 removed this because, at the time, MOBILE PAGING WAS DRIVEN
       BY TRANSFORM (translateX, same as desktop) -- !important-ing it to
       none would have killed mobile paging outright, so it had to go.
       2.19 changes the MECHANISM ITSELF: mobile paging is now native
       scroll (the track-clip element above), not transform. updateSlider()
       is unaware of the mobile/desktop distinction and always writes an
       inline transform to this element regardless of viewport -- at
       mobile width that transform must NOT visually offset the
       now-scrolled track on top of the scroll position, or the two
       mechanisms would fight each other. This declaration is doing the
       OPPOSITE job to its 2.11-2.13 ancestor (that one existed to kill a
       mechanism that shouldn't run; this one exists to neutralise a
       DIFFERENT mechanism's leftover output so the NEW one is what's
       seen), and it is authorised for exactly that reason -- do not read
       this as undoing 2.14's fix, and do not remove it by reflex either. */
    transform: none !important;
  }

  .ksm-style-2.ksm-style-2 .ksm-col-1 .ksm-reviews-container {
    grid-template-rows: repeat(1, auto);
  }

  .ksm-style-2.ksm-style-2 .ksm-col-3 .ksm-reviews-container {
    grid-template-rows: repeat(3, auto);
  }

  /* Grid items need none of the old flex-basis/grow/shrink shorthand --
     it is simply ignored once the parent is display:grid, left in as dead
     weight would only confuse a future reader. A grid item's inline size
     already defaults to filling its column (stretch), so no explicit
     width is needed; margin-bottom is removed because row-gap on the
     container above now provides that same 1rem spacing WITHIN a pair
     without doubling it, and margin cannot add space BETWEEN columns
     here regardless (column-gap:0 is load-bearing, see above) so a
     leftover side margin would only ever show on the left/right of the
     visible pair, not between pairs -- removed rather than left stale.
     Item (2.19): UNGATED (was .ksm-rc-js-prefixed) for the same reason as
     the grid container above, and doubled to (0,7,0) -- one class HIGHER
     than the desktop col-N rule's (0,6,0) that also targets this exact
     selector list (:967-984 area, unchanged, gated, desktop-only in
     practice since it never wins here any more) -- an outright win, not
     a tie relying on file order, the same idiom this file already uses
     when a delicate tie has bitten it before (the reduced-motion
     view-all-reviews fix documents the identical reasoning).
     THE ARITHMETIC, redone as required rather than assumed unaffected:
     margin stays 0 in BOTH the old gated-JS-on and the new ungated
     JS-off cases -- before this stage a JS-off visitor never matched
     this rule at all (no .ksm-rc-js class present) and so had NO item
     margin by simple absence, since nothing else in this file ever set
     one on a JS-off mobile item either; after ungating, this rule now
     matches and explicitly sets margin:0, the SAME numeric outcome by a
     different route. column-gap:0 above is likewise the only thing that
     has ever set gap for this container at style-2 mobile width (the
     base/frozen .ksm-reviews-container rule's gap:1.5rem, (0,1,0), is
     trivially outranked); no other rule sets column-gap here at any
     specificity, so there is nothing for these two declarations to
     double against, in either JS state. scroll-snap-align (2.19): BOTH
     cards in a mobile column share the same X-offset (2.14's own
     mechanism, unchanged), so snapping every item is sufficient for
     PAGE-level snapping -- no separate page/column wrapper element is
     needed, and none was added. */
  .ksm-style-2.ksm-style-2 .ksm-col-1 .ksm-reviews-container > .ksm-review-item[data-source],
  .ksm-style-2.ksm-style-2 .ksm-col-2 .ksm-reviews-container > .ksm-review-item[data-source],
  .ksm-style-2.ksm-style-2 .ksm-col-3 .ksm-reviews-container > .ksm-review-item[data-source] {
    margin: 0;
    min-width: 0; /* grid's own default min-width:auto can force a column
                     wider than 100% around long unbreakable content (e.g.
                     a long URL in review text); 0 matches the min-width:0
                     reset this same class of problem already needs under
                     flexbox elsewhere, same fix, same reason, different
                     layout mode. */
    scroll-snap-align: start;
  }

  /* Item 1 (2.22): fixed card height, mobile override -- 286.5px, per the
     derivation on the base .ksm-review-item[data-source] rule above
     (header/rating measure taller at this width than at 1440; the
     padding/topic/inner/read-more terms are the same at both). This
     selector list matches the item-margin rule immediately above it
     (same specificity, same reason -- it must beat the desktop rule's
     own height at this width, in both JS states). */
  .ksm-style-2.ksm-style-2 .ksm-col-1 .ksm-reviews-container > .ksm-review-item[data-source],
  .ksm-style-2.ksm-style-2 .ksm-col-2 .ksm-reviews-container > .ksm-review-item[data-source],
  .ksm-style-2.ksm-style-2 .ksm-col-3 .ksm-reviews-container > .ksm-review-item[data-source] {
    height: 286.5px;
  }

  /* Left UNGATED, deliberately: with no JS this hides the BASE .ksm-controls
     fallback row (see the desktop controls comment above) at mobile widths
     too, which is a harmless, arguably nicer, consistent no-JS-mobile look
     and costs nothing to leave applying either way. This rule alone is
     CORRECT for the no-JS path but NOT SUFFICIENT for the JS-on path -- see
     the gated rule immediately below for why both are needed together.
     UNCHANGED this stage -- do not read the "STAGE 8 FIX" history below as
     applying to this rule too, it does not; this is the no-JS rule, that
     one is the JS-on rule, and only the JS-on one changes. */
  .ksm-style-2 .ksm-controls {
    display: none;
  }

  /* Item 1 (2.14, §2): the JS-on mobile controls must now SHOW, not hide --
     the STAGE 8 history below explains why this selector has to stay
     .ksm-rc-js .ksm-style-2 .ksm-controls (0,3,0), matching the desktop
     gated rule's own specificity so file order (this rule, later) keeps
     deciding the tie the way it always has in this file, rather than
     leaving that guarantee to chance a third time.
     PLACEMENT CHOSEN: a static, centred row BELOW the cards -- i.e. this
     rule undoes the desktop rule's position:absolute/straddling treatment
     rather than adjusting it, falling back onto the UNGATED base
     .ksm-controls rule (:321-327, frozen, flex/justify-content:
     center/gap:1rem/margin-top:2rem) that already draws exactly that kind
     of row and was built for it. WHY: the desktop arrows straddle the
     panel edge -- fine when the cards area is ~73% of a wide panel, but
     at mobile the rail sits ABOVE the cards (this file's own :1422-1429)
     and the cards area is the full panel width, so "the edge" is directly
     over review text, not clear space. A below-cards row sidesteps that
     overlap entirely rather than trying to find a straddle-safe offset on
     a width that varies by phone. margin-top is NOT re-declared here --
     it keeps the base rule's 2rem, an existing value, not a new one
     invented for this stage. */
  .ksm-rc-js .ksm-style-2 .ksm-controls {
    display: flex;
    position: static;
    transform: none;
    left: auto;
    right: auto;
    justify-content: center;
    padding: 0;
    pointer-events: auto;
  }

  /* >=44px per §2's touch-target requirement (desktop's 38px, :1358 area,
     is too small here and is not reused). No negative margins either --
     those existed only to pull the desktop arrows in against the
     straddled edge; a static in-flow row needs none of that. */
  .ksm-rc-js .ksm-style-2 .ksm-arrow-prev,
  .ksm-rc-js .ksm-style-2 .ksm-arrow-next {
    width: 44px;
    height: 44px;
    margin-left: 0;
    margin-right: 0;
  }
}

/* Item (2.19): reduced motion for the mobile finger-swipe scroll,
   mirroring the desktop transform transition's own reduced-motion
   override (:905 area) -- smooth by default, instant when the visitor
   has asked for less motion. A COMPOUND media query (both conditions
   must hold) rather than nesting this inside the plain mobile block
   above: it only ever needs to apply at the SAME viewport as the
   scroll-behavior:smooth rule it overrides, and placing it in its own
   block, LATER in the file, lets the established "equal specificity,
   later wins" idiom settle the tie the same way it always does in this
   file -- the doubled .ksm-style-2 class here matches that rule's own
   (0,3,0) exactly, on purpose, not coincidentally. */
@media (max-width: 768px) and (prefers-reduced-motion: reduce) {
  .ksm-style-2.ksm-style-2 .ksm-style2-track-clip {
    scroll-behavior: auto;
  }
}

/* ------------------------------------------------------------------
   2.24.1 hotfix (2026-09-07): "Read more" on the style-2 slider must
   actually reveal the review. Measured live before this fix: after the
   click .ksm-inner stayed at clientHeight 96 (scrollHeight 360) because
   the clamp rule's explicit height:96px was never released -- the 2.22
   expanded rule only added a max-height, so the visitor saw the same
   four lines with no visible scrollbar. Shane's ruling reverses 2.22
   item 3 ("scroll over grow") for the EXPANDED state only: the text
   box, its card, every card in the same track (so the row keeps a
   level bottom edge via flex/grid stretch) and the track clip all
   release their fixed heights while any card in the track carries an
   active Read more. The clamped/initial state is untouched, so the
   2.23 zero-CLS figures still hold; a user-initiated expand is
   excluded from CLS by definition. "Read less" removes the class and
   every rule below stops matching, restoring today's exact figures.
   Every selector here is strictly more specific than the rule it
   overrides (one extra .ksm-rc-js / :has() term), no !important.
   ------------------------------------------------------------------ */
.ksm-rc-js .ksm-style-2 .ksm-review-item .ksm-review-text-container:has(.ksm-read-more-active) .ksm-inner {
  height: auto;
  max-height: none;
  overflow: visible;
}
.ksm-rc-js .ksm-style-2.ksm-style-2 .ksm-reviews-container:has(.ksm-read-more-active) > .ksm-review-item[data-source] {
  height: auto;
  min-height: 275px;
}
.ksm-rc-js .ksm-style-2.ksm-style-2 .ksm-style2-track-clip:has(.ksm-read-more-active) {
  height: auto;
}
@media (max-width: 768px) {
  .ksm-rc-js .ksm-style-2.ksm-style-2 .ksm-reviews-container:has(.ksm-read-more-active) > .ksm-review-item[data-source] {
    height: auto;
    min-height: 286.5px;
  }
  .ksm-rc-js .ksm-style-2.ksm-style-2 .ksm-style2-track-clip:has(.ksm-read-more-active) {
    height: auto;
  }
}

/* ==========================================================================
   2.25 -- LIST STYLE 2 (Stage 2, CSS only). APPEND-ONLY.

   Everything below is scoped under .ksm-list-style-2, the extra class the
   Stage-1 renderer puts on the list wrapper alongside .ksm-widget and
   .ksm-style-2 (render_list_style_2(), class-renderer.php). Nothing here
   can reach the slider or list style 1: the slider's wrapper never carries
   .ksm-list-style-2, and style 1's wrapper carries neither that nor
   .ksm-style-2. No existing selector or declaration in this file was
   edited for this block -- the whole of style 2's card language (fill,
   radius, padding, source icon, avatar, stars, topic chip, read-more,
   the 4-line clamp and the 2.24.1 grow-on-expand release) is reused as
   it stands, and only the three things that are genuinely slider-shaped
   are overridden here: the card's fixed height, the rail's column
   layout, and the load-more button's colours.

   Lengths are in px on purpose, not rem: this file's own existing
   comments record that 1rem is 10px on the KSM theme, so a rem figure
   here would silently mean something else on a theme that leaves the
   root at 16px. 24px is the slider's own card gap (0.75rem each side
   = 1.5rem = 24px, per the D9 track-geometry note above), reused rather
   than re-invented, so the grid's gutters read identically to the
   slider's.

   Colours come only from the same four custom properties the slider's
   rail already uses -- --ksm-rc-rail-bg, --ksm-rc-rail-text,
   --ksm-rc-rail-scrim and --ksm-rc-band-text -- with the same fallbacks
   the rail rule declares. No theme tokens, no new colour constants, no
   !important anywhere in this block.
   ========================================================================== */

/* --- The grid itself.
   .ksm-reviews-list is display:flex/column at (0,1,0) further up this
   file (the shared list container rule); this selector is (0,4,0), so it
   wins on specificity rather than on file order, and it wins ONLY on the
   list-style-2 wrapper. align-items:stretch is stated explicitly even
   though it is grid's initial value -- it is the property that gives a
   two-column row equal card heights now that the cards no longer carry a
   fixed pixel height, so it is load-bearing here and must not be read as
   a redundant default and deleted.
   One column below 768px, two at and above it, per the brief. Mobile
   first, so a browser that never evaluates the media query still gets a
   readable single column. */
.ksm-style-2.ksm-list-style-2 .ksm-reviews-list.ksm-list-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 24px;
  align-items: stretch;
}

@media (min-width: 768px) {
  .ksm-style-2.ksm-list-style-2 .ksm-reviews-list.ksm-list-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --- Cards must stay visible with JS on (the list's own D2).
   NOT cosmetic -- without this the style-2 list renders EMPTY as soon as
   JS runs. The frozen gate at :225,
   ".ksm-rc-js .ksm-review-item[data-source]:not([data-source='all'])
   { display: none; }", hides every card carrying a data-source once
   .ksm-rc-js is on the <html> element. The slider escapes it through the
   D2 rule at :930, but that rule is scoped to .ksm-reviews-container --
   an element the list does not have (its cards live in
   .ksm-reviews-list.ksm-list-grid), so the list gets no reprieve from it.
   MEASURED, not reasoned about: with the block below removed, headless
   Chrome reported 0 visible cards and card height 0 at 1400px on the
   Stage-1 markup; with it in place, 4.
   Specificity, worked the same way :930's comment works it: the gate is
   .ksm-rc-js (class) + .ksm-review-item (class) + [data-source] (attr) +
   the [data-source] attr inside :not() = (0,4,0). This selector is
   .ksm-style-2 + .ksm-list-style-2 + .ksm-list-grid + .ksm-review-item
   + :not(.ksm-review-hidden) (a class inside :not() counts) = 5 classes,
   + [data-source] = (0,6,1). Strictly higher, so it wins outright rather
   than on source order. If a future stage prefixes the :225 gate, this
   selector must be re-checked against it, exactly as :930 warns.
   The :not(.ksm-review-hidden) term is load-bearing and must not be
   dropped as noise: .ksm-review-hidden{display:none} is only (0,1,0), so
   without the exclusion this rule would out-specify it and reveal every
   card at once, destroying "View More Reviews" -- which reveals cards by
   removing that class and nothing else (initListViewLoadMore() writes no
   inline display), so this rule is also precisely what makes a revealed
   card appear, and what makes it :visible to the truncation pass that
   decides whether its "Read more" is warranted.
   display:flex + column, copied from :930 rather than invented: the card's
   own children (topic chip's align-self, the read-more's full width) are
   written against a flex column parent. */
.ksm-style-2.ksm-list-style-2 .ksm-list-grid .ksm-review-item[data-source]:not(.ksm-review-hidden) {
  display: flex;
  flex-direction: column;
}

/* --- Cards size to content, not to the slider's fixed height.
   The slider fixes .ksm-style-2 .ksm-review-item[data-source] to
   height:275px so its track cannot shift (2.22 item 1). That figure is a
   TRACK constant -- it exists because every card in a slider page must
   agree on one height -- and it has no meaning in a list, where a row of
   two cards can simply agree with each other. Released to auto here so a
   short review is a short card and a long one is a tall card; the equal
   heights within a row come from the grid's align-items:stretch above,
   which the fixed height would otherwise defeat outright (a definite
   height wins over stretch).
   min-height:0 is deliberate and not decorative: it guarantees no
   inherited floor re-imposes the 275px look through a longhand this rule
   does not name.
   The 4-line read-more window is NOT touched -- .ksm-inner keeps its
   height:96px clamp from the rule above, so an unexpanded card is still
   as compact as the slider's, and expansion still works through the
   existing 2.24.1 release of .ksm-inner's height/max-height/overflow
   (that rule is scoped to .ksm-review-text-container:has(...), not to
   the slider's .ksm-reviews-container, so it matches list cards exactly
   as written). With the card at height:auto, a released .ksm-inner
   simply makes the card taller -- and its row-mate stretches with it --
   which is the grow behaviour Stage 2 was asked to preserve; no
   list-scoped copy of the 2.24.1 mechanism is needed, and adding one
   would be a second source of truth for the same behaviour.
   Specificity is (0,3,1) against the fixed-height rule's (0,2,1), so
   this wins by specificity, not by file order.
   Not overridden here, on purpose: the mobile 286.5px card height. That
   rule is scoped .ksm-style-2.ksm-style-2 .ksm-col-1/2/3
   .ksm-reviews-container > .ksm-review-item[data-source] -- a list card
   sits in neither a .ksm-col-N wrapper nor a .ksm-reviews-container, so
   it never matches, and a defensive override would be a rule that can
   never apply. */
.ksm-style-2.ksm-list-style-2 .ksm-review-item[data-source] {
  height: auto;
  min-height: 0;
}

/* --- Summary band: the slider's rail laid across the top.
   Same element, same classes, same colours -- only the axis changes,
   because a list has no side column for a rail to be. The base rail rule
   sets flex:0 0 27%, flex-direction:column and a left-rounded radius,
   all three of which describe a 27%-wide left-hand column; each is
   restated here for a full-width band instead.
   The rail's background, its --secondary inset stripe and its
   Branch-A/Branch-B scrim are inherited untouched from that same base
   rule, so a band renders in exactly the colours the slider's rail would
   on the same site, from the same custom properties.
   (0,3,0) against the base rule's (0,2,0), so it wins on specificity at
   desktop; the mobile override below handles the (0,2,0) media-query
   rule separately. */
.ksm-style-2.ksm-list-style-2 .ksm-style2-rail--horizontal {
  flex: 0 0 auto;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 6px 24px; /* row gap, column gap -- the column figure is the same
                    24px gutter the grid above uses, so the band's
                    internal rhythm matches the cards below it. */
  padding: 18px 24px;
  border-radius: 12px;
  margin-bottom: 24px;
}

/* Rating word: the rail's 26px is sized for a narrow column where it is
   the only thing on its line. On a band it shares a row with three other
   items, so it is brought down to 22px -- still the largest and boldest
   thing in the band, still uppercase, still driven by
   --ksm-rc-rail-text through the base rule (colour is NOT restated here,
   so the contrast helper in class-renderer.php remains the single source
   of truth for it). The base rule's one-line truncation guard for a
   40-character admin override is inherited unchanged. */
.ksm-style-2.ksm-list-style-2 .ksm-style2-rail--horizontal .ksm-style2-rail-word {
  font-size: 22px;
}

/* Stars: 34px is a rail figure for the same reason -- it fills a column.
   22px keeps the row's items in proportion with the 22px rating word
   beside them. line-height:1 is inherited from the base rail-star rule,
   so the row box still cannot be pushed taller by a theme line-height.
   (0,4,0) beats that rule's (0,3,0).
   Deliberately NOT restated: the empty star's colour/opacity and the
   filled star's colour. Those were measured for contrast against the
   rail background (2.14, A4) and that background is unchanged here, so
   the measurements still hold; re-declaring them would fork them. */
.ksm-style-2.ksm-list-style-2 .ksm-style2-rail--horizontal .ksm-star {
  font-size: 22px;
}

/* Below 768px the band stacks. This also has to release the rail's
   mobile height:169px / overflow:hidden (the fixed-height guard added
   for the slider's mobile panel, where the rail is a fixed four-line
   column above the cards): a stacked band's height depends on how the
   four items wrap at the visitor's width, so a hard 169px would either
   leave a gap or clip the source line outright. Nothing about that guard
   is weakened for the slider -- this override reaches only elements
   carrying .ksm-list-style-2 and the --horizontal modifier.
   (0,3,0) against the mobile rail rule's (0,2,0): specificity, not order. */
@media (max-width: 768px) {
  .ksm-style-2.ksm-list-style-2 .ksm-style2-rail--horizontal {
    flex-direction: column;
    align-items: center;
    height: auto;
    overflow: visible;
    gap: 6px;
    padding: 16px 18px;
    border-radius: 12px;
  }
}

/* --- "View More Reviews", in the style-2 palette.
   The shared .ksm-load-more-btn rule above paints a #2196F3 Material
   blue that belongs to style 1 and matches nothing in style 2. Repainted
   here from the SAME properties the rail uses, with the SAME fallback
   chain (var(--ksm-rc-rail-bg, var(--primary, #0073aa))), so the button
   agrees with the band above it on every site without a second colour
   decision being taken anywhere.
   --ksm-rc-band-text is offered first for the label because a site that
   has set a band colour has already had a contrast-checked text colour
   computed for it by get_band_button_text_colour() in
   class-renderer.php; --ksm-rc-rail-text is the same helper's output for
   the rail colour and is the correct second choice; #fff is the last
   resort, matching the rail's own fallback.
   Hover darkens with the rail's own inset-scrim technique rather than a
   second hardcoded blue -- an inset rgba black works over whatever the
   custom property resolves to, which a fixed hex cannot. The base rule's
   :hover background-color still applies underneath, so the scrim is
   layered over that; both resolve to the same visual because this rule's
   own background-color wins on specificity in both states.
   (0,3,0) / (0,3,1) against the base rules' (0,1,0) / (0,1,1). */
.ksm-style-2.ksm-list-style-2 .ksm-load-more-container {
  margin-top: 24px;
}

.ksm-style-2.ksm-list-style-2 .ksm-load-more-btn {
  background-color: var(--ksm-rc-rail-bg, var(--primary, #0073aa));
  color: var(--ksm-rc-band-text, var(--ksm-rc-rail-text, #fff));
  border-radius: 8px; /* matches the card and band radius, not style 1's 4px */
  box-shadow: inset 0 0 0 999px transparent;
  transition: box-shadow 0.2s ease;
}

.ksm-style-2.ksm-list-style-2 .ksm-load-more-btn:hover,
.ksm-style-2.ksm-list-style-2 .ksm-load-more-btn:focus-visible {
  background-color: var(--ksm-rc-rail-bg, var(--primary, #0073aa));
  box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.18);
}

/* Reduced motion: the only transition this block introduces is the
   button's hover scrim. Removed rather than shortened, matching how the
   rest of this file treats prefers-reduced-motion (the track's
   scroll-behavior and transform transitions). The colour change itself
   still happens -- only the animation between the two states is
   dropped, so no state becomes unreachable. */
@media (prefers-reduced-motion: reduce) {
  .ksm-style-2.ksm-list-style-2 .ksm-load-more-btn {
    transition: none;
  }
}

/* ===========================================================================
   2.26 -- PARTIAL (FRACTIONAL) STAR, AVERAGE RATING ROW ONLY
   ===========================================================================
   Appended, never edited: nothing above this comment is touched by 2.26.

   WHAT MATCHES THIS. Every selector below carries .ksm-star--part, and
   class-renderer.php emits that class in exactly ONE place -- the partial
   branch of render_average_stars(), which only the four AGGREGATE rows call.
   An individual review card's stars are a whole number by definition, run
   through their own loops, and can never carry the class, so the
   background-clip:text rules here are structurally incapable of reaching a
   whole star or a card star. There is no bare .ksm-star selector anywhere in
   this block.

   HOW IT PAINTS. A 90deg gradient hard-stops from the ON colour to the OFF
   colour at --ksm-rc-star-fill, and background-clip:text clips that gradient
   to the glyph, so the left part of the star is orange and the rest is the
   row's empty colour. The stop is a hard one (both colours share the same
   position), so there is no gradient smear across the boundary.

   THE 50% FALLBACK IS THE SHIPPING VALUE, NOT A DEFAULT NOBODY USES. The
   renderer emits <span class="ksm-star ksm-star--part">, with NO style
   attribute, so --ksm-rc-star-fill is never set and every half star resolves
   through the var() fallback below to exactly 50%. Changing that 50% changes
   every half star on every site. It is deliberately the only value: a fill is
   a proportion of the SPAN's box width, and a star's ink is not spread evenly
   across its box, so a geometric fill and the ink the eye sees only agree at
   the symmetric centre. Measured off the real glyph mask: a 20% fill paints
   2.6% of the ink and a 70% fill paints 89.7%, while 50% paints 50%. The full
   measured curve and the history are in render_average_stars() in
   class-renderer.php -- read that before proposing proportional fills again.

   The custom property is kept rather than hard-coding 50% into the gradient so
   the mechanism stays available if Shane ever revisits the ruling.
   =========================================================================== */
.ksm-star.ksm-star--part {
  background: linear-gradient(90deg,
    var(--ksm-rc-star-on, #FFA500) var(--ksm-rc-star-fill, 50%),
    var(--ksm-rc-star-off, #ddd) var(--ksm-rc-star-fill, 50%));
  -webkit-background-clip: text;
  background-clip: text;
  /* Fallback colour, NOT the painted one. An engine with no
     background-clip:text never reaches the @supports block below, so it keeps
     this value and renders an ordinary solid star in the ON colour -- the
     degradation the 2.26 brief accepts. Setting color:transparent
     unconditionally here would instead leave such an engine with an INVISIBLE
     star, because the transparency and the clip must stand or fall together. */
  color: var(--ksm-rc-star-on, #FFA500);
}

/* The glyph is only made transparent where the clip actually works, so the
   fallback above stays reachable. -webkit-text-fill-color is what genuinely
   hides the glyph: it is unset everywhere else in this file, so it applies to
   the partial star whatever wins the `color` cascade around it.
   Two selectors, deliberately: specificity is computed per selector in a
   list, so the second one carries (0,4,0) and beats the frozen rail rule
   below without needing a separate block. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .ksm-star.ksm-star--part,
  .ksm-style-2 .ksm-style2-rail .ksm-star.ksm-star--part {
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}

/* STYLE 2 RAIL AND BAND. One selector covers both: the list band's element is
   <div class="ksm-style2-rail ksm-style2-rail--horizontal"> inside
   .ksm-style-2.ksm-list-style-2, so it matches .ksm-style-2 .ksm-style2-rail
   just as the slider's rail does. Verified in the rendered markup, not assumed.

   THE :not(.f) INTERACTION, worked out rather than assumed. The frozen rule
   `.ksm-style-2 .ksm-style2-rail .ksm-star:not(.f)` (0,3,0) MATCHES the
   partial star, because the partial star deliberately carries no .f. Left
   alone it would hand the partial star opacity:0.75, which would dim the
   ORANGE half as well as the empty half -- the one thing the brief forbids,
   since opacity cannot be applied to half an element. This rule is (0,4,0),
   so it beats that rule on both declarations and restores opacity to 1. The
   frozen rule itself is NOT edited and still computes exactly what it did
   before for every hollow star, none of which can carry .ksm-star--part.

   The 0.75 weight is not discarded, it MOVES: it goes into the OFF colour as
   a real alpha, so the unfilled half of the partial star composites to the
   same pixel value as the hollow star sitting next to it, while the filled
   half stays fully opaque #FFA500. Measured on the default rail: 75% white
   over #0073aa gives #BFDCEA at 3.63:1 -- the same figure the frozen rule's
   own comment records, reproduced independently here. */
.ksm-style-2 .ksm-style2-rail .ksm-star.ksm-star--part {
  /* Flat fallback for engines without color-mix(); the @supports block below
     replaces it with the rail-aware value wherever that is available. */
  --ksm-rc-star-off: rgba(255, 255, 255, 0.75);
  opacity: 1;
  color: var(--ksm-rc-star-on, #FFA500);
}

/* Rail-aware OFF colour: the same source of truth as the hollow star's own
   colour (--ksm-rc-rail-text), carrying the same 0.75 weight as an alpha
   rather than as opacity, so it stays correct on a dark rail AND on the light
   rail Branch A produces when the theme's brand colour is measurable.
   Kept in @supports rather than as a second custom-property declaration: a
   custom property holds an unparsed token stream, so an engine without
   color-mix() would accept the declaration and only fail later, at
   substitution time, which invalidates the whole `background` and would lose
   the gradient outright. @supports fails cleanly instead. */
@supports (color: color-mix(in srgb, #ffffff 75%, transparent)) {
  .ksm-style-2 .ksm-style2-rail .ksm-star.ksm-star--part {
    --ksm-rc-star-off: color-mix(in srgb, var(--ksm-rc-rail-text, #fff) 75%, transparent);
  }
}

/* Forced-colours (Windows high contrast): the user agent drops background
   images, so the clipped gradient is simply not painted, but it does not
   reset -webkit-text-fill-color -- which would leave a transparent glyph over
   no background at all, an invisible star. Handing the fill back to
   currentColor degrades it to a solid system-coloured star, the same
   degradation as the no-background-clip fallback above. Scoped to
   .ksm-star--part, so no other star is affected. */
@media (forced-colors: active) {
  .ksm-star.ksm-star--part {
    -webkit-text-fill-color: currentColor;
  }
}
