diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8374ceb..88fb9c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
# Peptide Starter Theme - Changelog
+## [2.2.1] - 2026-04-29 — RCT inline abbreviation tooltip
+
+### Added
+
+- `.pr-abbr` tap-target on the term "RCT" in the Established verdict card on
+ the verdict legend strip (homepage). Tapping/clicking opens a small inline
+ popover with the full definition: "Randomized Controlled Trial — the gold
+ standard for clinical evidence." Closes on outside click, Escape key, or
+ focus leaving the element.
+- `assets/js/abbr-tooltip.js` — new JS module (~130 lines) for popover open/
+ close, outside-click dismissal, keyboard accessibility (Enter/Space/Escape),
+ and ARIA attributes (`aria-expanded`, `aria-describedby`, `role="tooltip"`).
+ Enqueued on front page only (`is_front_page()`).
+- `peptide_starter_render_desc( string $text, array $abbrs ): string` helper in
+ `inc/verdict-helpers.php`. Builds safe HTML from `esc_html`/`esc_attr`
+ components; verdict taxonomy items carry an optional `abbrs` map.
+- `.pr-abbr` and `.pr-abbr-popover` CSS in `style.css` — dashed underline
+ affordance, fade-in animation, dark-mode-safe surface variables.
+
+### Fixed
+
+- `PEPTIDE_STARTER_VERSION` constant in `functions.php` was stale at
+ `2.0.0-alpha.3`; realigned to `2.2.1` to match `style.css` Version header.
+
## [2.1.8] - 2026-04-28 — Homepage verdict section spacing fix
### Fixed
diff --git a/assets/js/abbr-tooltip.js b/assets/js/abbr-tooltip.js
new file mode 100644
index 0000000..0284868
--- /dev/null
+++ b/assets/js/abbr-tooltip.js
@@ -0,0 +1,134 @@
+/**
+ * abbr-tooltip.js — Inline abbreviation popover for verdict legend screen.
+ *
+ * What: Opens a small popover anchored to any .pr-abbr element when tapped,
+ * clicked, or activated via keyboard. Closes on outside click or Escape.
+ * Who triggers: Enqueued by functions.php on is_front_page() requests only.
+ * Depends on: .pr-abbr elements in the DOM with data-def attributes; CSS in
+ * style.css (.pr-abbr-popover).
+ *
+ * @see inc/verdict-helpers.php — peptide_starter_render_desc() emits .pr-abbr elements
+ * @see style.css — .pr-abbr, .pr-abbr-popover visual styles
+ * @package peptide-starter
+ */
+
+( function () {
+ 'use strict';
+
+ var POPOVER_CLASS = 'pr-abbr-popover';
+ var ACTIVE_CLASS = 'pr-abbr--active';
+ var activePopover = null;
+ var activeAbbr = null;
+
+ /**
+ * Remove the currently open popover, if any.
+ *
+ * @return {void}
+ */
+ function closePopover() {
+ if ( activePopover ) {
+ activePopover.remove();
+ activePopover = null;
+ }
+ if ( activeAbbr ) {
+ activeAbbr.classList.remove( ACTIVE_CLASS );
+ activeAbbr.removeAttribute( 'aria-expanded' );
+ activeAbbr = null;
+ }
+ }
+
+ /**
+ * Create and position a popover anchored to the given abbr element.
+ *
+ * @param {HTMLElement} abbr - The .pr-abbr element that was activated.
+ * @return {void}
+ */
+ function openPopover( abbr ) {
+ closePopover();
+
+ var def = abbr.getAttribute( 'data-def' );
+ if ( ! def ) {
+ return;
+ }
+
+ var popover = document.createElement( 'div' );
+ popover.className = POPOVER_CLASS;
+ popover.textContent = def;
+ popover.setAttribute( 'role', 'tooltip' );
+ popover.setAttribute( 'id', POPOVER_CLASS + '-' + Date.now() );
+
+ document.body.appendChild( popover );
+
+ // Position below the abbr, clamped to viewport width.
+ var rect = abbr.getBoundingClientRect();
+ var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
+ var top = rect.bottom + scrollTop + 6;
+ var left = rect.left + scrollLeft;
+ var maxLeft = window.innerWidth - popover.offsetWidth - 16;
+
+ popover.style.top = top + 'px';
+ popover.style.left = Math.max( 8, Math.min( left, maxLeft ) ) + 'px';
+
+ abbr.classList.add( ACTIVE_CLASS );
+ abbr.setAttribute( 'aria-expanded', 'true' );
+ abbr.setAttribute( 'aria-describedby', popover.id );
+
+ activePopover = popover;
+ activeAbbr = abbr;
+ }
+
+ /**
+ * Toggle the popover for an abbr element (open if closed, close if open).
+ *
+ * @param {HTMLElement} abbr - The .pr-abbr element that was activated.
+ * @return {void}
+ */
+ function togglePopover( abbr ) {
+ if ( activeAbbr === abbr ) {
+ closePopover();
+ } else {
+ openPopover( abbr );
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Event listeners
+ // -------------------------------------------------------------------------
+
+ document.addEventListener( 'click', function ( e ) {
+ var abbr = e.target.closest( '.pr-abbr' );
+ if ( abbr ) {
+ e.stopPropagation();
+ togglePopover( abbr );
+ return;
+ }
+ // Click outside any abbr — close.
+ closePopover();
+ } );
+
+ document.addEventListener( 'keydown', function ( e ) {
+ if ( e.key === 'Escape' ) {
+ closePopover();
+ return;
+ }
+ if ( e.key === 'Enter' || e.key === ' ' ) {
+ var abbr = e.target.closest( '.pr-abbr' );
+ if ( abbr ) {
+ e.preventDefault();
+ togglePopover( abbr );
+ }
+ }
+ } );
+
+ // Close when focus leaves the popover+abbr pair entirely.
+ document.addEventListener( 'focusin', function ( e ) {
+ if (
+ activeAbbr &&
+ ! activeAbbr.contains( e.target ) &&
+ ( ! activePopover || ! activePopover.contains( e.target ) )
+ ) {
+ closePopover();
+ }
+ } );
+} )();
diff --git a/footer.php b/footer.php
index e5b2bbe..ee06f41 100644
--- a/footer.php
+++ b/footer.php
@@ -57,7 +57,7 @@
echo '
' . esc_html__( 'Our Method', 'peptide-starter' ) . '
';
echo '';
}
diff --git a/front-page.php b/front-page.php
index 79745bf..ffcfc31 100644
--- a/front-page.php
+++ b/front-page.php
@@ -101,7 +101,7 @@ $featured_monograph_ids = array( 211, 36, 177 );
-
+
diff --git a/functions.php b/functions.php
index 3c090fc..c594ecb 100644
--- a/functions.php
+++ b/functions.php
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
// Theme constants.
-define( 'PEPTIDE_STARTER_VERSION', '2.0.0-alpha.3' );
+define( 'PEPTIDE_STARTER_VERSION', '2.2.1' );
define( 'PEPTIDE_STARTER_DIR', get_template_directory() );
define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() );
@@ -88,6 +88,11 @@ function peptide_starter_scripts() {
wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true );
wp_enqueue_script( 'peptide-starter-settings-panel', PEPTIDE_STARTER_URI . '/assets/js/settings-panel.js', array(), PEPTIDE_STARTER_VERSION, true );
+ // Abbreviation popovers — only needed on the front page verdict legend strip.
+ if ( is_front_page() ) {
+ wp_enqueue_script( 'peptide-starter-abbr-tooltip', PEPTIDE_STARTER_URI . '/assets/js/abbr-tooltip.js', array(), PEPTIDE_STARTER_VERSION, true );
+ }
+
wp_localize_script(
'peptide-starter-theme',
'peptideStarterData',
diff --git a/inc/verdict-helpers.php b/inc/verdict-helpers.php
index 7013d5d..772c162 100644
--- a/inc/verdict-helpers.php
+++ b/inc/verdict-helpers.php
@@ -19,7 +19,7 @@
*
* Canonical order matches CMO brief §5.1 and the 5-state enum in verdict-meta.php.
*
- * @return array[] Each element: state (string), glyph (string), label (string), description (string).
+ * @return array[] Each element: state (string), glyph (string), label (string), description (string), abbrs (array, optional).
*/
function peptide_starter_get_verdict_taxonomy() {
return array(
@@ -28,6 +28,10 @@ function peptide_starter_get_verdict_taxonomy() {
'glyph' => '✓',
'label' => 'Established',
'description' => 'Strong RCT evidence; widely prescribed or validated.',
+ // abbrs: terms in description that should render as inline tap-targets.
+ 'abbrs' => array(
+ 'RCT' => 'Randomized Controlled Trial — the gold standard for clinical evidence. Participants are randomly assigned to treatment or control groups to measure real effect.',
+ ),
),
array(
'state' => 'promising',
@@ -88,3 +92,46 @@ function peptide_starter_get_verdict_config() {
),
);
}
+
+/**
+ * Renders a description string with optional inline abbreviation tap-targets.
+ *
+ * What: Builds safe HTML for a verdict card description, replacing known
+ * abbreviation terms with elements that trigger a tooltip.
+ * Who calls it: front-page.php verdict explainer loop.
+ * Depends on: Nothing — pure string transformation.
+ *
+ * The output is intentionally built from escaped components (not from raw $text)
+ * so we never expose unescaped user or data-layer content. Each substitution is:
+ * esc_html( $text ) → str_replace( literal_term, safe_abbr_html )
+ * This is safe because the abbr tag, class, and data-def are all hardcoded or
+ * escaped here, and the term key is a literal constant from our own data array.
+ *
+ * @param string $text Plain-text description string. Will be HTML-escaped.
+ * @param array $abbrs Associative array of term => definition. Keys are literal
+ * strings (e.g. 'RCT'). Values are plain-text definitions.
+ * @return string Safe HTML string suitable for echo without further escaping.
+ *
+ * @see inc/verdict-helpers.php — $abbrs data lives in peptide_starter_get_verdict_taxonomy()
+ * @see assets/js/abbr-tooltip.js — JS that handles tap/click to show the popover
+ * @see style.css — .pr-abbr and .pr-abbr-popover styles
+ */
+function peptide_starter_render_desc( string $text, array $abbrs ): string {
+ $html = esc_html( $text );
+
+ foreach ( $abbrs as $term => $definition ) {
+ $safe_term = esc_html( $term );
+ $safe_def = esc_attr( $definition );
+ $abbr_html = sprintf(
+ '%s',
+ $safe_def,
+ $safe_term,
+ $safe_def,
+ $safe_term
+ );
+ // Replace only the first occurrence — abbreviations should not stack.
+ $html = preg_replace( '/' . preg_quote( $safe_term, '/' ) . '/', $abbr_html, $html, 1 );
+ }
+
+ return $html;
+}
diff --git a/style.css b/style.css
index bce5998..f34cfec 100644
--- a/style.css
+++ b/style.css
@@ -4,7 +4,7 @@ Theme URI: https://github.com/peptiderepo/peptide-starter
Description: Premium, accessible WordPress theme for scientific peptide reference database
Author: Peptide Repo
Author URI: https://peptiderepo.com
-Version: 2.2.0
+Version: 2.2.1
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: peptide-starter
@@ -3410,3 +3410,55 @@ html[data-theme="dark"] .ps-verify-required {
grid-template-columns: repeat(5, 1fr);
}
}
+
+/* ==========================================================================
+ Abbreviation tooltip (.pr-abbr / .pr-abbr-popover)
+ Inline tap-targets for glossary terms in the verdict legend strip.
+ @see inc/verdict-helpers.php — peptide_starter_render_desc() emits .pr-abbr
+ @see assets/js/abbr-tooltip.js — JS popover logic
+ ========================================================================== */
+
+.pr-abbr {
+ border-bottom: 1px dashed currentColor;
+ cursor: help;
+ text-decoration: none;
+ white-space: nowrap;
+ /* Slightly muted so the dashed line reads as "interactive" not "broken link". */
+ opacity: 0.85;
+ transition: opacity 0.15s ease;
+}
+
+.pr-abbr:hover,
+.pr-abbr:focus,
+.pr-abbr--active {
+ opacity: 1;
+ outline: none;
+}
+
+.pr-abbr-popover {
+ position: absolute;
+ z-index: 9999;
+ max-width: min( 280px, calc( 100vw - 32px ) );
+ padding: 0.6rem 0.85rem;
+ border-radius: 6px;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ /* Use theme surface colours so dark-mode inherits correctly. */
+ background-color: var(--color-surface-elevated, #1e1e2e);
+ color: var(--color-text-primary, #e2e2f0);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
+ /* Subtle fade-in. */
+ animation: pr-popover-in 0.12s ease forwards;
+}
+
+@keyframes pr-popover-in {
+ from {
+ opacity: 0;
+ transform: translateY( -4px );
+ }
+ to {
+ opacity: 1;
+ transform: translateY( 0 );
+ }
+}
+