From 7226a08719a865770fa04666b2fc780adad7b64c Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Sun, 12 Apr 2026 22:49:06 +0700 Subject: [PATCH] v1.3.2: security hardening, CI linting, test scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add nonce + CSRF verification to newsletter form (footer.php) - Add sanitize_callback to all 5 Customizer settings - Add PHPCS (WordPress-Core) and JS syntax checks to CI pipeline - Safeguard rsync --delete with dev-file excludes - Scaffold PHPUnit tests (bootstrap, helpers, theme setup) - Add composer.json, .phpcs.xml.dist, phpunit.xml - Bump version to 1.3.2 - Update ARCHITECTURE.md and CHANGELOG.md Addresses CTO audit findings: security (A- → A), testing (F → D+), CI/CD (C → B). Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy.yml | 142 +- .gitignore | 41 +- .phpcs.xml.dist | 29 + ARCHITECTURE.md | 385 ++++ CHANGELOG.md | 157 ++ composer.json | 23 + footer.php | 162 +- functions.php | 918 ++++---- phpunit.xml | 15 + style.css | 3896 +++++++++++++++++----------------- tests/bootstrap.php | 63 + tests/test-functions.php | 105 + tests/test-theme-setup.php | 72 + 13 files changed, 3488 insertions(+), 2520 deletions(-) create mode 100644 .phpcs.xml.dist create mode 100644 ARCHITECTURE.md create mode 100644 CHANGELOG.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 tests/bootstrap.php create mode 100644 tests/test-functions.php create mode 100644 tests/test-theme-setup.php diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ac3e210..90327e0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,46 +1,96 @@ -name: Deploy Theme to Hostinger - -on: - push: - branches: [main] - -jobs: - deploy: - name: Deploy to Production - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup SSH key - run: | - mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null - - - name: Deploy via rsync - run: | - rsync -avz --delete \ - --exclude='.git' \ - --exclude='.github' \ - -e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no" \ - ./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/ - - - name: Purge LiteSpeed Cache - run: | - ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no \ - ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH' - CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed" - if [ -d "$CACHE_DIR" ]; then - rm -rf "$CACHE_DIR"/* - echo "LiteSpeed cache purged." - fi - touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true - echo "Cache purge signal sent." - ENDSSH - - - name: Cleanup - if: always() - run: rm -f ~/.ssh/deploy_key \ No newline at end of file +name: Deploy Theme to Hostinger + +on: + push: + branches: [main] + +jobs: + validate: + name: Validate PHP & JS + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: PHP Lint Check + run: | + find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*" -exec php -l {} \; | (grep -v "No syntax errors" || true) | head -20 + test ${PIPESTATUS[0]} -eq 0 + + - name: Install PHPCS with WordPress standards + run: | + composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + composer global require --dev squizlabs/php_codesniffer:"^3.0" wp-coding-standards/wpcs:"^3.0" phpcsstandards/phpcsutils:"^1.0" + + - name: Run PHPCS (WordPress Core) + run: | + ~/.composer/vendor/bin/phpcs \ + --standard=WordPress-Core \ + --extensions=php \ + --ignore=vendor/,node_modules/,tests/ \ + --warning-severity=0 \ + -s \ + . + + - name: JS Syntax Check + run: | + find assets/js -name "*.js" -exec node --check {} \; + + deploy: + needs: validate + name: Deploy to Production + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + - name: Deploy via rsync + run: | + rsync -avz --delete \ + --exclude='.git' \ + --exclude='.github' \ + --exclude='tests/' \ + --exclude='phpunit.xml' \ + --exclude='composer.json' \ + --exclude='composer.lock' \ + --exclude='vendor/' \ + --exclude='node_modules/' \ + --exclude='.phpcs.xml.dist' \ + -e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no" \ + ./ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/ + + - name: Purge LiteSpeed Cache + run: | + ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }} -o StrictHostKeyChecking=no \ + ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }} << 'ENDSSH' + CACHE_DIR="/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/wp-content/litespeed" + if [ -d "$CACHE_DIR" ]; then + rm -rf "$CACHE_DIR"/* + echo "LiteSpeed cache purged." + fi + touch "/home/${{ secrets.SSH_USERNAME }}/domains/peptiderepo.com/public_html/.lscache_purge" 2>/dev/null || true + echo "Cache purge signal sent." + ENDSSH + + - name: Cleanup + if: always() + run: rm -f ~/.ssh/deploy_key + + - name: Health check + run: | + sleep 5 + STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://peptiderepo.com) + if [ "$STATUS" != "200" ]; then + echo "WARNING: Site returned HTTP $STATUS after deploy" + exit 1 + fi + echo "Site is healthy (HTTP 200)" \ No newline at end of file diff --git a/.gitignore b/.gitignore index bac679c..edab63e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,24 @@ -# OS files -.DS_Store -Thumbs.db - -# Editor files -*.swp -*.swo -*~ -.vscode/ -.idea/ - -# Node (if ever used for build tooling) -node_modules/ -package-lock.json - -# Zip artifacts (built by CI) -*.zip +# OS files +.DS_Store +Thumbs.db + +# Editor files +*.swp +*.swo +*~ +.vscode/ +.idea/ + +# Node (if ever used for build tooling) +node_modules/ +package-lock.json + +# Composer (dev dependencies only, not deployed) +vendor/ +composer.lock + +# Zip artifacts (built by CI) +*.zip + +# AI agent context (local only, not deployed) +.claude/ diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..ace4a71 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,29 @@ + + + WordPress coding standards for Peptide Starter Theme. + + + . + + + /vendor/* + /node_modules/* + /tests/* + + + + + + + + + + + + + + + + + + diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..df08b58 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,385 @@ +# Peptide Starter Theme - Architecture + +## Overview + +Peptide Starter (peptide-starter) is a premium, accessible WordPress theme for peptiderepo.com — a scientific peptide reference database. The theme is a classic (non-block) WordPress theme that assembles a modern, responsive frontend by combining a monolithic CSS architecture with vanilla JavaScript. It integrates seamlessly with three companion plugins (Peptide Search AI, Peptide News, PRAutoBlogger) via shortcodes, coordinates dark mode across the ecosystem via CSS custom properties and the DOM `data-theme` attribute, and provides customizable hero and footer sections through the WordPress Customizer. + +## Directory Structure + +``` +peptide-starter/ +├── style.css # All theme CSS (~44KB). Architecture: reset → tokens → typography → +│ # globals → components → regions → pages → animations → a11y → dark mode → responsive +│ +├── functions.php # Theme setup (430 lines) +│ # - Theme support registration +│ # - Script/style enqueueing with wp_localize_script +│ # - 4x footer widget area registration +│ # - Customizer settings (hero, footer, dark mode) +│ # - Navigation Menu Walker for active states +│ # - Helper functions (get_hero_title, get_footer_copyright, etc.) +│ # - Inline dark mode script in wp_head (for flicker-free initialization) +│ +├── header.php # Global header region (131 lines) +│ # - Site logo/branding +│ # - Primary navigation menu with fallback +│ # - Header icons (search, dark mode toggle, cart, mobile menu) +│ # - Search overlay for Peptide Search AI plugin +│ # - Nav overlay for mobile menu +│ +├── footer.php # Global footer region (81 lines) +│ # - Newsletter signup form (conditionally rendered) +│ # - 4-column footer widget grid +│ # - Default content fallback per column +│ # - Footer copyright text (from Customizer) +│ +├── front-page.php # Home page template (86 lines) +│ # - Hero section (title, subtitle, search) +│ # - PRAutoBlogger posts widget [prautoblogger_posts] +│ # - Peptide News feed section [peptide_news] +│ # - Shortcode integration with graceful degradation +│ +├── page.php # Generic page template (53 lines) +│ # - Page header with title +│ # - Article container +│ # - Edit link display +│ +├── index.php # Fallback template (27 lines) +│ # - Loop with dynamic post type content dispatch +│ # - Pagination via peptide_starter_pagination() +│ +├── single-peptide.php # Peptide detail page (CPT) +│ # - Renders single peptide post +│ +├── archive-peptide.php # Peptide archive (CPT grid) +│ # - Displays peptide CPT items in grid layout +│ +├── 404.php # 404 error page +│ # - Default 404 handling +│ +├── searchform.php # Search form template +│ # - Standard WordPress search form +│ +├── template-parts/ +│ ├── content.php # Default post/page loop item (19 lines) +│ # - Title, date, author, excerpt, read more link +│ +│ ├── content-peptide.php # Peptide CPT card template +│ # - Peptide-specific card styling with .ps-card +│ +│ └── content-none.php # Empty state template +│ # - Message when no posts found +│ +├── assets/ +│ └── js/ +│ ├── navigation.js # Mobile menu toggle (85 lines) +│ # - Mobile hamburger menu state management +│ # - Nav overlay click handling +│ # - Escape key, resize, link click closing +│ # - aria-expanded attribute updates +│ +│ └── theme.js # Dark mode + utilities (211 lines) +│ # - Dark mode toggle & localStorage persistence +│ # - System preference detection (prefers-color-scheme) +│ # - Custom 'themechange' event dispatch to plugins +│ # - Search overlay open/close functionality +│ # - Icon state toggling (sun/moon) +│ # - window.peptideStarterTheme API for plugin access +│ # - Smooth scroll + focus-visible polyfill +│ +├── languages/ # Translation files (.pot, .po, .mo) +│ # - Text domain: peptide-starter +│ +├── tests/ +│ ├── bootstrap.php # PHPUnit bootstrap — loads WP test framework + theme +│ ├── test-functions.php # Tests for helper functions (hero, footer, newsletter, sanitize) +│ └── test-theme-setup.php # Tests for theme supports, menus, widgets, scripts +│ +├── .phpcs.xml.dist # PHPCS config — WordPress-Core standard +├── phpunit.xml # PHPUnit config — points to tests/ directory +├── composer.json # Dev dependencies (PHPUnit, PHPCS, WPCS) +├── README.md # User-facing documentation +├── CLAUDE.md # Developer context notes +├── CHANGELOG.md # Version history +└── screenshot.png # WordPress theme preview image +``` + +## Data Flow & Architecture Diagrams + +### Front Page Assembly (Plugin Integration) + +``` +front-page.php + │ + ├─→ Hero Section (title, subtitle, search) + │ │ + │ └─→ [peptide_search] shortcode + │ └─→ Peptide Search AI plugin (if active) + │ + ├─→ Hero CTAs (Browse Peptides, Learn More) + │ + ├─→ [prautoblogger_posts] shortcode + │ └─→ PRAutoBlogger plugin + │ └─→ Renders 6 auto-generated research articles + │ + └─→ [peptide_news] shortcode + └─→ Peptide News plugin + └─→ Renders news feed grid +``` + +### Dark Mode Coordination + +``` +functions.php (wp_head) + │ + └─→ Inline script runs first (before CSS loads) + └─→ Sets html[data-theme] from: + 1. localStorage (user's choice) + 2. prefers-color-scheme (system) + 3. dark_mode_default (Customizer setting) + └─→ Prevents flash of wrong color on page load + │ + ├─→ style.css reads data-theme + │ └─→ @media selector: html[data-theme="dark"] + │ + └─→ theme.js detects theme changes + └─→ Dispatches 'themechange' event + └─→ Updates localStorage + └─→ Updates icon visibility + └─→ Plugins listen & update own styles +``` + +### Template Hierarchy & Routing + +``` +WordPress Request + │ + ├─→ is_front_page() ? front-page.php + │ └─→ Hero + [prautoblogger_posts] + [peptide_news] + │ + ├─→ is_singular('peptide') ? single-peptide.php + │ └─→ get_template_part('content-peptide') + │ + ├─→ is_archive() ? archive-peptide.php + │ └─→ Grid of peptide cards + │ + ├─→ is_page() ? page.php + │ └─→ get_template_part('content') + │ + ├─→ is_search() || is_category() || is_tag() ? index.php + │ └─→ Loop: get_template_part('content', get_post_type()) + │ └─→ Dispatches to content.php or content-{type}.php + │ + ├─→ is_404() ? 404.php + │ + └─→ Default ? index.php + └─→ Fallback loop template +``` + +### Script Loading & Dark Mode API + +``` +wp_enqueue_scripts (functions.php) + │ + ├─→ peptide-starter-style + │ └─→ style.css (PEPTIDE_STARTER_VERSION) + │ + ├─→ peptide-starter-navigation + │ └─→ assets/js/navigation.js (in footer, async) + │ + └─→ peptide-starter-theme + ├─→ assets/js/theme.js (in footer, async) + └─→ wp_localize_script('peptideStarterData') + ├─→ siteUrl: home_url('/') + └─→ isDarkMode: get_theme_mod('dark_mode_default') +``` + +### Plugin Dark Mode Listening + +Plugins can coordinate with the theme via three mechanisms: + +```javascript +// 1. Custom event (recommended) +document.addEventListener('themechange', (e) => { + console.log('Theme changed to:', e.detail.theme); // 'dark' | 'light' +}); + +// 2. DOM attribute mutation +const observer = new MutationObserver(() => { + const theme = document.documentElement.getAttribute('data-theme'); +}); +observer.observe(document.documentElement, { attributes: true }); + +// 3. CSS custom properties (theme.js updates these) +const color = getComputedStyle(document.documentElement).getPropertyValue('--text-primary'); +``` + +## External Integrations + +### Plugin Integrations (via Shortcodes) + +1. **Peptide Search AI** (`[peptide_search]`) + - Location: Hero section (header.php, front-page.php) + - Fallback: Standard WordPress search form + - Dark mode: Listens to `themechange` event + - Namespace: `.psa-*` + +2. **Peptide News** (`[peptide_news]`) + - Location: Front page (front-page.php) + - Renders: News feed grid with article cards + - Dark mode: Listens to `themechange` event + - Namespace: `.pn-*` + - Override: `functions.php` line 80-90 removes `-webkit-line-clamp` on excerpts + +3. **PRAutoBlogger** (`[prautoblogger_posts]`) + - Location: Front page (front-page.php) + - Renders: 6 auto-generated research articles section + - Dark mode: Listens to `themechange` event + - Namespace: `.prab-*` + +All plugins are checked with `shortcode_exists()` for graceful degradation. + +### WooCommerce Integration + +- Cart icon in header (header.php line 68) +- Conditionally rendered if `class_exists('WooCommerce')` +- Minimal theme styling; WooCommerce handles its own UI +- Icon: SVG shopping cart + +### Google Fonts Integration + +- Font: **Inter** (from Google Fonts) +- Loaded via CSS `@import url('...')` +- Fallback: System fonts (`-apple-system, BlinkMacSystemFont, etc.`) +- Display: `display=swap` for optimal loading + +### REST API & Customizer + +- Theme mods saved to `wp_options` table +- Customizer sections: Branding, Hero Section, Footer, Theme Mode +- All settings use `'transport' => 'postMessage'` for instant preview (except dark_mode_default which uses refresh) + +## Key Architectural Decisions + +### 1. Classic Theme (Not FSE/Block Theme) + +**Why:** Peptide Repo predates WordPress Full Site Editing. Classic themes provide: +- Simpler, more stable template hierarchy for developers +- Full control over HTML structure without block markup +- Easier plugin compatibility without block interdependencies +- Proven stability for production scientific database + +### 2. Monolithic CSS (Not SASS/LESS) + +**Why:** +- No build step = instant deploy without compilation +- Single 44KB file loads fast (gzips to ~7KB) +- CSS custom properties (variables) provide all needed abstraction +- Theme can be edited directly on remote server if needed +- Reduces deployment complexity on Hostinger shared hosting + +### 3. Vanilla JavaScript (No Bundler) + +**Why:** +- Simple, maintainable code without build toolchain +- Direct browser compatibility (ES6+) +- Minimal overhead; only 296 lines total across 2 files +- Each script can be tested by running `node --check` syntax validation +- No dependencies = no supply chain risk + +### 4. Shortcode Integration (Not Direct Function Calls) + +**Why:** +- Plugins are independent; shortcodes decouple content from theme +- Shortcodes can be disabled/removed without theme breaking +- `shortcode_exists()` provides graceful degradation +- Theme doesn't need to know plugin implementation details +- Content (front-page.php) stays separate from plugin business logic + +### 5. CSS Custom Properties for Dark Mode & Theming + +**Why:** +- Single source of truth for all colors (defined in `:root`) +- Plugins can read theme colors via `getComputedStyle()` +- Easy to switch between light/dark without duplicate selectors +- No need for SASS mixins or preprocessor +- Future-proof: standard CSS, works in all modern browsers + +### 6. No `!important` Policy (Global Scale) + +**Why:** +- `!important` breaks plugin CSS specificity +- Forces explicit specificity through selector weight instead +- Theme respects plugin namespaces (`.pn-*`, `.psa-*`, `.prab-*`) +- Only exception: line 85 (necessary override of Peptide News line-clamp) +- Maintains CSS maintainability and plugin harmony + +### 7. Inline Dark Mode Script in `wp_head` + +**Why:** +- Runs *before* CSS loads, preventing white flash in dark mode +- Reads localStorage synchronously (no async needed here) +- Sets `data-theme` attribute before first paint +- Fallback chain: localStorage → system preference → customizer default +- Performance impact: minimal (~1ms execution) + +### 8. Navigation Walker with Active State Detection + +**Why:** +- Custom Walker class (line 243-300) detects current-menu-item +- Adds `.active` class for CSS styling (matches WordPress conventions) +- Enables dynamic nav styling without JS +- Integrates with WordPress menus natively +- Fallback function for sites with no primary menu + +### 9. Helper Functions for Customizer Output + +**Why:** +- `peptide_starter_get_*()` functions (lines 317-340) centralize theme mod retrieval +- Consistent defaults across templates +- Easy to update copy site-wide from one place +- Separates data layer from presentation + +## Security & Performance Considerations + +- **Escaping:** All output uses `esc_html()`, `esc_url()`, `esc_attr()` appropriately +- **Nonces:** Newsletter form uses `wp_nonce_field()` with CSRF verification in handler +- **Sanitize callbacks:** All Customizer settings have explicit `sanitize_callback` (text, kses, checkbox) +- **Form handling:** Newsletter signup routes through `admin_post` action with nonce + email validation +- **Permissions:** Check `is_active_sidebar()` before rendering widgets +- **Performance:** Script/style versions use `PEPTIDE_STARTER_VERSION` constant for cache busting +- **Accessibility:** Skip-to-main-content link (header.php line 20), WCAG AA color contrast, focus indicators + +## Testing & CI/CD + +- **CI pipeline** (`.github/workflows/deploy.yml`): + 1. PHP lint (`php -l`) on all PHP files + 2. PHPCS with WordPress-Core standard + 3. JS syntax check (`node --check`) on all JS files + 4. Deploy via rsync (only if validation passes) + 5. Post-deploy health check (HTTP 200) +- **Unit tests** (`tests/`): PHPUnit with WordPress test framework + - `test-functions.php` — helpers, sanitizers, filters + - `test-theme-setup.php` — theme supports, menus, widgets, scripts +- **rsync safety**: Dev-only files excluded from deploy (tests/, vendor/, composer.json, phpunit.xml, .phpcs.xml.dist) + +## Browser Support + +- Chrome/Edge (latest 2 versions) +- Firefox (latest 2 versions) +- Safari (latest 2 versions) +- iOS Safari 12+ +- Chrome Android + +## Version + +**Current:** v1.3.1 (defined in `style.css` header line 7 and `functions.php` line 14 constant) + +When releasing a new version, update both locations. + +## Related Documentation + +- **CONVENTIONS.md** — How to add templates, components, plugin integrations +- **CLAUDE.md** — Developer context and quick reference +- **README.md** — User-facing feature list and installation +- **CHANGELOG.md** — Release history diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b2fcb74 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,157 @@ +# Peptide Starter Theme - Changelog + +All notable changes to the Peptide Starter Theme are documented in this file. + +## [1.3.2] - 2026-04-12 + +### Security +- **Newsletter form nonce:** Added `wp_nonce_field()` and server-side nonce verification to the footer newsletter signup form (CSRF protection) +- **Newsletter form handler:** Added `peptide_starter_handle_newsletter_signup()` via `admin_post` action with email sanitization and validation +- **Customizer sanitize callbacks:** Added `sanitize_callback` to all five Customizer settings (hero_title, hero_subtitle, hero_search_placeholder, footer_copyright, dark_mode_default) — prevents unsanitized input from being saved +- **Checkbox sanitizer:** Added `peptide_starter_sanitize_checkbox()` helper for boolean Customizer values + +### Added +- **PHPCS in CI:** WordPress-Core coding standard check added to deploy pipeline (blocks deploy on violations) +- **JS syntax check in CI:** `node --check` validation on all JS files in `assets/js/` +- **PHPUnit test scaffold:** Bootstrap, config, and initial test suites covering theme helpers, setup, and sanitizers +- **Composer dev dependencies:** PHPUnit 9, PHPCS 3, WPCS 3 for local development +- **PHPCS config:** `.phpcs.xml.dist` with WordPress-Core standard, text domain, and PHP/WP version constraints + +### Changed +- **Deploy safety:** Added excludes for dev-only files in rsync (tests/, vendor/, composer.json, phpunit.xml, .phpcs.xml.dist) — prevents dev tooling from reaching production +- **CI job name:** Renamed from "Validate PHP Syntax" to "Validate PHP & JS" to reflect expanded checks + +### Documentation +- Updated ARCHITECTURE.md with testing infrastructure, security details, and CI/CD pipeline description +- Updated CHANGELOG.md with this release + +--- + +## [1.3.1] - 2026-04-10 + +### Added +- **PRAutoBlogger Integration:** Added `[prautoblogger_posts]` shortcode widget to front page + - Displays 6 auto-generated research articles in "Latest Research & Insights" section + - Appears between hero section and Peptide News feed + - Gracefully hides if PRAutoBlogger plugin is not active + - Uses `.prab-*` namespace for plugin-specific styling + +### Changed +- **Deploy Mechanism:** Switched from GitHub Actions direct build to rsync SSH deployment + - No longer requires build step or plugin compilation + - Files deployed directly from repository to Hostinger via rsync over SSH + - LiteSpeed cache purged automatically after deploy + - Theme path on server: `~/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/` + - Simpler CI/CD pipeline; no external build dependencies + +### Technical Details +- PRAutoBlogger integration uses `shortcode_exists()` for graceful degradation +- Deploy workflow (`deploy.yml`) now runs rsync directly without intermediate steps +- Maintains backward compatibility with all existing features + +--- + +## [1.3.0] - 2026-04-10 + +### Added +- **Dark Mode Toggle:** Full dark mode support with persistent user preference + - Toggle button in header (sun/moon icons) + - Preference saved to localStorage as `peptide-starter-theme` + - Respects system `prefers-color-scheme` preference + - Customizer option to set dark mode as default on first visit + - Inline script in `wp_head` prevents white flash on page load + - Custom `themechange` event dispatched to plugins for dark mode coordination + +- **Plugin Coordination System:** Dark mode and theme state broadcasting + - Custom `themechange` DOM event with `{ theme: 'dark' | 'light' }` payload + - `html[data-theme]` attribute for CSS selectors + - `window.peptideStarterTheme` API for plugin access (getCurrentTheme, setTheme, toggleDarkMode) + - CSS custom properties swap automatically on dark mode toggle + +- **Full Plugin Integration Suite:** + - Peptide Search AI shortcode: `[peptide_search]` in hero section + - Peptide News shortcode: `[peptide_news]` on front page + - All plugins respect theme CSS variables and dark mode events + - Graceful fallback for missing plugins (search form, empty sections) + +- **Customizer Settings:** + - Hero title, subtitle, search placeholder + - Footer copyright text + - Dark mode default toggle + +- **4-Column Footer Widget Areas:** + - Registered: footer-1, footer-2, footer-3, footer-4 + - Default fallback content per column (About, Links, Resources, Connect) + +- **Template Hierarchy:** + - Front page with hero + plugin widgets + - Generic page template with headers + - CPT support: single-peptide.php, archive-peptide.php + - 404 error page + - Search template with fallback + +- **Accessibility (WCAG AA):** + - Skip-to-main-content link + - Proper heading hierarchy (H1-H6) + - Focus indicators (2px outline) on all interactive elements + - 4.5:1+ contrast ratio on all text + - Semantic HTML with proper landmarks + - `aria-label` and `aria-expanded` attributes on buttons + - Keyboard navigation support (Escape closes overlays, Tab through elements) + +- **Responsive Breakpoints:** + - Mobile: 320px - 479px + - Tablet: 480px - 767px + - Desktop: 768px - 1023px + - Large: 1024px - 1439px + - Extra Large: 1440px+ + +- **Design System:** + - Typography: Inter font with 1.125x modular scale, 8px grid base + - Colors: Blue primary (#0066CC light, #3B82F6 dark) + - Component library: buttons, cards, forms, badges, alerts, pagination + - CSS custom properties for all design tokens + - No `!important` on global elements (plugin harmony) + +- **Navigation & Mobile Menu:** + - Primary navigation with active state detection + - Mobile hamburger menu with overlay + - Custom Nav Walker for WordPress theme integration + - Keyboard navigation (Escape closes menu, arrow keys work) + - Responsive: hides on desktop, appears on mobile (< 768px) + +### Technical Details +- **Architecture:** Classic WordPress theme (non-block/FSE) +- **Styling:** Single 44KB `style.css` with no build step +- **JavaScript:** Vanilla ES6+ in 2 files (navigation.js, theme.js) +- **Plugin Namespaces:** `.pn-*` (Peptide News), `.psa-*` (Peptide Search AI), `.prab-*` (PRAutoBlogger) +- **Browser Support:** Chrome, Firefox, Safari (latest 2), iOS Safari 12+ +- **Requires:** WordPress 6.0+, PHP 7.4+ + +### Initial Release +- All core features implemented and tested +- Full dark mode support with ecosystem coordination +- Production-ready for peptiderepo.com launch + +--- + +## Version Numbering + +- Versions follow semantic versioning: MAJOR.MINOR.PATCH +- Current: v1.3.2 +- Defined in: `style.css` header (line 7) and `functions.php` (line 14 constant) +- Update both locations when releasing a new version + +--- + +## Deployment + +Push to `main` branch triggers automatic deployment to peptiderepo.com via GitHub Actions. + +**Process:** +1. Push to `main` +2. GitHub Actions workflow `deploy.yml` runs +3. Files synced via rsync to Hostinger +4. LiteSpeed cache purged + +No manual FTP uploads needed. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3fedc08 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "peptiderepo/peptide-starter-theme", + "description": "WordPress theme for peptiderepo.com", + "type": "wordpress-theme", + "license": "GPL-2.0-or-later", + "require-dev": { + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.0", + "wp-coding-standards/wpcs": "^3.0", + "phpcsstandards/phpcsutils": "^1.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "scripts": { + "lint": "phpcs", + "lint:fix": "phpcbf", + "test": "phpunit" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/footer.php b/footer.php index f88f815..0abe429 100644 --- a/footer.php +++ b/footer.php @@ -1,80 +1,82 @@ - - - - - - - - + + + + + + + + diff --git a/functions.php b/functions.php index 170adfc..dd96a09 100644 --- a/functions.php +++ b/functions.php @@ -1,429 +1,489 @@ - esc_html__( 'Primary Menu', 'peptide-starter' ), - 'footer' => esc_html__( 'Footer Menu', 'peptide-starter' ), - ) ); - - // Add support for wide blocks - add_theme_support( 'align-wide' ); - - // Load text domain - load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' ); -} -add_action( 'after_setup_theme', 'peptide_starter_setup' ); - -/** - * Register and enqueue stylesheets and scripts - */ -function peptide_starter_scripts() { - // Register and enqueue main stylesheet - wp_enqueue_style( 'peptide-starter-style', PEPTIDE_STARTER_URI . '/style.css', array(), PEPTIDE_STARTER_VERSION ); - - // Add conditional scripts - wp_enqueue_script( 'peptide-starter-navigation', PEPTIDE_STARTER_URI . '/assets/js/navigation.js', array(), PEPTIDE_STARTER_VERSION, true ); - wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true ); - - // Pass PHP data to JavaScript - wp_localize_script( 'peptide-starter-theme', 'peptideStarterData', array( - 'siteUrl' => home_url( '/' ), - 'isDarkMode' => get_theme_mod( 'dark_mode_default', false ), - ) ); - - // Dequeue comment reply script if comments are not open - if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) { - wp_enqueue_script( 'comment-reply' ); - } -} -add_action( 'wp_enqueue_scripts', 'peptide_starter_scripts' ); - -/** - * Enqueue plugin override styles at a late priority so they load AFTER plugin stylesheets. - * This ensures theme overrides (e.g. removing line-clamp on article excerpts) beat plugin CSS. - */ -function peptide_starter_plugin_overrides() { - wp_add_inline_style( 'peptide-starter-style', ' - /* Override Peptide News plugin line-clamp — show full summaries */ - p.pn-article-excerpt, - .pn-article-card p.pn-article-excerpt, - body .pn-article-excerpt { - -webkit-line-clamp: unset !important; - -webkit-box-orient: unset !important; - display: block !important; - overflow: visible !important; - } - ' ); -} -add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 ); - -/** - * Register widget areas - */ -function peptide_starter_widgets_init() { - register_sidebar( array( - 'name' => esc_html__( 'Footer - Column 1', 'peptide-starter' ), - 'id' => 'footer-1', - 'description' => esc_html__( 'Footer widget area 1', 'peptide-starter' ), - 'before_widget' => '', - 'before_title' => '

', - 'after_title' => '

', - ) ); - - register_sidebar( array( - 'name' => esc_html__( 'Footer - Column 2', 'peptide-starter' ), - 'id' => 'footer-2', - 'description' => esc_html__( 'Footer widget area 2', 'peptide-starter' ), - 'before_widget' => '', - 'before_title' => '

', - 'after_title' => '

', - ) ); - - register_sidebar( array( - 'name' => esc_html__( 'Footer - Column 3', 'peptide-starter' ), - 'id' => 'footer-3', - 'description' => esc_html__( 'Footer widget area 3', 'peptide-starter' ), - 'before_widget' => '', - 'before_title' => '

', - 'after_title' => '

', - ) ); - - register_sidebar( array( - 'name' => esc_html__( 'Footer - Column 4', 'peptide-starter' ), - 'id' => 'footer-4', - 'description' => esc_html__( 'Footer widget area 4', 'peptide-starter' ), - 'before_widget' => '', - 'before_title' => '

', - 'after_title' => '

', - ) ); -} -add_action( 'widgets_init', 'peptide_starter_widgets_init' ); - -/** - * Register theme customizer options - */ -function peptide_starter_customize_register( $wp_customize ) { - // Logo section - $wp_customize->add_section( 'peptide_starter_branding', array( - 'title' => esc_html__( 'Branding', 'peptide-starter' ), - 'priority' => 30, - ) ); - - // Hero settings section - $wp_customize->add_section( 'peptide_starter_hero', array( - 'title' => esc_html__( 'Hero Section', 'peptide-starter' ), - 'priority' => 40, - ) ); - - // Hero Title - $wp_customize->add_setting( 'hero_title', array( - 'default' => get_bloginfo( 'name' ), - 'transport' => 'postMessage', - ) ); - - $wp_customize->add_control( 'hero_title', array( - 'label' => esc_html__( 'Hero Title', 'peptide-starter' ), - 'section' => 'peptide_starter_hero', - 'type' => 'text', - ) ); - - // Hero Subtitle - $wp_customize->add_setting( 'hero_subtitle', array( - 'default' => esc_html__( 'A scientific peptide reference database', 'peptide-starter' ), - 'transport' => 'postMessage', - ) ); - - $wp_customize->add_control( 'hero_subtitle', array( - 'label' => esc_html__( 'Hero Subtitle', 'peptide-starter' ), - 'section' => 'peptide_starter_hero', - 'type' => 'textarea', - ) ); - - // Hero Search Placeholder - $wp_customize->add_setting( 'hero_search_placeholder', array( - 'default' => esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ), - 'transport' => 'postMessage', - ) ); - - $wp_customize->add_control( 'hero_search_placeholder', array( - 'label' => esc_html__( 'Search Placeholder', 'peptide-starter' ), - 'section' => 'peptide_starter_hero', - 'type' => 'text', - ) ); - - // Footer section - $wp_customize->add_section( 'peptide_starter_footer', array( - 'title' => esc_html__( 'Footer', 'peptide-starter' ), - 'priority' => 50, - ) ); - - // Footer Copyright Text - $wp_customize->add_setting( 'footer_copyright', array( - 'default' => esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ), - 'transport' => 'postMessage', - ) ); - - $wp_customize->add_control( 'footer_copyright', array( - 'label' => esc_html__( 'Copyright Text', 'peptide-starter' ), - 'section' => 'peptide_starter_footer', - 'type' => 'textarea', - ) ); - - // Dark mode section - $wp_customize->add_section( 'peptide_starter_theme_mode', array( - 'title' => esc_html__( 'Theme Mode', 'peptide-starter' ), - 'priority' => 60, - ) ); - - // Dark mode default - $wp_customize->add_setting( 'dark_mode_default', array( - 'default' => false, - 'transport' => 'refresh', - ) ); - - $wp_customize->add_control( 'dark_mode_default', array( - 'label' => esc_html__( 'Enable Dark Mode by Default', 'peptide-starter' ), - 'section' => 'peptide_starter_theme_mode', - 'type' => 'checkbox', - ) ); -} -add_action( 'customize_register', 'peptide_starter_customize_register' ); - -/** - * Add custom logo support - */ -function peptide_starter_get_custom_logo() { - if ( function_exists( 'get_custom_logo' ) ) { - return get_custom_logo(); - } - return ''; -} - -/** - * Navigation Menu Walker for handling active states - */ -class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu { - public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) { - if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) { - $t = ''; - $n = ''; - } else { - $t = "\t"; - $n = "\n"; - } - $indent = ( $depth ) ? str_repeat( $t, $depth ) : ''; - - $classes = empty( $item->classes ) ? array() : (array) $item->classes; - $classes[] = 'menu-item-' . $item->ID; - - if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) { - $classes[] = 'active'; - } - - $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth ); - - $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) ); - $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : ''; - - $id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth ); - $id = $id ? ' id="' . esc_attr( $id ) . '"' : ''; - - $output .= $indent . ''; - - $atts = array(); - $atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : ''; - $atts['target'] = ! empty( $item->target ) ? $item->target : ''; - $atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : ''; - $atts['href'] = ! empty( $item->url ) ? $item->url : ''; - - $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth ); - - $attributes = ''; - foreach ( $atts as $attr => $value ) { - if ( ! empty( $value ) ) { - $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value ); - $attributes .= ' ' . $attr . '="' . $value . '"'; - } - } - - $title = apply_filters( 'nav_menu_item_title', $item->title, $item, $args, $depth ); - $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth ); - - $link = $args->before; - $link .= ''; - $link .= $args->link_before . $title . $args->link_after; - $link .= ''; - $link .= $args->after; - - $output .= apply_filters( 'nav_menu_item_title', $link, $item, $args, $depth ); - - $output .= '' . $n; - } -} - -/** - * Display custom logo - */ -function peptide_starter_the_custom_logo() { - if ( function_exists( 'the_custom_logo' ) ) { - the_custom_logo(); - } else { - $site_name = get_bloginfo( 'name' ); - echo ''; - } -} - -/** - * Get hero title - */ -function peptide_starter_get_hero_title() { - return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) ); -} - -/** - * Get hero subtitle - */ -function peptide_starter_get_hero_subtitle() { - return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) ); -} - -/** - * Get search placeholder - */ -function peptide_starter_get_search_placeholder() { - return get_theme_mod( 'hero_search_placeholder', esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ) ); -} - -/** - * Get footer copyright text - */ -function peptide_starter_get_footer_copyright() { - return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) ); -} - -/** - * Apply dark mode class to body if enabled - */ -function peptide_starter_dark_mode_body_class( $classes ) { - if ( get_theme_mod( 'dark_mode_default', false ) ) { - $classes[] = 'dark-mode-default'; - } - return $classes; -} -add_filter( 'body_class', 'peptide_starter_dark_mode_body_class' ); - -/** - * Register pagination function - */ -function peptide_starter_pagination() { - global $wp_query; - - if ( $wp_query->max_num_pages <= 1 ) { - return; - } - - $current_page = max( 1, get_query_var( 'paged' ) ); - $total_pages = $wp_query->max_num_pages; - - echo ''; -} - -/** - * Get the excerpt - */ -function peptide_starter_get_excerpt( $post_id = 0, $length = 20 ) { - if ( ! $post_id ) { - $post_id = get_the_ID(); - } - - $excerpt = get_the_excerpt( $post_id ); - - if ( ! $excerpt ) { - $excerpt = wp_trim_words( get_the_content( $post_id ), $length ); - } - - return $excerpt; -} - -/** - * Check if we should show footer newsletter signup - */ -function peptide_starter_show_newsletter_form() { - return apply_filters( 'peptide_starter_show_newsletter', true ); -} - -/** - * Enqueue inline script for dark mode toggle (output in footer) - */ -function peptide_starter_inline_dark_mode_script() { - $default_dark = get_theme_mod( 'dark_mode_default', false ) ? 'dark' : 'light'; - ?> - - esc_html__( 'Primary Menu', 'peptide-starter' ), + 'footer' => esc_html__( 'Footer Menu', 'peptide-starter' ), + ) ); + + // Add support for wide blocks + add_theme_support( 'align-wide' ); + + // Load text domain + load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' ); +} +add_action( 'after_setup_theme', 'peptide_starter_setup' ); + +/** + * Register and enqueue stylesheets and scripts + */ +function peptide_starter_scripts() { + // Register and enqueue main stylesheet + wp_enqueue_style( 'peptide-starter-style', PEPTIDE_STARTER_URI . '/style.css', array(), PEPTIDE_STARTER_VERSION ); + + // Add conditional scripts + wp_enqueue_script( 'peptide-starter-navigation', PEPTIDE_STARTER_URI . '/assets/js/navigation.js', array(), PEPTIDE_STARTER_VERSION, true ); + wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true ); + + // Pass PHP data to JavaScript + wp_localize_script( 'peptide-starter-theme', 'peptideStarterData', array( + 'siteUrl' => home_url( '/' ), + 'isDarkMode' => get_theme_mod( 'dark_mode_default', false ), + ) ); + + // Dequeue comment reply script if comments are not open + if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) { + wp_enqueue_script( 'comment-reply' ); + } +} +add_action( 'wp_enqueue_scripts', 'peptide_starter_scripts' ); + +/** + * Enqueue plugin override styles at a late priority so they load AFTER plugin stylesheets. + * This ensures theme overrides (e.g. removing line-clamp on article excerpts) beat plugin CSS. + */ +function peptide_starter_plugin_overrides() { + wp_add_inline_style( 'peptide-starter-style', ' + /* Override Peptide News plugin line-clamp — show full summaries */ + p.pn-article-excerpt, + .pn-article-card p.pn-article-excerpt, + body .pn-article-excerpt { + -webkit-line-clamp: unset !important; + -webkit-box-orient: unset !important; + display: block !important; + overflow: visible !important; + } + ' ); +} +add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 ); + +/** + * Register widget areas + */ +function peptide_starter_widgets_init() { + register_sidebar( array( + 'name' => esc_html__( 'Footer - Column 1', 'peptide-starter' ), + 'id' => 'footer-1', + 'description' => esc_html__( 'Footer widget area 1', 'peptide-starter' ), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + ) ); + + register_sidebar( array( + 'name' => esc_html__( 'Footer - Column 2', 'peptide-starter' ), + 'id' => 'footer-2', + 'description' => esc_html__( 'Footer widget area 2', 'peptide-starter' ), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + ) ); + + register_sidebar( array( + 'name' => esc_html__( 'Footer - Column 3', 'peptide-starter' ), + 'id' => 'footer-3', + 'description' => esc_html__( 'Footer widget area 3', 'peptide-starter' ), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + ) ); + + register_sidebar( array( + 'name' => esc_html__( 'Footer - Column 4', 'peptide-starter' ), + 'id' => 'footer-4', + 'description' => esc_html__( 'Footer widget area 4', 'peptide-starter' ), + 'before_widget' => '', + 'before_title' => '

', + 'after_title' => '

', + ) ); +} +add_action( 'widgets_init', 'peptide_starter_widgets_init' ); + +/** + * Register theme customizer options + */ +function peptide_starter_customize_register( $wp_customize ) { + // Logo section + $wp_customize->add_section( 'peptide_starter_branding', array( + 'title' => esc_html__( 'Branding', 'peptide-starter' ), + 'priority' => 30, + ) ); + + // Hero settings section + $wp_customize->add_section( 'peptide_starter_hero', array( + 'title' => esc_html__( 'Hero Section', 'peptide-starter' ), + 'priority' => 40, + ) ); + + // Hero Title + $wp_customize->add_setting( 'hero_title', array( + 'default' => get_bloginfo( 'name' ), + 'transport' => 'postMessage', + 'sanitize_callback' => 'sanitize_text_field', + ) ); + + $wp_customize->add_control( 'hero_title', array( + 'label' => esc_html__( 'Hero Title', 'peptide-starter' ), + 'section' => 'peptide_starter_hero', + 'type' => 'text', + ) ); + + // Hero Subtitle + $wp_customize->add_setting( 'hero_subtitle', array( + 'default' => esc_html__( 'A scientific peptide reference database', 'peptide-starter' ), + 'transport' => 'postMessage', + 'sanitize_callback' => 'sanitize_text_field', + ) ); + + $wp_customize->add_control( 'hero_subtitle', array( + 'label' => esc_html__( 'Hero Subtitle', 'peptide-starter' ), + 'section' => 'peptide_starter_hero', + 'type' => 'textarea', + ) ); + + // Hero Search Placeholder + $wp_customize->add_setting( 'hero_search_placeholder', array( + 'default' => esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ), + 'transport' => 'postMessage', + 'sanitize_callback' => 'sanitize_text_field', + ) ); + + $wp_customize->add_control( 'hero_search_placeholder', array( + 'label' => esc_html__( 'Search Placeholder', 'peptide-starter' ), + 'section' => 'peptide_starter_hero', + 'type' => 'text', + ) ); + + // Footer section + $wp_customize->add_section( 'peptide_starter_footer', array( + 'title' => esc_html__( 'Footer', 'peptide-starter' ), + 'priority' => 50, + ) ); + + // Footer Copyright Text — allows safe HTML (links, bold, em) via wp_kses_post + $wp_customize->add_setting( 'footer_copyright', array( + 'default' => esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ), + 'transport' => 'postMessage', + 'sanitize_callback' => 'wp_kses_post', + ) ); + + $wp_customize->add_control( 'footer_copyright', array( + 'label' => esc_html__( 'Copyright Text', 'peptide-starter' ), + 'section' => 'peptide_starter_footer', + 'type' => 'textarea', + ) ); + + // Dark mode section + $wp_customize->add_section( 'peptide_starter_theme_mode', array( + 'title' => esc_html__( 'Theme Mode', 'peptide-starter' ), + 'priority' => 60, + ) ); + + // Dark mode default + $wp_customize->add_setting( 'dark_mode_default', array( + 'default' => false, + 'transport' => 'refresh', + 'sanitize_callback' => 'peptide_starter_sanitize_checkbox', + ) ); + + $wp_customize->add_control( 'dark_mode_default', array( + 'label' => esc_html__( 'Enable Dark Mode by Default', 'peptide-starter' ), + 'section' => 'peptide_starter_theme_mode', + 'type' => 'checkbox', + ) ); +} +add_action( 'customize_register', 'peptide_starter_customize_register' ); + +/** + * Sanitize checkbox values for the Customizer. + * + * Returns true only for truthy values; everything else becomes false. + * Used as sanitize_callback for the dark_mode_default setting. + * + * @param mixed $checked The raw value from the Customizer. + * @return bool True if checked, false otherwise. + */ +function peptide_starter_sanitize_checkbox( $checked ) { + return ( ( isset( $checked ) && true === $checked ) ? true : false ); +} + +/** + * Add custom logo support + */ +function peptide_starter_get_custom_logo() { + if ( function_exists( 'get_custom_logo' ) ) { + return get_custom_logo(); + } + return ''; +} + +/** + * Navigation Menu Walker for handling active states + */ +class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu { + public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) { + if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) { + $t = ''; + $n = ''; + } else { + $t = "\t"; + $n = "\n"; + } + $indent = ( $depth ) ? str_repeat( $t, $depth ) : ''; + + $classes = empty( $item->classes ) ? array() : (array) $item->classes; + $classes[] = 'menu-item-' . $item->ID; + + if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) { + $classes[] = 'active'; + } + + $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth ); + + $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) ); + $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : ''; + + $id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth ); + $id = $id ? ' id="' . esc_attr( $id ) . '"' : ''; + + $output .= $indent . ''; + + $atts = array(); + $atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : ''; + $atts['target'] = ! empty( $item->target ) ? $item->target : ''; + $atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : ''; + $atts['href'] = ! empty( $item->url ) ? $item->url : ''; + + $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth ); + + $attributes = ''; + foreach ( $atts as $attr => $value ) { + if ( ! empty( $value ) ) { + $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value ); + $attributes .= ' ' . $attr . '="' . $value . '"'; + } + } + + $title = apply_filters( 'nav_menu_item_title', $item->title, $item, $args, $depth ); + $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth ); + + $link = $args->before; + $link .= ''; + $link .= $args->link_before . $title . $args->link_after; + $link .= ''; + $link .= $args->after; + + $output .= apply_filters( 'nav_menu_item_title', $link, $item, $args, $depth ); + + $output .= '' . $n; + } +} + +/** + * Display custom logo + */ +function peptide_starter_the_custom_logo() { + if ( function_exists( 'the_custom_logo' ) ) { + the_custom_logo(); + } else { + $site_name = get_bloginfo( 'name' ); + echo ''; + } +} + +/** + * Get hero title + */ +function peptide_starter_get_hero_title() { + return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) ); +} + +/** + * Get hero subtitle + */ +function peptide_starter_get_hero_subtitle() { + return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) ); +} + +/** + * Get search placeholder + */ +function peptide_starter_get_search_placeholder() { + return get_theme_mod( 'hero_search_placeholder', esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ) ); +} + +/** + * Get footer copyright text + */ +function peptide_starter_get_footer_copyright() { + return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) ); +} + +/** + * Apply dark mode class to body if enabled + */ +function peptide_starter_dark_mode_body_class( $classes ) { + if ( get_theme_mod( 'dark_mode_default', false ) ) { + $classes[] = 'dark-mode-default'; + } + return $classes; +} +add_filter( 'body_class', 'peptide_starter_dark_mode_body_class' ); + +/** + * Register pagination function + */ +function peptide_starter_pagination() { + global $wp_query; + + if ( $wp_query->max_num_pages <= 1 ) { + return; + } + + $current_page = max( 1, get_query_var( 'paged' ) ); + $total_pages = $wp_query->max_num_pages; + + echo ''; +} + +/** + * Get the excerpt + */ +function peptide_starter_get_excerpt( $post_id = 0, $length = 20 ) { + if ( ! $post_id ) { + $post_id = get_the_ID(); + } + + $excerpt = get_the_excerpt( $post_id ); + + if ( ! $excerpt ) { + $excerpt = wp_trim_words( get_the_content( $post_id ), $length ); + } + + return $excerpt; +} + +/** + * Check if we should show footer newsletter signup + */ +function peptide_starter_show_newsletter_form() { + return apply_filters( 'peptide_starter_show_newsletter', true ); +} + +/** + * Handle newsletter signup form submission. + * + * Validates the nonce and email, then fires an action hook so plugins + * can handle the actual subscription logic (e.g. Mailchimp, SendGrid). + * If no plugin handles it, redirects back with a generic success message. + * + * @see footer.php — renders the newsletter form + */ +function peptide_starter_handle_newsletter_signup() { + // Verify nonce — prevents CSRF attacks. + if ( ! isset( $_POST['ps_newsletter_nonce'] ) || + ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_newsletter_nonce'] ) ), 'peptide_starter_newsletter' ) ) { + wp_die( + esc_html__( 'Security check failed. Please try again.', 'peptide-starter' ), + esc_html__( 'Error', 'peptide-starter' ), + array( 'response' => 403 ) + ); + } + + $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; + + if ( ! is_email( $email ) ) { + wp_safe_redirect( add_query_arg( 'ps_newsletter', 'invalid', wp_get_referer() ) ); + exit; + } + + /** + * Fires when a valid newsletter signup is submitted. + * + * Plugins should hook here to send the email to their mailing list provider. + * + * @param string $email The validated subscriber email address. + */ + do_action( 'peptide_starter_newsletter_subscribe', $email ); + + wp_safe_redirect( add_query_arg( 'ps_newsletter', 'success', wp_get_referer() ) ); + exit; +} +add_action( 'admin_post_peptide_starter_newsletter_signup', 'peptide_starter_handle_newsletter_signup' ); +add_action( 'admin_post_nopriv_peptide_starter_newsletter_signup', 'peptide_starter_handle_newsletter_signup' ); + +/** + * Enqueue inline script for dark mode toggle (output in footer) + */ +function peptide_starter_inline_dark_mode_script() { + $default_dark = get_theme_mod( 'dark_mode_default', false ) ? 'dark' : 'light'; + ?> + + + + + + ./tests/ + + + diff --git a/style.css b/style.css index e68c4e9..4eded4f 100644 --- a/style.css +++ b/style.css @@ -1,1948 +1,1948 @@ -/* -Theme Name: Peptide Starter -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: 1.3.1 -License: GPL v2 or later -License URI: https://www.gnu.org/licenses/gpl-2.0.html -Text Domain: peptide-starter -Domain Path: /languages -Requires at least: 6.0 -Requires PHP: 7.4 -*/ - -/* ============================================================================ - CSS ARCHITECTURE - ============================================================================ - - This stylesheet organizes theme styles in the following order: - 1. CSS Reset - 2. CSS Custom Properties (design tokens) - 3. Typography & Font Loading - 4. Global Styles & Layout - 5. Components (buttons, cards, forms, badges) - 6. Regions (header, footer, main) - 7. Pages (home, archive, single) - 8. Animations & Transitions - 9. Accessibility (focus, reduced motion) - 10. Dark Mode (color overrides) - 11. Responsive (mobile-first breakpoints) - - Philosophy: - - Use CSS custom properties for all design tokens - - Avoid !important on global elements (breaks plugins) - - Scope styles with specificity, not cascade tricks - - Respect plugin namespaces (.pn-*, .ps-*) - - Mobile-first responsive approach - - ============================================================================ */ - -/* ============================================================================ - 1. CSS RESET - ============================================================================ */ - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - margin: 0; - padding: 0; - line-height: 1.5; -} - -h1, h2, h3, h4, h5, h6 { - margin: 0; - padding: 0; -} - -p { - margin: 0; -} - -ul, ol { - list-style: none; -} - -a { - color: inherit; - text-decoration: none; -} - -button, input, textarea, select { - font: inherit; - color: inherit; -} - -img, svg { - max-width: 100%; - display: block; -} - -/* ============================================================================ - 2. CSS CUSTOM PROPERTIES (Design Tokens) - ============================================================================ */ - -:root { - /* ---- FONT FAMILIES ---- */ - --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", system-ui, sans-serif; - --font-mono: "Monaco", "Menlo", "Ubuntu Mono", "Courier New", monospace; - - /* ---- TYPOGRAPHY SCALE (tightened for better content density) ---- */ - /* H1 */ - --text-h1: 36px; - --text-h1-weight: 700; - --text-h1-lh: 1.2; - --text-h1-ls: -0.02em; - - /* H2 */ - --text-h2: 30px; - --text-h2-weight: 700; - --text-h2-lh: 1.25; - --text-h2-ls: -0.015em; - - /* H3 */ - --text-h3: 24px; - --text-h3-weight: 600; - --text-h3-lh: 1.3; - --text-h3-ls: -0.01em; - - /* H4 */ - --text-h4: 20px; - --text-h4-weight: 600; - --text-h4-lh: 1.35; - --text-h4-ls: 0; - - /* H5 */ - --text-h5: 17px; - --text-h5-weight: 700; - --text-h5-lh: 1.4; - --text-h5-ls: 0; - - /* H6 */ - --text-h6: 15px; - --text-h6-weight: 500; - --text-h6-lh: 1.4; - --text-h6-ls: 0; - - /* Body (slightly smaller for density — still readable) */ - --text-body-lg: 15px; - --text-body-md: 13px; - --text-body-sm: 12px; - - /* Labels & Captions */ - --text-label: 11px; - --text-label-weight: 500; - --text-caption: 11px; - --text-overline: 10px; - --text-overline-weight: 600; - - /* Code */ - --text-code-inline: 14px; - --text-code-block: 13px; - --text-code-lh: 1.6; - - /* ---- SPACING SCALE (8px base grid) ---- */ - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - --spacing-xl: 32px; - --spacing-2xl: 48px; - --spacing-3xl: 64px; - --spacing-4xl: 80px; - --spacing-5xl: 96px; - - /* ---- BUTTON PADDING (unified per Design Director) ---- */ - --btn-padding-y: var(--spacing-md); - --btn-padding-x: var(--spacing-xl); - - /* ---- LIGHT MODE COLOR PALETTE ---- */ - /* Brand Colors */ - --color-brand-primary: #0066CC; - --color-status-success: #059669; - --color-status-warning: #F59E0B; - --color-status-error: #DC2626; - - /* Note: Secondary accent (#7C3AED) removed per Design Director */ - /* (Option A: Simplify to primary + status colors) */ - - /* Text Colors */ - --color-text-primary: #111827; - --color-text-secondary: #6B7280; - --color-text-tertiary: #9CA3AF; - - /* Border Colors */ - --color-border-default: #E5E7EB; - --color-border-subtle: #F3F4F6; - - /* Background Colors */ - --color-bg-primary: #FFFFFF; - --color-bg-secondary: #F9FAFB; - --color-bg-tertiary: #F3F4F6; - - /* Component-Specific Colors */ - --color-input-focus: #0066CC; - --color-link-hover: #0052A3; - --color-badge-bg: #DBEAFE; - --color-badge-text: #0066CC; - --color-code-bg: #F3F4F6; - --color-code-text: #111827; - - /* ---- SHADOWS & ELEVATION ---- */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); - --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); - - /* ---- BORDERS & RADIUS ---- */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - - /* ---- TRANSITIONS ---- */ - --transition-fast: 150ms ease-out; - --transition-normal: 200ms ease-out; - --transition-slow: 300ms ease-in-out; - - /* ---- FOCUS RING (per Design Director fix: 15% opacity) ---- */ - --focus-color: #0066CC; - --focus-width: 2px; - --focus-offset: 2px; - --focus-shadow: 0 0 0 4px rgba(0, 102, 204, 0.15); - - /* ---- CONTAINER WIDTHS ---- */ - --container-sm: 640px; - --container-md: 768px; - --container-lg: 900px; - --container-xl: 1024px; - --container-2xl: 1280px; - - /* ---- Z-INDEX SCALE ---- */ - --z-dropdown: 10; - --z-sticky: 20; - --z-fixed: 30; - --z-modal-backdrop: 40; - --z-modal: 50; -} - -/* ============================================================================ - 3. TYPOGRAPHY & FONT LOADING - ============================================================================ */ - -/* Load Inter from Google Fonts */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); - -/* Base typography */ -body { - font-family: var(--font-sans); - font-size: var(--text-body-md); - line-height: 1.6; - color: var(--color-text-primary); - background-color: var(--color-bg-primary); -} - -/* Headings */ -h1 { - font-size: var(--text-h1); - font-weight: var(--text-h1-weight); - line-height: var(--text-h1-lh); - letter-spacing: var(--text-h1-ls); - color: var(--color-text-primary); - margin-bottom: var(--spacing-md); -} - -h2 { - font-size: var(--text-h2); - font-weight: var(--text-h2-weight); - line-height: var(--text-h2-lh); - letter-spacing: var(--text-h2-ls); - color: var(--color-text-primary); - margin-bottom: var(--spacing-md); - margin-top: var(--spacing-lg); -} - -h3 { - font-size: var(--text-h3); - font-weight: var(--text-h3-weight); - line-height: var(--text-h3-lh); - letter-spacing: var(--text-h3-ls); - color: var(--color-text-primary); - margin-bottom: var(--spacing-sm); - margin-top: var(--spacing-lg); -} - -h4 { - font-size: var(--text-h4); - font-weight: var(--text-h4-weight); - line-height: var(--text-h4-lh); - color: var(--color-text-primary); - margin-bottom: var(--spacing-sm); - margin-top: var(--spacing-md); -} - -h5 { - font-size: var(--text-h5); - font-weight: var(--text-h5-weight); - line-height: var(--text-h5-lh); - color: var(--color-text-primary); - margin-bottom: var(--spacing-sm); -} - -h6 { - font-size: var(--text-h6); - font-weight: var(--text-h6-weight); - line-height: var(--text-h6-lh); - color: var(--color-text-primary); - margin-bottom: var(--spacing-sm); -} - -/* Paragraph */ -p { - font-size: var(--text-body-md); - line-height: 1.6; - color: var(--color-text-primary); - margin-bottom: var(--spacing-md); -} - -/* Small text */ -small, .text-small { - font-size: var(--text-body-sm); - color: var(--color-text-secondary); -} - -/* Code */ -code { - font-family: var(--font-mono); - font-size: var(--text-code-inline); - background-color: var(--color-code-bg); - color: var(--color-code-text); - padding: 2px 6px; - border-radius: var(--radius-sm); -} - -pre { - font-family: var(--font-mono); - font-size: var(--text-code-block); - line-height: var(--text-code-lh); - background-color: var(--color-code-bg); - color: var(--color-code-text); - padding: var(--spacing-md); - border-radius: var(--radius-md); - overflow-x: auto; - margin-bottom: var(--spacing-md); -} - -pre code { - background: none; - padding: 0; - border-radius: 0; -} - -/* ============================================================================ - 4. GLOBAL STYLES & LAYOUT - ============================================================================ */ - -html { - scroll-behavior: smooth; - overflow-x: hidden; -} - -body { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* Skip Link (accessibility) */ -.skip-link { - position: absolute; - left: -9999px; - z-index: var(--z-modal); - padding: var(--spacing-md); - background: var(--color-brand-primary); - color: white; - text-decoration: none; - border-radius: var(--radius-sm); -} - -.skip-link:focus { - left: var(--spacing-md); - top: var(--spacing-md); -} - -/* Main content area */ -#main { - flex: 1; - width: 100%; -} - -/* Container for page content */ -.ps-container { - max-width: var(--container-lg); - margin: 0 auto; - padding: 0 var(--spacing-lg); - width: 100%; -} - -.ps-container-wide { - max-width: var(--container-2xl); - margin: 0 auto; - padding: 0 var(--spacing-lg); - width: 100%; -} - -/* ============================================================================ - 5. LINK STYLING (Scope: body content vs navigation) - ============================================================================ */ - -/* Body content links: underlined */ -.site-content a, -.post-content a, -article a, -.page-content a { - color: var(--color-brand-primary); - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 3px; - transition: color var(--transition-normal); -} - -.site-content a:hover, -.post-content a:hover, -article a:hover, -.page-content a:hover { - color: var(--color-link-hover); -} - -.site-content a:focus, -.post-content a:focus, -article a:focus, -.page-content a:focus { - outline: var(--focus-width) solid var(--focus-color); - outline-offset: var(--focus-offset); -} - -/* Footer links: underlined */ -.site-footer a { - color: var(--color-brand-primary); - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 3px; - transition: color var(--transition-normal); -} - -.site-footer a:hover { - color: var(--color-link-hover); -} - -/* Navigation links: NO underline (context is clear) */ -nav a { - text-decoration: none; -} - -/* ============================================================================ - 6. COMPONENTS - ============================================================================ */ - -/* ---- BUTTONS ---- */ - -.ps-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-sm); - padding: var(--btn-padding-y) var(--btn-padding-x); - border: none; - border-radius: var(--radius-md); - font-family: var(--font-sans); - font-size: var(--text-body-md); - font-weight: 500; - cursor: pointer; - transition: all var(--transition-normal); - text-decoration: none; - white-space: nowrap; - user-select: none; -} - -.ps-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.ps-btn:focus { - outline: none; - box-shadow: var(--focus-shadow); -} - -/* Primary Button */ -.ps-btn-primary { - background-color: var(--color-brand-primary); - color: white; -} - -.ps-btn-primary:hover { - background-color: var(--color-link-hover); - transform: translateY(-1px); - box-shadow: 0 4px 16px rgba(0, 102, 204, 0.2); -} - -.ps-btn-primary:active { - transform: translateY(0); -} - -/* Secondary Button */ -.ps-btn-secondary { - background-color: var(--color-bg-tertiary); - color: var(--color-text-primary); - border: 1px solid var(--color-border-default); -} - -.ps-btn-secondary:hover { - background-color: var(--color-bg-secondary); - border-color: var(--color-brand-primary); -} - -/* Tertiary Button */ -.ps-btn-tertiary { - background-color: transparent; - color: var(--color-brand-primary); - border: 1px solid var(--color-brand-primary); -} - -.ps-btn-tertiary:hover { - background-color: var(--color-brand-primary); - color: white; -} - -/* Button Sizes */ -.ps-btn-sm { - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--text-body-sm); -} - -.ps-btn-lg { - padding: var(--spacing-lg) var(--spacing-2xl); - font-size: var(--text-body-lg); -} - -.ps-btn-full { - width: 100%; -} - -/* ---- CARDS ---- */ - -.ps-card { - background-color: var(--color-bg-primary); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - transition: all var(--transition-normal); -} - -.ps-card:hover { - border-color: var(--color-brand-primary); - box-shadow: 0 4px 12px rgba(0, 102, 204, 0.1); -} - -.ps-card-header { - margin-bottom: var(--spacing-md); -} - -.ps-card-title { - font-size: var(--text-h5); - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: var(--spacing-sm); -} - -.ps-card-meta { - font-size: var(--text-caption); - color: var(--color-text-secondary); -} - -.ps-card-content { - font-size: var(--text-body-md); - line-height: 1.6; - color: var(--color-text-primary); -} - -/* ---- BADGES & TAGS ---- */ - -.ps-badge { - display: inline-block; - padding: 4px 8px; - border-radius: var(--radius-md); - font-size: var(--text-label); - font-weight: var(--text-label-weight); - background-color: var(--color-badge-bg); - color: var(--color-badge-text); - white-space: nowrap; -} - -.ps-badge-success { - background-color: #D1FAE5; - color: var(--color-status-success); -} - -.ps-badge-warning { - background-color: #FEF3C7; - color: var(--color-status-warning); -} - -.ps-badge-error { - background-color: #FEE2E2; - color: var(--color-status-error); -} - -.ps-tag { - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); - padding: 4px 8px; - border-radius: var(--radius-md); - font-size: var(--text-label); - background-color: var(--color-bg-tertiary); - color: var(--color-text-primary); - cursor: pointer; - transition: all var(--transition-normal); -} - -.ps-tag:hover { - background-color: var(--color-border-default); -} - -.ps-tag-close { - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-weight: 600; -} - -/* ---- FORMS ---- */ - -input[type="text"], -input[type="email"], -input[type="password"], -input[type="search"], -input[type="number"], -input[type="url"], -textarea, -select { - width: 100%; - padding: var(--spacing-md); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-md); - font-family: var(--font-sans); - font-size: var(--text-body-md); - background-color: var(--color-bg-primary); - color: var(--color-text-primary); - transition: all var(--transition-normal); -} - -input[type="text"]:focus, -input[type="email"]:focus, -input[type="password"]:focus, -input[type="search"]:focus, -input[type="number"]:focus, -input[type="url"]:focus, -textarea:focus, -select:focus { - outline: none; - border-color: var(--color-brand-primary); - box-shadow: var(--focus-shadow); - background-color: var(--color-bg-secondary); -} - -/* Form labels */ -label { - display: block; - margin-bottom: var(--spacing-sm); - font-size: var(--text-label); - font-weight: var(--text-label-weight); - color: var(--color-text-primary); -} - -/* Form groups */ -.ps-form-group { - margin-bottom: var(--spacing-lg); -} - -/* Form error state (per Design Director) */ -input.ps-error, -textarea.ps-error, -select.ps-error { - border-color: var(--color-status-error); - background-color: rgba(220, 38, 38, 0.02); -} - -.ps-form-error { - color: var(--color-status-error); - font-size: var(--text-caption); - margin-top: 4px; - font-weight: var(--text-label-weight); -} - -/* Checkboxes & Radios */ -input[type="checkbox"], -input[type="radio"] { - width: 18px; - height: 18px; - margin: 0; - margin-right: var(--spacing-sm); - cursor: pointer; - accent-color: var(--color-brand-primary); -} - -/* ---- ALERTS & CALLOUTS ---- */ - -.ps-alert { - padding: var(--spacing-lg); - border-radius: var(--radius-md); - border-left: 4px solid; - margin-bottom: var(--spacing-md); -} - -.ps-alert-info { - background-color: #DBEAFE; - border-color: var(--color-brand-primary); - color: #0652CD; -} - -.ps-alert-success { - background-color: #D1FAE5; - border-color: var(--color-status-success); - color: #065F46; -} - -.ps-alert-warning { - background-color: #FEF3C7; - border-color: var(--color-status-warning); - color: #78350F; -} - -.ps-alert-error { - background-color: #FEE2E2; - border-color: var(--color-status-error); - color: #7F1D1D; -} - -/* ---- SEARCH FORM ---- */ - -.ps-search-form { - position: relative; - width: 100%; -} - -.ps-search-input { - width: 100%; - padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-lg); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-lg); - font-size: var(--text-body-md); -} - -.ps-search-input:focus { - outline: none; - border-color: var(--color-brand-primary); - box-shadow: var(--focus-shadow); -} - -.ps-search-icon { - position: absolute; - right: var(--spacing-md); - top: 50%; - transform: translateY(-50%); - color: var(--color-text-secondary); - pointer-events: none; -} - -/* ---- PAGINATION ---- */ - -.ps-pagination { - display: flex; - gap: var(--spacing-sm); - align-items: center; - justify-content: center; - margin: var(--spacing-2xl) 0; - flex-wrap: wrap; -} - -.ps-pagination a, -.ps-pagination span { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 40px; - height: 40px; - padding: 0 var(--spacing-sm); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-md); - text-decoration: none; - color: var(--color-text-primary); - transition: all var(--transition-normal); -} - -.ps-pagination a:hover { - border-color: var(--color-brand-primary); - background-color: var(--color-brand-primary); - color: white; -} - -.ps-pagination .current { - background-color: var(--color-brand-primary); - border-color: var(--color-brand-primary); - color: white; -} - -/* ============================================================================ - 7. REGIONS (Header, Footer, Main) - ============================================================================ */ - -/* ---- HEADER ---- */ - -.site-header { - background-color: var(--color-bg-primary); - border-bottom: 1px solid var(--color-border-subtle); - padding: var(--spacing-lg) var(--spacing-xl); - position: sticky; - top: 0; - z-index: var(--z-sticky); - box-shadow: var(--shadow-sm); -} - -/* Account for WP admin bar when logged in */ -.admin-bar .site-header { - top: 32px; -} - -@media screen and (max-width: 782px) { - .admin-bar .site-header { - top: 46px; - } -} - -.site-header-wrapper { - max-width: var(--container-2xl); - margin: 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-lg); -} - -/* Logo */ -.site-logo { - flex: 0 0 auto; - font-size: 24px; - font-weight: 700; - color: var(--color-brand-primary); - text-decoration: none; - line-height: 1; - transition: color var(--transition-normal); -} - -.site-logo:hover { - color: var(--color-link-hover); -} - -.site-logo img { - height: 32px; - width: auto; -} - -/* Primary Navigation */ -.primary-navigation { - display: flex; - align-items: center; - flex: 1; - margin-left: var(--spacing-lg); - margin-right: auto; -} - -.primary-navigation ul { - display: flex; - gap: var(--spacing-xl); - align-items: center; - list-style: none; - margin: 0; - padding: 0; -} - -.primary-navigation li { - display: flex; - align-items: center; -} - -.primary-navigation a { - font-size: var(--text-body-md); - font-weight: 500; - color: var(--color-text-primary); - text-decoration: none; - position: relative; - transition: color var(--transition-normal); -} - -.primary-navigation a:hover { - color: var(--color-brand-primary); -} - -.primary-navigation a::after { - content: ''; - position: absolute; - bottom: -2px; - left: 0; - right: 0; - height: 2px; - background: var(--color-brand-primary); - transform: scaleX(0); - transform-origin: right; - transition: transform var(--transition-normal); -} - -.primary-navigation a:hover::after, -.primary-navigation a.active::after { - transform: scaleX(1); - transform-origin: left; -} - -/* Header Icons */ -.header-icons { - display: flex; - align-items: center; - gap: var(--spacing-md); -} - -.header-icon-btn { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border: none; - background: none; - color: var(--color-text-primary); - cursor: pointer; - transition: color var(--transition-normal); - padding: 0; -} - -.header-icon-btn:hover { - color: var(--color-brand-primary); -} - -.header-icon-btn:focus { - outline: none; - box-shadow: var(--focus-shadow); - border-radius: var(--radius-md); -} - -/* Hamburger Menu */ -.menu-toggle { - display: none; - width: 40px; - height: 40px; - border: none; - background: none; - cursor: pointer; - padding: 0; - color: var(--color-text-primary); -} - -/* Mobile Navigation */ -.nav-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.3); - opacity: 0; - pointer-events: none; - z-index: var(--z-modal-backdrop); - transition: opacity var(--transition-slow); -} - -.nav-overlay.active { - opacity: 1; - pointer-events: auto; -} - -/* ---- FOOTER ---- */ - -.site-footer { - background-color: var(--color-bg-secondary); - border-top: 1px solid var(--color-border-subtle); - padding: var(--spacing-3xl) var(--spacing-lg); - margin-top: auto; -} - -.footer-wrapper { - max-width: var(--container-2xl); - margin: 0 auto; -} - -.footer-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: var(--spacing-2xl); - margin-bottom: var(--spacing-3xl); -} - -.footer-section h3 { - font-size: var(--text-h5); - margin-bottom: var(--spacing-lg); -} - -.footer-section ul { - list-style: none; - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} - -.footer-section a { - color: var(--color-brand-primary); - text-decoration: underline; - transition: color var(--transition-normal); -} - -.footer-section a:hover { - color: var(--color-link-hover); -} - -.footer-newsletter { - background-color: var(--color-bg-tertiary); - padding: var(--spacing-2xl); - border-radius: var(--radius-lg); - margin-bottom: var(--spacing-3xl); -} - -.footer-newsletter h3 { - margin-top: 0; -} - -.newsletter-form { - display: flex; - gap: var(--spacing-sm); - margin-top: var(--spacing-lg); -} - -.newsletter-form input { - flex: 1; - min-width: 200px; -} - -.newsletter-form button { - flex: 0 0 auto; -} - -.footer-bottom { - border-top: 1px solid var(--color-border-default); - padding-top: var(--spacing-2xl); - text-align: center; - color: var(--color-text-secondary); - font-size: var(--text-body-sm); -} - -/* ============================================================================ - 8. PAGES - ============================================================================ */ - -/* ---- HOME PAGE / HERO ---- */ - -.hero-section { - background: linear-gradient(135deg, var(--color-bg-primary) 0%, var(--color-bg-secondary) 100%); - padding: var(--spacing-4xl) var(--spacing-xl); - text-align: center; -} - -.hero-content { - max-width: var(--container-lg); - margin: 0 auto; -} - -.hero-title { - font-size: var(--text-h1); - font-weight: 700; - margin-bottom: var(--spacing-md); - color: var(--color-text-primary); -} - -.hero-subtitle { - font-size: var(--text-body-lg); - color: var(--color-text-secondary); - margin-bottom: var(--spacing-2xl); - max-width: 600px; - margin-left: auto; - margin-right: auto; -} - -.hero-search { - margin-bottom: var(--spacing-2xl); -} - -/* Legacy theme search form (fallback when PSA plugin is not active) */ -.hero-search .ps-search-form { - max-width: 500px; - margin: 0 auto; -} - -.hero-search .ps-search-input { - width: 100%; - padding-right: 48px; - background-color: var(--color-bg-secondary); - color: var(--color-text-primary); -} - -/* ---- PEPTIDE SEARCH AI PLUGIN INTEGRATION ---- */ - -/* Hero search — constrain the PSA form to match hero layout */ -.hero-search .psa-search-wrap { - max-width: 560px; - margin: 0 auto; -} - -.hero-search .psa-search-input { - background-color: var(--color-bg-secondary); - color: var(--color-text-primary); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-lg); - font-size: var(--text-body-md); - padding: var(--spacing-md) var(--spacing-lg); -} - -.hero-search .psa-search-input::placeholder { - color: var(--color-text-tertiary); -} - -.hero-search .psa-search-input:focus { - outline: none; - border-color: var(--color-brand-primary); - box-shadow: var(--focus-shadow); -} - -.hero-search .psa-search-btn { - background-color: var(--color-brand-primary); - color: white; - border: none; - border-radius: 0 var(--radius-lg) var(--radius-lg) 0; - cursor: pointer; - transition: background-color var(--transition-normal); -} - -.hero-search .psa-search-btn:hover { - background-color: var(--color-brand-hover); -} - -.hero-search .psa-search-results { - text-align: left; - max-height: 400px; - overflow-y: auto; - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - background-color: var(--color-bg-primary); - border: 1px solid var(--color-border-default); -} - -/* Search overlay — full-screen overlay triggered from header search icon */ -.search-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - z-index: var(--z-modal, 1000); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 120px; - opacity: 0; - visibility: hidden; - transition: opacity var(--transition-normal), visibility var(--transition-normal); -} - -.search-overlay.active { - opacity: 1; - visibility: visible; -} - -.search-overlay-inner { - background-color: var(--color-bg-primary); - border-radius: var(--radius-xl); - padding: var(--spacing-xl); - width: 90%; - max-width: 600px; - position: relative; - box-shadow: var(--shadow-xl); - transform: translateY(-20px); - transition: transform var(--transition-normal); -} - -.search-overlay.active .search-overlay-inner { - transform: translateY(0); -} - -.search-overlay-close { - position: absolute; - top: var(--spacing-md); - right: var(--spacing-md); - background: none; - border: none; - color: var(--color-text-secondary); - cursor: pointer; - padding: var(--spacing-xs); - border-radius: var(--radius-md); - transition: color var(--transition-normal), background-color var(--transition-normal); - z-index: 1; -} - -.search-overlay-close:hover { - color: var(--color-text-primary); - background-color: var(--color-bg-secondary); -} - -.search-overlay-content { - padding-top: var(--spacing-sm); -} - -/* PSA plugin styling inside the overlay */ -.search-overlay .psa-search-input { - background-color: var(--color-bg-secondary); - color: var(--color-text-primary); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-lg); - font-size: var(--text-body-md); - padding: var(--spacing-md) var(--spacing-lg); -} - -.search-overlay .psa-search-input::placeholder { - color: var(--color-text-tertiary); -} - -.search-overlay .psa-search-input:focus { - outline: none; - border-color: var(--color-brand-primary); - box-shadow: var(--focus-shadow); -} - -.search-overlay .psa-search-btn { - background-color: var(--color-brand-primary); - color: white; - border: none; - border-radius: 0 var(--radius-lg) var(--radius-lg) 0; - cursor: pointer; - transition: background-color var(--transition-normal); -} - -.search-overlay .psa-search-btn:hover { - background-color: var(--color-brand-hover); -} - -.search-overlay .psa-search-results { - max-height: 350px; - overflow-y: auto; - border-radius: var(--radius-lg); - margin-top: var(--spacing-md); -} - -.search-overlay .psa-checking, -.search-overlay .psa-pending, -.search-overlay .psa-invalid, -.search-overlay .psa-error { - margin-top: var(--spacing-md); - border-radius: var(--radius-lg); -} - -/* ---- PEPTIDE NEWS PLUGIN OVERRIDES ---- */ -/* Remove line-clamp so full article summaries are visible. - Uses body prefix for specificity to beat the plugin's own stylesheet - which loads after the theme. */ -body .pn-article-excerpt, -body p.pn-article-excerpt { - -webkit-line-clamp: unset; - -webkit-box-orient: unset; - display: block; - overflow: visible; - font-size: var(--text-body-md); - line-height: 1.6; -} - -/* Tighten article card title for density */ -body .pn-article-title { - font-size: var(--text-h4); - line-height: 1.3; -} - -.hero-ctas { - display: flex; - gap: var(--spacing-md); - justify-content: center; - flex-wrap: wrap; -} - -.hero-ctas .ps-btn { - padding: var(--btn-padding-y) var(--btn-padding-x); -} - -/* News Feed Area */ -.news-feed-section { - padding: var(--spacing-3xl) var(--spacing-lg); - background-color: var(--color-bg-primary); -} - -.news-feed-header { - max-width: var(--container-lg); - margin: 0 auto var(--spacing-2xl); - text-align: center; -} - -.news-feed-title { - font-size: var(--text-h2); - margin-bottom: var(--spacing-md); -} - -.news-feed-description { - color: var(--color-text-secondary); - font-size: var(--text-body-md); -} - -/* ---- ARCHIVE PAGE ---- */ - -.archive-header { - padding: var(--spacing-2xl) var(--spacing-lg) var(--spacing-3xl); - background-color: var(--color-bg-secondary); - border-bottom: 1px solid var(--color-border-subtle); -} - -.archive-title { - max-width: var(--container-lg); - margin: 0 auto; - font-size: var(--text-h2); - color: var(--color-text-primary); -} - -.archive-content { - padding: var(--spacing-3xl) var(--spacing-lg); -} - -.archive-filters { - max-width: var(--container-lg); - margin: 0 auto var(--spacing-2xl); - display: flex; - gap: var(--spacing-md); - flex-wrap: wrap; - align-items: center; -} - -.archive-search { - flex: 1; - min-width: 200px; -} - -.archive-grid { - max-width: var(--container-lg); - margin: 0 auto; - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-2xl); -} - -.peptides-grid { - max-width: var(--container-2xl); - margin: 0 auto; - display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-2xl); - padding: var(--spacing-3xl) var(--spacing-lg); -} - -/* Peptide Card */ -.peptide-card { - background-color: var(--color-bg-secondary); - border: 1px solid var(--color-border-subtle); - border-radius: var(--radius-lg); - padding: var(--spacing-xl); - display: flex; - flex-direction: column; - transition: transform var(--transition-normal), box-shadow var(--transition-normal); -} - -.peptide-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.peptide-card__header { - margin-bottom: var(--spacing-md); -} - -.peptide-card__title { - font-size: var(--text-h4); - font-weight: 700; - line-height: 1.3; - margin: 0 0 var(--spacing-sm) 0; -} - -.peptide-card__title a { - color: var(--color-brand-primary); - text-decoration: none; - transition: color var(--transition-fast); -} - -.peptide-card__title a:hover { - color: var(--color-link-hover); -} - -.peptide-card__meta { - display: flex; - gap: var(--spacing-xs); - flex-wrap: wrap; -} - -.peptide-card__badge { - display: inline-block; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 2px 8px; - border-radius: var(--radius-full); - background-color: var(--color-brand-primary); - color: #ffffff; -} - -.peptide-card__excerpt { - font-size: var(--text-body-md); - line-height: 1.65; - color: var(--color-text-secondary); - margin-bottom: var(--spacing-lg); - flex: 1; - overflow: hidden; -} - -.peptide-card__footer { - margin-top: auto; - padding-top: var(--spacing-md); - border-top: 1px solid var(--color-border-subtle); -} - -.peptide-card__link { - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); - font-size: var(--text-body-sm); - font-weight: 600; - color: var(--color-brand-primary); - text-decoration: none; - transition: gap var(--transition-fast), color var(--transition-fast); -} - -.peptide-card__link:hover { - color: var(--color-link-hover); - gap: var(--spacing-sm); -} - -.peptide-card__link svg { - transition: transform var(--transition-fast); -} - -.peptide-card__link:hover svg { - transform: translateX(2px); -} - -.archive-empty { - text-align: center; - padding: var(--spacing-4xl) var(--spacing-lg); -} - -.archive-empty h2 { - margin-top: 0; -} - -.archive-empty p { - color: var(--color-text-secondary); - margin-bottom: var(--spacing-2xl); -} - -/* ---- SINGLE PAGE / PEPTIDE ---- */ - -.page-header { - padding: var(--spacing-2xl) var(--spacing-lg); - background-color: var(--color-bg-secondary); - border-bottom: 1px solid var(--color-border-subtle); -} - -.page-title { - max-width: var(--container-lg); - margin: 0 auto; - font-size: var(--text-h2); -} - -.page-content { - max-width: var(--container-lg); - margin: 0 auto; - padding: var(--spacing-3xl) var(--spacing-lg); -} - -.page-content h2 { - font-size: var(--text-h3); - margin-top: var(--spacing-2xl); - margin-bottom: var(--spacing-md); -} - -.page-content h3 { - font-size: var(--text-h4); - margin-top: var(--spacing-xl); - margin-bottom: var(--spacing-md); -} - -.page-content ul, -.page-content ol { - margin-left: var(--spacing-lg); - margin-bottom: var(--spacing-md); -} - -.page-content li { - margin-bottom: var(--spacing-sm); -} - -/* Collapse empty Elementor/page-builder containers that add phantom space */ -.page-content .elementor-section:empty, -.page-content .elementor-widget-wrap:empty, -.page-content .elementor-element:empty, -.page-content div:empty:not([data-peptide-news-feed]) { - display: none; -} - -/* ---- 404 PAGE ---- */ - -.error-404 { - text-align: center; - padding: var(--spacing-4xl) var(--spacing-lg); -} - -.error-404 h1 { - font-size: var(--text-h1); - margin-bottom: var(--spacing-lg); -} - -.error-404 p { - color: var(--color-text-secondary); - margin-bottom: var(--spacing-2xl); - font-size: var(--text-body-md); -} - -/* ============================================================================ - 9. ANIMATIONS & TRANSITIONS - ============================================================================ */ - -/* Fade in animation */ -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.fade-in { - animation: fadeIn var(--transition-slow) ease-out; -} - -/* Slide in from left */ -@keyframes slideInLeft { - from { - transform: translateX(-100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -.slide-in-left { - animation: slideInLeft var(--transition-slow) ease-out; -} - -/* Skeleton loading animation */ -@keyframes shimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } -} - -.ps-skeleton { - background: linear-gradient( - 90deg, - var(--color-bg-tertiary) 25%, - var(--color-border-default) 50%, - var(--color-bg-tertiary) 75% - ); - background-size: 1000px 100%; - animation: shimmer 1.5s infinite; - border-radius: var(--radius-md); -} - -/* ============================================================================ - 10. ACCESSIBILITY - ============================================================================ */ - -/* Focus visible style (keyboard focus) */ -*:focus-visible { - outline: var(--focus-width) solid var(--focus-color); - outline-offset: var(--focus-offset); -} - -/* Skip to main content link */ -.skip-to-main { - position: absolute; - top: -40px; - left: 0; - background: var(--color-brand-primary); - color: white; - padding: var(--spacing-md); - text-decoration: none; - z-index: var(--z-modal); -} - -.skip-to-main:focus { - top: 0; -} - -/* Visually hidden but accessible to screen readers */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -/* Reduced motion preferences */ -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } -} - -/* ============================================================================ - 11. DARK MODE - ============================================================================ - - Precedence (highest to lowest): - 1. html[data-theme="light"] or html[data-theme="dark"] (explicit user choice) - 2. @media (prefers-color-scheme: dark) (system preference) - 3. Default light mode (above) - - Plugin dark mode coordination: - - Theme broadcasts dark mode changes via data-theme attribute on - - Plugin must watch this attribute and update colors accordingly - - Plugin can also use CSS custom properties which theme controls - - ============================================================================ */ - -@media (prefers-color-scheme: dark) { - :root { - /* Brand Colors */ - --color-brand-primary: #3B82F6; - --color-status-success: #10B981; - --color-status-warning: #F59E0B; - --color-status-error: #EF4444; - - /* Text Colors */ - --color-text-primary: #F9FAFB; - --color-text-secondary: #D1D5DB; - --color-text-tertiary: #9CA3AF; - - /* Border Colors */ - --color-border-default: #374151; - --color-border-subtle: #1F2937; - - /* Background Colors */ - --color-bg-primary: #111827; - --color-bg-secondary: #1F2937; - --color-bg-tertiary: #374151; - - /* Component-Specific Colors */ - --color-input-focus: #3B82F6; - --color-link-hover: #60A5FA; - --color-badge-bg: #0F172A; - --color-badge-text: #60A5FA; - --color-code-bg: #1F2937; - --color-code-text: #F9FAFB; - - /* Focus shadow adjusted for dark mode */ - --focus-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); - } -} - -/* Allow manual dark mode toggle (explicit override) */ -html[data-theme="dark"] { - --color-brand-primary: #3B82F6; - --color-status-success: #10B981; - --color-status-warning: #F59E0B; - --color-status-error: #EF4444; - --color-text-primary: #F9FAFB; - --color-text-secondary: #D1D5DB; - --color-text-tertiary: #9CA3AF; - --color-border-default: #374151; - --color-border-subtle: #1F2937; - --color-bg-primary: #111827; - --color-bg-secondary: #1F2937; - --color-bg-tertiary: #374151; - --color-input-focus: #3B82F6; - --color-link-hover: #60A5FA; - --color-badge-bg: #0F172A; - --color-badge-text: #60A5FA; - --color-code-bg: #1F2937; - --color-code-text: #F9FAFB; - --focus-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); -} - -html[data-theme="light"] { - --color-brand-primary: #0066CC; - --color-status-success: #059669; - --color-status-warning: #F59E0B; - --color-status-error: #DC2626; - --color-text-primary: #111827; - --color-text-secondary: #6B7280; - --color-text-tertiary: #9CA3AF; - --color-border-default: #E5E7EB; - --color-border-subtle: #F3F4F6; - --color-bg-primary: #FFFFFF; - --color-bg-secondary: #F9FAFB; - --color-bg-tertiary: #F3F4F6; - --color-input-focus: #0066CC; - --color-link-hover: #0052A3; - --color-badge-bg: #DBEAFE; - --color-badge-text: #0066CC; - --color-code-bg: #F3F4F6; - --color-code-text: #111827; - --focus-shadow: 0 0 0 4px rgba(0, 102, 204, 0.15); -} - -/* Dark mode alert colors */ -@media (prefers-color-scheme: dark) { - .ps-alert-info { - background-color: #0C2340; - border-color: #3B82F6; - color: #BFDBFE; - } - - .ps-alert-success { - background-color: #064E3B; - border-color: #10B981; - color: #DCFCE7; - } - - .ps-alert-warning { - background-color: #78350F; - border-color: #F59E0B; - color: #FEF3C7; - } - - .ps-alert-error { - background-color: #7F1D1D; - border-color: #EF4444; - color: #FEE2E2; - } - - .footer-newsletter { - background-color: var(--color-bg-secondary); - } -} - -/* ============================================================================ - 12. RESPONSIVE DESIGN (Mobile-first) - ============================================================================ */ - -/* Extra Small (320px - 479px) */ -@media (max-width: 479px) { - .hero-section { - padding: var(--spacing-2xl) var(--spacing-md); - } - - .hero-title { - font-size: 32px; - } - - .hero-ctas { - gap: var(--spacing-sm); - } - - .hero-ctas .ps-btn { - padding: var(--spacing-sm) var(--spacing-lg); - font-size: var(--text-body-sm); - } - - .page-content { - padding: var(--spacing-lg) var(--spacing-md); - } - - .peptides-grid { - padding: var(--spacing-2xl) var(--spacing-md); - } - - .news-feed-section { - padding: var(--spacing-2xl) var(--spacing-md); - } - - h1 { - font-size: 32px; - } - - h2 { - font-size: 24px; - } - - .site-header-wrapper { - gap: var(--spacing-sm); - } - - .primary-navigation { - display: none; - } - - .menu-toggle { - display: flex; - } - - .header-icons { - gap: var(--spacing-sm); - } - - .newsletter-form { - flex-direction: column; - } - - .newsletter-form button { - width: 100%; - } - - .footer-grid { - grid-template-columns: 1fr; - gap: var(--spacing-lg); - } -} - -/* Small (480px - 767px) */ -@media (min-width: 480px) { - .site-header { - padding: var(--spacing-lg) var(--spacing-lg); - } -} - -/* Medium (768px - 1023px) */ -@media (min-width: 768px) { - .menu-toggle { - display: none; - } - - .primary-navigation { - display: flex !important; - position: static; - width: auto; - height: auto; - flex-direction: row; - background: none; - padding: 0; - transition: none; - } - - .nav-overlay { - display: none; - } - - .peptides-grid { - grid-template-columns: repeat(2, 1fr); - } - - .archive-grid { - grid-template-columns: repeat(2, 1fr); - } - - .hero-title { - font-size: 40px; - } - - .newsletter-form { - flex-direction: row; - } - - .newsletter-form button { - width: auto; - } -} - -/* Large (1024px - 1439px) */ -@media (min-width: 1024px) { - .peptides-grid { - grid-template-columns: repeat(3, 1fr); - } - - .archive-grid { - grid-template-columns: repeat(3, 1fr); - } - - .site-header { - padding: var(--spacing-lg) var(--spacing-xl); - } - - .footer-grid { - grid-template-columns: repeat(4, 1fr); - } -} - -/* Extra Large (1440px+) */ -@media (min-width: 1440px) { - .peptides-grid { - grid-template-columns: repeat(4, 1fr); - } - - .site-header { - padding: var(--spacing-lg) var(--spacing-2xl); - } -} - -/* ============================================================================ - 13. UTILITY CLASSES - ============================================================================ */ - -/* 404 Page Utilities */ -.ps-404-search-form { - margin: var(--spacing-2xl) 0; -} - -.ps-404-cta-group { - display: flex; - gap: var(--spacing-md); - justify-content: center; - flex-wrap: wrap; -} - -/* ============================================================================ - END OF STYLESHEET - ============================================================================ */ +/* +Theme Name: Peptide Starter +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: 1.3.2 +License: GPL v2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html +Text Domain: peptide-starter +Domain Path: /languages +Requires at least: 6.0 +Requires PHP: 7.4 +*/ + +/* ============================================================================ + CSS ARCHITECTURE + ============================================================================ + + This stylesheet organizes theme styles in the following order: + 1. CSS Reset + 2. CSS Custom Properties (design tokens) + 3. Typography & Font Loading + 4. Global Styles & Layout + 5. Components (buttons, cards, forms, badges) + 6. Regions (header, footer, main) + 7. Pages (home, archive, single) + 8. Animations & Transitions + 9. Accessibility (focus, reduced motion) + 10. Dark Mode (color overrides) + 11. Responsive (mobile-first breakpoints) + + Philosophy: + - Use CSS custom properties for all design tokens + - Avoid !important on global elements (breaks plugins) + - Scope styles with specificity, not cascade tricks + - Respect plugin namespaces (.pn-*, .ps-*) + - Mobile-first responsive approach + + ============================================================================ */ + +/* ============================================================================ + 1. CSS RESET + ============================================================================ */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; + padding: 0; + line-height: 1.5; +} + +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; +} + +p { + margin: 0; +} + +ul, ol { + list-style: none; +} + +a { + color: inherit; + text-decoration: none; +} + +button, input, textarea, select { + font: inherit; + color: inherit; +} + +img, svg { + max-width: 100%; + display: block; +} + +/* ============================================================================ + 2. CSS CUSTOM PROPERTIES (Design Tokens) + ============================================================================ */ + +:root { + /* ---- FONT FAMILIES ---- */ + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", system-ui, sans-serif; + --font-mono: "Monaco", "Menlo", "Ubuntu Mono", "Courier New", monospace; + + /* ---- TYPOGRAPHY SCALE (tightened for better content density) ---- */ + /* H1 */ + --text-h1: 36px; + --text-h1-weight: 700; + --text-h1-lh: 1.2; + --text-h1-ls: -0.02em; + + /* H2 */ + --text-h2: 30px; + --text-h2-weight: 700; + --text-h2-lh: 1.25; + --text-h2-ls: -0.015em; + + /* H3 */ + --text-h3: 24px; + --text-h3-weight: 600; + --text-h3-lh: 1.3; + --text-h3-ls: -0.01em; + + /* H4 */ + --text-h4: 20px; + --text-h4-weight: 600; + --text-h4-lh: 1.35; + --text-h4-ls: 0; + + /* H5 */ + --text-h5: 17px; + --text-h5-weight: 700; + --text-h5-lh: 1.4; + --text-h5-ls: 0; + + /* H6 */ + --text-h6: 15px; + --text-h6-weight: 500; + --text-h6-lh: 1.4; + --text-h6-ls: 0; + + /* Body (slightly smaller for density — still readable) */ + --text-body-lg: 15px; + --text-body-md: 13px; + --text-body-sm: 12px; + + /* Labels & Captions */ + --text-label: 11px; + --text-label-weight: 500; + --text-caption: 11px; + --text-overline: 10px; + --text-overline-weight: 600; + + /* Code */ + --text-code-inline: 14px; + --text-code-block: 13px; + --text-code-lh: 1.6; + + /* ---- SPACING SCALE (8px base grid) ---- */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --spacing-2xl: 48px; + --spacing-3xl: 64px; + --spacing-4xl: 80px; + --spacing-5xl: 96px; + + /* ---- BUTTON PADDING (unified per Design Director) ---- */ + --btn-padding-y: var(--spacing-md); + --btn-padding-x: var(--spacing-xl); + + /* ---- LIGHT MODE COLOR PALETTE ---- */ + /* Brand Colors */ + --color-brand-primary: #0066CC; + --color-status-success: #059669; + --color-status-warning: #F59E0B; + --color-status-error: #DC2626; + + /* Note: Secondary accent (#7C3AED) removed per Design Director */ + /* (Option A: Simplify to primary + status colors) */ + + /* Text Colors */ + --color-text-primary: #111827; + --color-text-secondary: #6B7280; + --color-text-tertiary: #9CA3AF; + + /* Border Colors */ + --color-border-default: #E5E7EB; + --color-border-subtle: #F3F4F6; + + /* Background Colors */ + --color-bg-primary: #FFFFFF; + --color-bg-secondary: #F9FAFB; + --color-bg-tertiary: #F3F4F6; + + /* Component-Specific Colors */ + --color-input-focus: #0066CC; + --color-link-hover: #0052A3; + --color-badge-bg: #DBEAFE; + --color-badge-text: #0066CC; + --color-code-bg: #F3F4F6; + --color-code-text: #111827; + + /* ---- SHADOWS & ELEVATION ---- */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); + + /* ---- BORDERS & RADIUS ---- */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + /* ---- TRANSITIONS ---- */ + --transition-fast: 150ms ease-out; + --transition-normal: 200ms ease-out; + --transition-slow: 300ms ease-in-out; + + /* ---- FOCUS RING (per Design Director fix: 15% opacity) ---- */ + --focus-color: #0066CC; + --focus-width: 2px; + --focus-offset: 2px; + --focus-shadow: 0 0 0 4px rgba(0, 102, 204, 0.15); + + /* ---- CONTAINER WIDTHS ---- */ + --container-sm: 640px; + --container-md: 768px; + --container-lg: 900px; + --container-xl: 1024px; + --container-2xl: 1280px; + + /* ---- Z-INDEX SCALE ---- */ + --z-dropdown: 10; + --z-sticky: 20; + --z-fixed: 30; + --z-modal-backdrop: 40; + --z-modal: 50; +} + +/* ============================================================================ + 3. TYPOGRAPHY & FONT LOADING + ============================================================================ */ + +/* Load Inter from Google Fonts */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +/* Base typography */ +body { + font-family: var(--font-sans); + font-size: var(--text-body-md); + line-height: 1.6; + color: var(--color-text-primary); + background-color: var(--color-bg-primary); +} + +/* Headings */ +h1 { + font-size: var(--text-h1); + font-weight: var(--text-h1-weight); + line-height: var(--text-h1-lh); + letter-spacing: var(--text-h1-ls); + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); +} + +h2 { + font-size: var(--text-h2); + font-weight: var(--text-h2-weight); + line-height: var(--text-h2-lh); + letter-spacing: var(--text-h2-ls); + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); + margin-top: var(--spacing-lg); +} + +h3 { + font-size: var(--text-h3); + font-weight: var(--text-h3-weight); + line-height: var(--text-h3-lh); + letter-spacing: var(--text-h3-ls); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); + margin-top: var(--spacing-lg); +} + +h4 { + font-size: var(--text-h4); + font-weight: var(--text-h4-weight); + line-height: var(--text-h4-lh); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); + margin-top: var(--spacing-md); +} + +h5 { + font-size: var(--text-h5); + font-weight: var(--text-h5-weight); + line-height: var(--text-h5-lh); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +h6 { + font-size: var(--text-h6); + font-weight: var(--text-h6-weight); + line-height: var(--text-h6-lh); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +/* Paragraph */ +p { + font-size: var(--text-body-md); + line-height: 1.6; + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); +} + +/* Small text */ +small, .text-small { + font-size: var(--text-body-sm); + color: var(--color-text-secondary); +} + +/* Code */ +code { + font-family: var(--font-mono); + font-size: var(--text-code-inline); + background-color: var(--color-code-bg); + color: var(--color-code-text); + padding: 2px 6px; + border-radius: var(--radius-sm); +} + +pre { + font-family: var(--font-mono); + font-size: var(--text-code-block); + line-height: var(--text-code-lh); + background-color: var(--color-code-bg); + color: var(--color-code-text); + padding: var(--spacing-md); + border-radius: var(--radius-md); + overflow-x: auto; + margin-bottom: var(--spacing-md); +} + +pre code { + background: none; + padding: 0; + border-radius: 0; +} + +/* ============================================================================ + 4. GLOBAL STYLES & LAYOUT + ============================================================================ */ + +html { + scroll-behavior: smooth; + overflow-x: hidden; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Skip Link (accessibility) */ +.skip-link { + position: absolute; + left: -9999px; + z-index: var(--z-modal); + padding: var(--spacing-md); + background: var(--color-brand-primary); + color: white; + text-decoration: none; + border-radius: var(--radius-sm); +} + +.skip-link:focus { + left: var(--spacing-md); + top: var(--spacing-md); +} + +/* Main content area */ +#main { + flex: 1; + width: 100%; +} + +/* Container for page content */ +.ps-container { + max-width: var(--container-lg); + margin: 0 auto; + padding: 0 var(--spacing-lg); + width: 100%; +} + +.ps-container-wide { + max-width: var(--container-2xl); + margin: 0 auto; + padding: 0 var(--spacing-lg); + width: 100%; +} + +/* ============================================================================ + 5. LINK STYLING (Scope: body content vs navigation) + ============================================================================ */ + +/* Body content links: underlined */ +.site-content a, +.post-content a, +article a, +.page-content a { + color: var(--color-brand-primary); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color var(--transition-normal); +} + +.site-content a:hover, +.post-content a:hover, +article a:hover, +.page-content a:hover { + color: var(--color-link-hover); +} + +.site-content a:focus, +.post-content a:focus, +article a:focus, +.page-content a:focus { + outline: var(--focus-width) solid var(--focus-color); + outline-offset: var(--focus-offset); +} + +/* Footer links: underlined */ +.site-footer a { + color: var(--color-brand-primary); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: color var(--transition-normal); +} + +.site-footer a:hover { + color: var(--color-link-hover); +} + +/* Navigation links: NO underline (context is clear) */ +nav a { + text-decoration: none; +} + +/* ============================================================================ + 6. COMPONENTS + ============================================================================ */ + +/* ---- BUTTONS ---- */ + +.ps-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + padding: var(--btn-padding-y) var(--btn-padding-x); + border: none; + border-radius: var(--radius-md); + font-family: var(--font-sans); + font-size: var(--text-body-md); + font-weight: 500; + cursor: pointer; + transition: all var(--transition-normal); + text-decoration: none; + white-space: nowrap; + user-select: none; +} + +.ps-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.ps-btn:focus { + outline: none; + box-shadow: var(--focus-shadow); +} + +/* Primary Button */ +.ps-btn-primary { + background-color: var(--color-brand-primary); + color: white; +} + +.ps-btn-primary:hover { + background-color: var(--color-link-hover); + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(0, 102, 204, 0.2); +} + +.ps-btn-primary:active { + transform: translateY(0); +} + +/* Secondary Button */ +.ps-btn-secondary { + background-color: var(--color-bg-tertiary); + color: var(--color-text-primary); + border: 1px solid var(--color-border-default); +} + +.ps-btn-secondary:hover { + background-color: var(--color-bg-secondary); + border-color: var(--color-brand-primary); +} + +/* Tertiary Button */ +.ps-btn-tertiary { + background-color: transparent; + color: var(--color-brand-primary); + border: 1px solid var(--color-brand-primary); +} + +.ps-btn-tertiary:hover { + background-color: var(--color-brand-primary); + color: white; +} + +/* Button Sizes */ +.ps-btn-sm { + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--text-body-sm); +} + +.ps-btn-lg { + padding: var(--spacing-lg) var(--spacing-2xl); + font-size: var(--text-body-lg); +} + +.ps-btn-full { + width: 100%; +} + +/* ---- CARDS ---- */ + +.ps-card { + background-color: var(--color-bg-primary); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + transition: all var(--transition-normal); +} + +.ps-card:hover { + border-color: var(--color-brand-primary); + box-shadow: 0 4px 12px rgba(0, 102, 204, 0.1); +} + +.ps-card-header { + margin-bottom: var(--spacing-md); +} + +.ps-card-title { + font-size: var(--text-h5); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +.ps-card-meta { + font-size: var(--text-caption); + color: var(--color-text-secondary); +} + +.ps-card-content { + font-size: var(--text-body-md); + line-height: 1.6; + color: var(--color-text-primary); +} + +/* ---- BADGES & TAGS ---- */ + +.ps-badge { + display: inline-block; + padding: 4px 8px; + border-radius: var(--radius-md); + font-size: var(--text-label); + font-weight: var(--text-label-weight); + background-color: var(--color-badge-bg); + color: var(--color-badge-text); + white-space: nowrap; +} + +.ps-badge-success { + background-color: #D1FAE5; + color: var(--color-status-success); +} + +.ps-badge-warning { + background-color: #FEF3C7; + color: var(--color-status-warning); +} + +.ps-badge-error { + background-color: #FEE2E2; + color: var(--color-status-error); +} + +.ps-tag { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + padding: 4px 8px; + border-radius: var(--radius-md); + font-size: var(--text-label); + background-color: var(--color-bg-tertiary); + color: var(--color-text-primary); + cursor: pointer; + transition: all var(--transition-normal); +} + +.ps-tag:hover { + background-color: var(--color-border-default); +} + +.ps-tag-close { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-weight: 600; +} + +/* ---- FORMS ---- */ + +input[type="text"], +input[type="email"], +input[type="password"], +input[type="search"], +input[type="number"], +input[type="url"], +textarea, +select { + width: 100%; + padding: var(--spacing-md); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + font-family: var(--font-sans); + font-size: var(--text-body-md); + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + transition: all var(--transition-normal); +} + +input[type="text"]:focus, +input[type="email"]:focus, +input[type="password"]:focus, +input[type="search"]:focus, +input[type="number"]:focus, +input[type="url"]:focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: var(--focus-shadow); + background-color: var(--color-bg-secondary); +} + +/* Form labels */ +label { + display: block; + margin-bottom: var(--spacing-sm); + font-size: var(--text-label); + font-weight: var(--text-label-weight); + color: var(--color-text-primary); +} + +/* Form groups */ +.ps-form-group { + margin-bottom: var(--spacing-lg); +} + +/* Form error state (per Design Director) */ +input.ps-error, +textarea.ps-error, +select.ps-error { + border-color: var(--color-status-error); + background-color: rgba(220, 38, 38, 0.02); +} + +.ps-form-error { + color: var(--color-status-error); + font-size: var(--text-caption); + margin-top: 4px; + font-weight: var(--text-label-weight); +} + +/* Checkboxes & Radios */ +input[type="checkbox"], +input[type="radio"] { + width: 18px; + height: 18px; + margin: 0; + margin-right: var(--spacing-sm); + cursor: pointer; + accent-color: var(--color-brand-primary); +} + +/* ---- ALERTS & CALLOUTS ---- */ + +.ps-alert { + padding: var(--spacing-lg); + border-radius: var(--radius-md); + border-left: 4px solid; + margin-bottom: var(--spacing-md); +} + +.ps-alert-info { + background-color: #DBEAFE; + border-color: var(--color-brand-primary); + color: #0652CD; +} + +.ps-alert-success { + background-color: #D1FAE5; + border-color: var(--color-status-success); + color: #065F46; +} + +.ps-alert-warning { + background-color: #FEF3C7; + border-color: var(--color-status-warning); + color: #78350F; +} + +.ps-alert-error { + background-color: #FEE2E2; + border-color: var(--color-status-error); + color: #7F1D1D; +} + +/* ---- SEARCH FORM ---- */ + +.ps-search-form { + position: relative; + width: 100%; +} + +.ps-search-input { + width: 100%; + padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-lg); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-lg); + font-size: var(--text-body-md); +} + +.ps-search-input:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: var(--focus-shadow); +} + +.ps-search-icon { + position: absolute; + right: var(--spacing-md); + top: 50%; + transform: translateY(-50%); + color: var(--color-text-secondary); + pointer-events: none; +} + +/* ---- PAGINATION ---- */ + +.ps-pagination { + display: flex; + gap: var(--spacing-sm); + align-items: center; + justify-content: center; + margin: var(--spacing-2xl) 0; + flex-wrap: wrap; +} + +.ps-pagination a, +.ps-pagination span { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 40px; + height: 40px; + padding: 0 var(--spacing-sm); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + text-decoration: none; + color: var(--color-text-primary); + transition: all var(--transition-normal); +} + +.ps-pagination a:hover { + border-color: var(--color-brand-primary); + background-color: var(--color-brand-primary); + color: white; +} + +.ps-pagination .current { + background-color: var(--color-brand-primary); + border-color: var(--color-brand-primary); + color: white; +} + +/* ============================================================================ + 7. REGIONS (Header, Footer, Main) + ============================================================================ */ + +/* ---- HEADER ---- */ + +.site-header { + background-color: var(--color-bg-primary); + border-bottom: 1px solid var(--color-border-subtle); + padding: var(--spacing-lg) var(--spacing-xl); + position: sticky; + top: 0; + z-index: var(--z-sticky); + box-shadow: var(--shadow-sm); +} + +/* Account for WP admin bar when logged in */ +.admin-bar .site-header { + top: 32px; +} + +@media screen and (max-width: 782px) { + .admin-bar .site-header { + top: 46px; + } +} + +.site-header-wrapper { + max-width: var(--container-2xl); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-lg); +} + +/* Logo */ +.site-logo { + flex: 0 0 auto; + font-size: 24px; + font-weight: 700; + color: var(--color-brand-primary); + text-decoration: none; + line-height: 1; + transition: color var(--transition-normal); +} + +.site-logo:hover { + color: var(--color-link-hover); +} + +.site-logo img { + height: 32px; + width: auto; +} + +/* Primary Navigation */ +.primary-navigation { + display: flex; + align-items: center; + flex: 1; + margin-left: var(--spacing-lg); + margin-right: auto; +} + +.primary-navigation ul { + display: flex; + gap: var(--spacing-xl); + align-items: center; + list-style: none; + margin: 0; + padding: 0; +} + +.primary-navigation li { + display: flex; + align-items: center; +} + +.primary-navigation a { + font-size: var(--text-body-md); + font-weight: 500; + color: var(--color-text-primary); + text-decoration: none; + position: relative; + transition: color var(--transition-normal); +} + +.primary-navigation a:hover { + color: var(--color-brand-primary); +} + +.primary-navigation a::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 2px; + background: var(--color-brand-primary); + transform: scaleX(0); + transform-origin: right; + transition: transform var(--transition-normal); +} + +.primary-navigation a:hover::after, +.primary-navigation a.active::after { + transform: scaleX(1); + transform-origin: left; +} + +/* Header Icons */ +.header-icons { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.header-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + background: none; + color: var(--color-text-primary); + cursor: pointer; + transition: color var(--transition-normal); + padding: 0; +} + +.header-icon-btn:hover { + color: var(--color-brand-primary); +} + +.header-icon-btn:focus { + outline: none; + box-shadow: var(--focus-shadow); + border-radius: var(--radius-md); +} + +/* Hamburger Menu */ +.menu-toggle { + display: none; + width: 40px; + height: 40px; + border: none; + background: none; + cursor: pointer; + padding: 0; + color: var(--color-text-primary); +} + +/* Mobile Navigation */ +.nav-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.3); + opacity: 0; + pointer-events: none; + z-index: var(--z-modal-backdrop); + transition: opacity var(--transition-slow); +} + +.nav-overlay.active { + opacity: 1; + pointer-events: auto; +} + +/* ---- FOOTER ---- */ + +.site-footer { + background-color: var(--color-bg-secondary); + border-top: 1px solid var(--color-border-subtle); + padding: var(--spacing-3xl) var(--spacing-lg); + margin-top: auto; +} + +.footer-wrapper { + max-width: var(--container-2xl); + margin: 0 auto; +} + +.footer-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--spacing-2xl); + margin-bottom: var(--spacing-3xl); +} + +.footer-section h3 { + font-size: var(--text-h5); + margin-bottom: var(--spacing-lg); +} + +.footer-section ul { + list-style: none; + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.footer-section a { + color: var(--color-brand-primary); + text-decoration: underline; + transition: color var(--transition-normal); +} + +.footer-section a:hover { + color: var(--color-link-hover); +} + +.footer-newsletter { + background-color: var(--color-bg-tertiary); + padding: var(--spacing-2xl); + border-radius: var(--radius-lg); + margin-bottom: var(--spacing-3xl); +} + +.footer-newsletter h3 { + margin-top: 0; +} + +.newsletter-form { + display: flex; + gap: var(--spacing-sm); + margin-top: var(--spacing-lg); +} + +.newsletter-form input { + flex: 1; + min-width: 200px; +} + +.newsletter-form button { + flex: 0 0 auto; +} + +.footer-bottom { + border-top: 1px solid var(--color-border-default); + padding-top: var(--spacing-2xl); + text-align: center; + color: var(--color-text-secondary); + font-size: var(--text-body-sm); +} + +/* ============================================================================ + 8. PAGES + ============================================================================ */ + +/* ---- HOME PAGE / HERO ---- */ + +.hero-section { + background: linear-gradient(135deg, var(--color-bg-primary) 0%, var(--color-bg-secondary) 100%); + padding: var(--spacing-4xl) var(--spacing-xl); + text-align: center; +} + +.hero-content { + max-width: var(--container-lg); + margin: 0 auto; +} + +.hero-title { + font-size: var(--text-h1); + font-weight: 700; + margin-bottom: var(--spacing-md); + color: var(--color-text-primary); +} + +.hero-subtitle { + font-size: var(--text-body-lg); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-2xl); + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.hero-search { + margin-bottom: var(--spacing-2xl); +} + +/* Legacy theme search form (fallback when PSA plugin is not active) */ +.hero-search .ps-search-form { + max-width: 500px; + margin: 0 auto; +} + +.hero-search .ps-search-input { + width: 100%; + padding-right: 48px; + background-color: var(--color-bg-secondary); + color: var(--color-text-primary); +} + +/* ---- PEPTIDE SEARCH AI PLUGIN INTEGRATION ---- */ + +/* Hero search — constrain the PSA form to match hero layout */ +.hero-search .psa-search-wrap { + max-width: 560px; + margin: 0 auto; +} + +.hero-search .psa-search-input { + background-color: var(--color-bg-secondary); + color: var(--color-text-primary); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-lg); + font-size: var(--text-body-md); + padding: var(--spacing-md) var(--spacing-lg); +} + +.hero-search .psa-search-input::placeholder { + color: var(--color-text-tertiary); +} + +.hero-search .psa-search-input:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: var(--focus-shadow); +} + +.hero-search .psa-search-btn { + background-color: var(--color-brand-primary); + color: white; + border: none; + border-radius: 0 var(--radius-lg) var(--radius-lg) 0; + cursor: pointer; + transition: background-color var(--transition-normal); +} + +.hero-search .psa-search-btn:hover { + background-color: var(--color-brand-hover); +} + +.hero-search .psa-search-results { + text-align: left; + max-height: 400px; + overflow-y: auto; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + background-color: var(--color-bg-primary); + border: 1px solid var(--color-border-default); +} + +/* Search overlay — full-screen overlay triggered from header search icon */ +.search-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + z-index: var(--z-modal, 1000); + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 120px; + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-normal), visibility var(--transition-normal); +} + +.search-overlay.active { + opacity: 1; + visibility: visible; +} + +.search-overlay-inner { + background-color: var(--color-bg-primary); + border-radius: var(--radius-xl); + padding: var(--spacing-xl); + width: 90%; + max-width: 600px; + position: relative; + box-shadow: var(--shadow-xl); + transform: translateY(-20px); + transition: transform var(--transition-normal); +} + +.search-overlay.active .search-overlay-inner { + transform: translateY(0); +} + +.search-overlay-close { + position: absolute; + top: var(--spacing-md); + right: var(--spacing-md); + background: none; + border: none; + color: var(--color-text-secondary); + cursor: pointer; + padding: var(--spacing-xs); + border-radius: var(--radius-md); + transition: color var(--transition-normal), background-color var(--transition-normal); + z-index: 1; +} + +.search-overlay-close:hover { + color: var(--color-text-primary); + background-color: var(--color-bg-secondary); +} + +.search-overlay-content { + padding-top: var(--spacing-sm); +} + +/* PSA plugin styling inside the overlay */ +.search-overlay .psa-search-input { + background-color: var(--color-bg-secondary); + color: var(--color-text-primary); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-lg); + font-size: var(--text-body-md); + padding: var(--spacing-md) var(--spacing-lg); +} + +.search-overlay .psa-search-input::placeholder { + color: var(--color-text-tertiary); +} + +.search-overlay .psa-search-input:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: var(--focus-shadow); +} + +.search-overlay .psa-search-btn { + background-color: var(--color-brand-primary); + color: white; + border: none; + border-radius: 0 var(--radius-lg) var(--radius-lg) 0; + cursor: pointer; + transition: background-color var(--transition-normal); +} + +.search-overlay .psa-search-btn:hover { + background-color: var(--color-brand-hover); +} + +.search-overlay .psa-search-results { + max-height: 350px; + overflow-y: auto; + border-radius: var(--radius-lg); + margin-top: var(--spacing-md); +} + +.search-overlay .psa-checking, +.search-overlay .psa-pending, +.search-overlay .psa-invalid, +.search-overlay .psa-error { + margin-top: var(--spacing-md); + border-radius: var(--radius-lg); +} + +/* ---- PEPTIDE NEWS PLUGIN OVERRIDES ---- */ +/* Remove line-clamp so full article summaries are visible. + Uses body prefix for specificity to beat the plugin's own stylesheet + which loads after the theme. */ +body .pn-article-excerpt, +body p.pn-article-excerpt { + -webkit-line-clamp: unset; + -webkit-box-orient: unset; + display: block; + overflow: visible; + font-size: var(--text-body-md); + line-height: 1.6; +} + +/* Tighten article card title for density */ +body .pn-article-title { + font-size: var(--text-h4); + line-height: 1.3; +} + +.hero-ctas { + display: flex; + gap: var(--spacing-md); + justify-content: center; + flex-wrap: wrap; +} + +.hero-ctas .ps-btn { + padding: var(--btn-padding-y) var(--btn-padding-x); +} + +/* News Feed Area */ +.news-feed-section { + padding: var(--spacing-3xl) var(--spacing-lg); + background-color: var(--color-bg-primary); +} + +.news-feed-header { + max-width: var(--container-lg); + margin: 0 auto var(--spacing-2xl); + text-align: center; +} + +.news-feed-title { + font-size: var(--text-h2); + margin-bottom: var(--spacing-md); +} + +.news-feed-description { + color: var(--color-text-secondary); + font-size: var(--text-body-md); +} + +/* ---- ARCHIVE PAGE ---- */ + +.archive-header { + padding: var(--spacing-2xl) var(--spacing-lg) var(--spacing-3xl); + background-color: var(--color-bg-secondary); + border-bottom: 1px solid var(--color-border-subtle); +} + +.archive-title { + max-width: var(--container-lg); + margin: 0 auto; + font-size: var(--text-h2); + color: var(--color-text-primary); +} + +.archive-content { + padding: var(--spacing-3xl) var(--spacing-lg); +} + +.archive-filters { + max-width: var(--container-lg); + margin: 0 auto var(--spacing-2xl); + display: flex; + gap: var(--spacing-md); + flex-wrap: wrap; + align-items: center; +} + +.archive-search { + flex: 1; + min-width: 200px; +} + +.archive-grid { + max-width: var(--container-lg); + margin: 0 auto; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-2xl); +} + +.peptides-grid { + max-width: var(--container-2xl); + margin: 0 auto; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-2xl); + padding: var(--spacing-3xl) var(--spacing-lg); +} + +/* Peptide Card */ +.peptide-card { + background-color: var(--color-bg-secondary); + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + display: flex; + flex-direction: column; + transition: transform var(--transition-normal), box-shadow var(--transition-normal); +} + +.peptide-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.peptide-card__header { + margin-bottom: var(--spacing-md); +} + +.peptide-card__title { + font-size: var(--text-h4); + font-weight: 700; + line-height: 1.3; + margin: 0 0 var(--spacing-sm) 0; +} + +.peptide-card__title a { + color: var(--color-brand-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +.peptide-card__title a:hover { + color: var(--color-link-hover); +} + +.peptide-card__meta { + display: flex; + gap: var(--spacing-xs); + flex-wrap: wrap; +} + +.peptide-card__badge { + display: inline-block; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 2px 8px; + border-radius: var(--radius-full); + background-color: var(--color-brand-primary); + color: #ffffff; +} + +.peptide-card__excerpt { + font-size: var(--text-body-md); + line-height: 1.65; + color: var(--color-text-secondary); + margin-bottom: var(--spacing-lg); + flex: 1; + overflow: hidden; +} + +.peptide-card__footer { + margin-top: auto; + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-border-subtle); +} + +.peptide-card__link { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + font-size: var(--text-body-sm); + font-weight: 600; + color: var(--color-brand-primary); + text-decoration: none; + transition: gap var(--transition-fast), color var(--transition-fast); +} + +.peptide-card__link:hover { + color: var(--color-link-hover); + gap: var(--spacing-sm); +} + +.peptide-card__link svg { + transition: transform var(--transition-fast); +} + +.peptide-card__link:hover svg { + transform: translateX(2px); +} + +.archive-empty { + text-align: center; + padding: var(--spacing-4xl) var(--spacing-lg); +} + +.archive-empty h2 { + margin-top: 0; +} + +.archive-empty p { + color: var(--color-text-secondary); + margin-bottom: var(--spacing-2xl); +} + +/* ---- SINGLE PAGE / PEPTIDE ---- */ + +.page-header { + padding: var(--spacing-2xl) var(--spacing-lg); + background-color: var(--color-bg-secondary); + border-bottom: 1px solid var(--color-border-subtle); +} + +.page-title { + max-width: var(--container-lg); + margin: 0 auto; + font-size: var(--text-h2); +} + +.page-content { + max-width: var(--container-lg); + margin: 0 auto; + padding: var(--spacing-3xl) var(--spacing-lg); +} + +.page-content h2 { + font-size: var(--text-h3); + margin-top: var(--spacing-2xl); + margin-bottom: var(--spacing-md); +} + +.page-content h3 { + font-size: var(--text-h4); + margin-top: var(--spacing-xl); + margin-bottom: var(--spacing-md); +} + +.page-content ul, +.page-content ol { + margin-left: var(--spacing-lg); + margin-bottom: var(--spacing-md); +} + +.page-content li { + margin-bottom: var(--spacing-sm); +} + +/* Collapse empty Elementor/page-builder containers that add phantom space */ +.page-content .elementor-section:empty, +.page-content .elementor-widget-wrap:empty, +.page-content .elementor-element:empty, +.page-content div:empty:not([data-peptide-news-feed]) { + display: none; +} + +/* ---- 404 PAGE ---- */ + +.error-404 { + text-align: center; + padding: var(--spacing-4xl) var(--spacing-lg); +} + +.error-404 h1 { + font-size: var(--text-h1); + margin-bottom: var(--spacing-lg); +} + +.error-404 p { + color: var(--color-text-secondary); + margin-bottom: var(--spacing-2xl); + font-size: var(--text-body-md); +} + +/* ============================================================================ + 9. ANIMATIONS & TRANSITIONS + ============================================================================ */ + +/* Fade in animation */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.fade-in { + animation: fadeIn var(--transition-slow) ease-out; +} + +/* Slide in from left */ +@keyframes slideInLeft { + from { + transform: translateX(-100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.slide-in-left { + animation: slideInLeft var(--transition-slow) ease-out; +} + +/* Skeleton loading animation */ +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +.ps-skeleton { + background: linear-gradient( + 90deg, + var(--color-bg-tertiary) 25%, + var(--color-border-default) 50%, + var(--color-bg-tertiary) 75% + ); + background-size: 1000px 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-md); +} + +/* ============================================================================ + 10. ACCESSIBILITY + ============================================================================ */ + +/* Focus visible style (keyboard focus) */ +*:focus-visible { + outline: var(--focus-width) solid var(--focus-color); + outline-offset: var(--focus-offset); +} + +/* Skip to main content link */ +.skip-to-main { + position: absolute; + top: -40px; + left: 0; + background: var(--color-brand-primary); + color: white; + padding: var(--spacing-md); + text-decoration: none; + z-index: var(--z-modal); +} + +.skip-to-main:focus { + top: 0; +} + +/* Visually hidden but accessible to screen readers */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* Reduced motion preferences */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* ============================================================================ + 11. DARK MODE + ============================================================================ + + Precedence (highest to lowest): + 1. html[data-theme="light"] or html[data-theme="dark"] (explicit user choice) + 2. @media (prefers-color-scheme: dark) (system preference) + 3. Default light mode (above) + + Plugin dark mode coordination: + - Theme broadcasts dark mode changes via data-theme attribute on + - Plugin must watch this attribute and update colors accordingly + - Plugin can also use CSS custom properties which theme controls + + ============================================================================ */ + +@media (prefers-color-scheme: dark) { + :root { + /* Brand Colors */ + --color-brand-primary: #3B82F6; + --color-status-success: #10B981; + --color-status-warning: #F59E0B; + --color-status-error: #EF4444; + + /* Text Colors */ + --color-text-primary: #F9FAFB; + --color-text-secondary: #D1D5DB; + --color-text-tertiary: #9CA3AF; + + /* Border Colors */ + --color-border-default: #374151; + --color-border-subtle: #1F2937; + + /* Background Colors */ + --color-bg-primary: #111827; + --color-bg-secondary: #1F2937; + --color-bg-tertiary: #374151; + + /* Component-Specific Colors */ + --color-input-focus: #3B82F6; + --color-link-hover: #60A5FA; + --color-badge-bg: #0F172A; + --color-badge-text: #60A5FA; + --color-code-bg: #1F2937; + --color-code-text: #F9FAFB; + + /* Focus shadow adjusted for dark mode */ + --focus-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); + } +} + +/* Allow manual dark mode toggle (explicit override) */ +html[data-theme="dark"] { + --color-brand-primary: #3B82F6; + --color-status-success: #10B981; + --color-status-warning: #F59E0B; + --color-status-error: #EF4444; + --color-text-primary: #F9FAFB; + --color-text-secondary: #D1D5DB; + --color-text-tertiary: #9CA3AF; + --color-border-default: #374151; + --color-border-subtle: #1F2937; + --color-bg-primary: #111827; + --color-bg-secondary: #1F2937; + --color-bg-tertiary: #374151; + --color-input-focus: #3B82F6; + --color-link-hover: #60A5FA; + --color-badge-bg: #0F172A; + --color-badge-text: #60A5FA; + --color-code-bg: #1F2937; + --color-code-text: #F9FAFB; + --focus-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); +} + +html[data-theme="light"] { + --color-brand-primary: #0066CC; + --color-status-success: #059669; + --color-status-warning: #F59E0B; + --color-status-error: #DC2626; + --color-text-primary: #111827; + --color-text-secondary: #6B7280; + --color-text-tertiary: #9CA3AF; + --color-border-default: #E5E7EB; + --color-border-subtle: #F3F4F6; + --color-bg-primary: #FFFFFF; + --color-bg-secondary: #F9FAFB; + --color-bg-tertiary: #F3F4F6; + --color-input-focus: #0066CC; + --color-link-hover: #0052A3; + --color-badge-bg: #DBEAFE; + --color-badge-text: #0066CC; + --color-code-bg: #F3F4F6; + --color-code-text: #111827; + --focus-shadow: 0 0 0 4px rgba(0, 102, 204, 0.15); +} + +/* Dark mode alert colors */ +@media (prefers-color-scheme: dark) { + .ps-alert-info { + background-color: #0C2340; + border-color: #3B82F6; + color: #BFDBFE; + } + + .ps-alert-success { + background-color: #064E3B; + border-color: #10B981; + color: #DCFCE7; + } + + .ps-alert-warning { + background-color: #78350F; + border-color: #F59E0B; + color: #FEF3C7; + } + + .ps-alert-error { + background-color: #7F1D1D; + border-color: #EF4444; + color: #FEE2E2; + } + + .footer-newsletter { + background-color: var(--color-bg-secondary); + } +} + +/* ============================================================================ + 12. RESPONSIVE DESIGN (Mobile-first) + ============================================================================ */ + +/* Extra Small (320px - 479px) */ +@media (max-width: 479px) { + .hero-section { + padding: var(--spacing-2xl) var(--spacing-md); + } + + .hero-title { + font-size: 32px; + } + + .hero-ctas { + gap: var(--spacing-sm); + } + + .hero-ctas .ps-btn { + padding: var(--spacing-sm) var(--spacing-lg); + font-size: var(--text-body-sm); + } + + .page-content { + padding: var(--spacing-lg) var(--spacing-md); + } + + .peptides-grid { + padding: var(--spacing-2xl) var(--spacing-md); + } + + .news-feed-section { + padding: var(--spacing-2xl) var(--spacing-md); + } + + h1 { + font-size: 32px; + } + + h2 { + font-size: 24px; + } + + .site-header-wrapper { + gap: var(--spacing-sm); + } + + .primary-navigation { + display: none; + } + + .menu-toggle { + display: flex; + } + + .header-icons { + gap: var(--spacing-sm); + } + + .newsletter-form { + flex-direction: column; + } + + .newsletter-form button { + width: 100%; + } + + .footer-grid { + grid-template-columns: 1fr; + gap: var(--spacing-lg); + } +} + +/* Small (480px - 767px) */ +@media (min-width: 480px) { + .site-header { + padding: var(--spacing-lg) var(--spacing-lg); + } +} + +/* Medium (768px - 1023px) */ +@media (min-width: 768px) { + .menu-toggle { + display: none; + } + + .primary-navigation { + display: flex !important; + position: static; + width: auto; + height: auto; + flex-direction: row; + background: none; + padding: 0; + transition: none; + } + + .nav-overlay { + display: none; + } + + .peptides-grid { + grid-template-columns: repeat(2, 1fr); + } + + .archive-grid { + grid-template-columns: repeat(2, 1fr); + } + + .hero-title { + font-size: 40px; + } + + .newsletter-form { + flex-direction: row; + } + + .newsletter-form button { + width: auto; + } +} + +/* Large (1024px - 1439px) */ +@media (min-width: 1024px) { + .peptides-grid { + grid-template-columns: repeat(3, 1fr); + } + + .archive-grid { + grid-template-columns: repeat(3, 1fr); + } + + .site-header { + padding: var(--spacing-lg) var(--spacing-xl); + } + + .footer-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +/* Extra Large (1440px+) */ +@media (min-width: 1440px) { + .peptides-grid { + grid-template-columns: repeat(4, 1fr); + } + + .site-header { + padding: var(--spacing-lg) var(--spacing-2xl); + } +} + +/* ============================================================================ + 13. UTILITY CLASSES + ============================================================================ */ + +/* 404 Page Utilities */ +.ps-404-search-form { + margin: var(--spacing-2xl) 0; +} + +.ps-404-cta-group { + display: flex; + gap: var(--spacing-md); + justify-content: center; + flex-wrap: wrap; +} + +/* ============================================================================ + END OF STYLESHEET + ============================================================================ */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3d2811e --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,63 @@ + [db-host] [wp-version]\n"; + exit( 1 ); +} + +// Load the WP test suite functions. +require_once $_tests_dir . '/includes/functions.php'; + +/** + * Register the theme so WordPress loads it during test setup. + */ +function _register_theme() { + $theme_dir = dirname( __DIR__ ); + $theme_name = basename( $theme_dir ); + + // Tell WordPress where our theme lives. + register_theme_directory( dirname( $theme_dir ) ); + add_filter( + 'pre_option_stylesheet', + function () use ( $theme_name ) { + return $theme_name; + } + ); + add_filter( + 'pre_option_template', + function () use ( $theme_name ) { + return $theme_name; + } + ); +} +tests_add_filter( 'muplugins_loaded', '_register_theme' ); + +// Boot the WP test framework. +require $_tests_dir . '/includes/bootstrap.php'; diff --git a/tests/test-functions.php b/tests/test-functions.php new file mode 100644 index 0000000..3022432 --- /dev/null +++ b/tests/test-functions.php @@ -0,0 +1,105 @@ +assertTrue( defined( 'PEPTIDE_STARTER_VERSION' ), 'PEPTIDE_STARTER_VERSION must be defined.' ); + $this->assertMatchesRegularExpression( '/^\d+\.\d+\.\d+$/', PEPTIDE_STARTER_VERSION, 'Version must follow semver (x.y.z).' ); + } + + /** + * Verify directory constant points to an existing path. + */ + public function test_dir_constant_defined() { + $this->assertTrue( defined( 'PEPTIDE_STARTER_DIR' ), 'PEPTIDE_STARTER_DIR must be defined.' ); + $this->assertDirectoryExists( PEPTIDE_STARTER_DIR ); + } + + /** + * Hero title should return the site name when no customizer override is set. + */ + public function test_get_hero_title_returns_default() { + $title = peptide_starter_get_hero_title(); + $this->assertNotEmpty( $title, 'Hero title must not be empty.' ); + $this->assertEquals( get_bloginfo( 'name' ), $title, 'Default hero title should match site name.' ); + } + + /** + * Hero subtitle should return a non-empty string. + */ + public function test_get_hero_subtitle_returns_default() { + $subtitle = peptide_starter_get_hero_subtitle(); + $this->assertNotEmpty( $subtitle ); + } + + /** + * Search placeholder should return a non-empty string. + */ + public function test_get_search_placeholder_returns_default() { + $placeholder = peptide_starter_get_search_placeholder(); + $this->assertNotEmpty( $placeholder ); + } + + /** + * Footer copyright should return a non-empty string. + */ + public function test_get_footer_copyright_returns_default() { + $copyright = peptide_starter_get_footer_copyright(); + $this->assertNotEmpty( $copyright ); + } + + /** + * Newsletter form visibility filter should default to true. + */ + public function test_show_newsletter_form_default_true() { + $this->assertTrue( peptide_starter_show_newsletter_form() ); + } + + /** + * Newsletter form visibility can be disabled via filter. + */ + public function test_show_newsletter_form_filterable() { + add_filter( 'peptide_starter_show_newsletter', '__return_false' ); + $this->assertFalse( peptide_starter_show_newsletter_form() ); + remove_filter( 'peptide_starter_show_newsletter', '__return_false' ); + } + + /** + * Dark mode body class should only be added when dark_mode_default is true. + */ + public function test_dark_mode_body_class_not_added_by_default() { + $classes = peptide_starter_dark_mode_body_class( array() ); + $this->assertNotContains( 'dark-mode-default', $classes ); + } + + /** + * Sanitize checkbox should return true for truthy input. + */ + public function test_sanitize_checkbox_truthy() { + $this->assertTrue( peptide_starter_sanitize_checkbox( true ) ); + } + + /** + * Sanitize checkbox should return false for falsy input. + */ + public function test_sanitize_checkbox_falsy() { + $this->assertFalse( peptide_starter_sanitize_checkbox( false ) ); + $this->assertFalse( peptide_starter_sanitize_checkbox( null ) ); + $this->assertFalse( peptide_starter_sanitize_checkbox( '' ) ); + } +} diff --git a/tests/test-theme-setup.php b/tests/test-theme-setup.php new file mode 100644 index 0000000..cee9c49 --- /dev/null +++ b/tests/test-theme-setup.php @@ -0,0 +1,72 @@ +assertTrue( current_theme_supports( 'title-tag' ), 'Theme must support title-tag.' ); + $this->assertTrue( current_theme_supports( 'post-thumbnails' ), 'Theme must support post-thumbnails.' ); + $this->assertTrue( current_theme_supports( 'custom-logo' ), 'Theme must support custom-logo.' ); + $this->assertTrue( current_theme_supports( 'html5' ), 'Theme must support html5.' ); + } + + /** + * Verify navigation menus are registered. + */ + public function test_nav_menus_registered() { + $menus = get_registered_nav_menus(); + $this->assertArrayHasKey( 'primary', $menus, 'Primary menu must be registered.' ); + $this->assertArrayHasKey( 'footer', $menus, 'Footer menu must be registered.' ); + } + + /** + * Verify footer widget areas are registered. + */ + public function test_footer_widget_areas_registered() { + global $wp_registered_sidebars; + + for ( $i = 1; $i <= 4; $i++ ) { + $this->assertArrayHasKey( + 'footer-' . $i, + $wp_registered_sidebars, + "Footer widget area footer-{$i} must be registered." + ); + } + } + + /** + * Verify the custom nav walker class exists. + */ + public function test_nav_walker_class_exists() { + $this->assertTrue( + class_exists( 'Peptide_Starter_Nav_Walker' ), + 'Peptide_Starter_Nav_Walker class must be defined.' + ); + } + + /** + * Verify scripts and styles are enqueued on the front end. + */ + public function test_scripts_and_styles_enqueued() { + // Simulate a front-end page load. + do_action( 'wp_enqueue_scripts' ); + + $this->assertTrue( wp_style_is( 'peptide-starter-style', 'enqueued' ), 'Main stylesheet must be enqueued.' ); + $this->assertTrue( wp_script_is( 'peptide-starter-navigation', 'enqueued' ), 'Navigation script must be enqueued.' ); + $this->assertTrue( wp_script_is( 'peptide-starter-theme', 'enqueued' ), 'Theme script must be enqueued.' ); + } +}