diff --git a/.gitea/.mokostandards b/.gitea/.mokostandards deleted file mode 100644 index 272b5a4..0000000 --- a/.gitea/.mokostandards +++ /dev/null @@ -1,54 +0,0 @@ - - - - - MokoOnyx - MokoConsulting - MokoOnyx - Joomla site template (successor to MokoCassiopeia) - GNU General Public License v3 - - - joomla - 04.07.00 - https://git.mokoconsulting.tech/MokoConsulting/MokoStandards - 2026-05-02T23:06:05+00:00 - - - CSS - php:>=8.1 - joomla-extension - src/templateDetails.xml - - - - - - - - - diff --git a/.gitignore b/.gitignore index d89e74c..fde323a 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,8 @@ site/ *.map *.css.map *.js.map +*.min.css +*.min.js *.tsbuildinfo # ============================================================ @@ -211,6 +213,10 @@ hypothesis/ src/media/css/theme/*.custom.css src/media/css/theme/*.custom.min.css templates/*.custom.css + +# User override files (site-specific, not version controlled) +src/media/css/user.css +src/media/js/user.js update.xml .moko-standards profile.ps1 diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 9c3149a..0000000 --- a/.mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mcpServers": { - "joomla-api": { - "type": "stdio", - "command": "node", - "args": ["A:/joomla-api-mcp/dist/index.js"] - } - } -} diff --git a/.mokogitea/ISSUE_TEMPLATE/adr.md b/.mokogitea/ISSUE_TEMPLATE/adr.md new file mode 100644 index 0000000..eb40760 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/adr.md @@ -0,0 +1,110 @@ +--- +name: Architecture Decision Record (ADR) +about: Propose or document an architectural decision +title: '[ADR] ' +labels: 'architecture, decision' +assignees: '' + +--- + + +## ADR Number +ADR-XXXX + +## Status +- [ ] Proposed +- [ ] Accepted +- [ ] Deprecated +- [ ] Superseded by ADR-XXXX + +## Context +Describe the issue or problem that motivates this decision. + +## Decision +State the architecture decision and provide rationale. + +## Consequences +### Positive +- List positive consequences + +### Negative +- List negative consequences or trade-offs + +### Neutral +- List neutral aspects + +## Alternatives Considered +### Alternative 1 +- Description +- Pros +- Cons +- Why not chosen + +### Alternative 2 +- Description +- Pros +- Cons +- Why not chosen + +## Implementation Plan +1. Step 1 +2. Step 2 +3. Step 3 + +## Stakeholders +- **Decision Makers**: @user1, @user2 +- **Consulted**: @user3, @user4 +- **Informed**: team-name + +## Technical Details +### Architecture Diagram +``` +[Add diagram or link] +``` + +### Dependencies +- Dependency 1 +- Dependency 2 + +### Impact Analysis +- **Performance**: [Impact description] +- **Security**: [Impact description] +- **Scalability**: [Impact description] +- **Maintainability**: [Impact description] + +## Testing Strategy +- [ ] Unit tests +- [ ] Integration tests +- [ ] Performance tests +- [ ] Security tests + +## Documentation +- [ ] Architecture documentation updated +- [ ] API documentation updated +- [ ] Developer guide updated +- [ ] Runbook created + +## Migration Path +Describe how to migrate from current state to new architecture. + +## Rollback Plan +Describe how to rollback if issues occur. + +## Timeline +- **Proposal Date**: +- **Decision Date**: +- **Implementation Start**: +- **Expected Completion**: + +## References +- Related ADRs: +- External resources: +- RFCs: + +## Review Checklist +- [ ] Aligns with enterprise architecture principles +- [ ] Security implications reviewed +- [ ] Performance implications reviewed +- [ ] Cost implications reviewed +- [ ] Compliance requirements met +- [ ] Team consensus achieved diff --git a/.mokogitea/ISSUE_TEMPLATE/bug_report.md b/.mokogitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..38a16a7 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug Report +about: Report a bug or issue with the project +title: '[BUG] ' +labels: 'bug' +assignees: '' + +--- + + +## Bug Description +A clear and concise description of what the bug is. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior +A clear and concise description of what you expected to happen. + +## Actual Behavior +A clear and concise description of what actually happened. + +## Screenshots +If applicable, add screenshots to help explain your problem. + +## Environment +- **Project**: [e.g., MokoDoliTools, moko-cassiopeia] +- **Version**: [e.g., 1.2.3] +- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0] +- **PHP Version**: [e.g., 8.1] +- **Database**: [e.g., MySQL 8.0, PostgreSQL 14] +- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121] +- **OS**: [e.g., Ubuntu 22.04, Windows 11] + +## Additional Context +Add any other context about the problem here. + +## Possible Solution +If you have suggestions on how to fix the issue, please describe them here. + +## Checklist +- [ ] I have searched for similar issues before creating this one +- [ ] I have provided all the requested information +- [ ] I have tested this on the latest stable version +- [ ] I have checked the documentation and couldn't find a solution diff --git a/.mokogitea/ISSUE_TEMPLATE/config.yml b/.mokogitea/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d4d49ec --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,18 @@ +--- +blank_issues_enabled: true +contact_links: + - name: 💼 Enterprise Support + url: https://mokoconsulting.tech/enterprise + about: Enterprise-level support and consultation services + - name: 💬 Ask a Question + url: https://mokoconsulting.tech/ + about: Get help or ask questions through our website + - name: 📚 MokoStandards Documentation + url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform + about: View our coding standards and best practices + - name: 🔒 Report a Security Vulnerability + url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new + about: Report security vulnerabilities privately (for critical issues) + - name: 💡 Community Discussions + url: https://github.com/orgs/mokoconsulting-tech/discussions + about: Join community discussions and Q&A diff --git a/.mokogitea/ISSUE_TEMPLATE/documentation.md b/.mokogitea/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..ed4dabc --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,52 @@ +--- +name: Documentation Issue +about: Report an issue with documentation +title: '[DOCS] ' +labels: 'documentation' +assignees: '' + +--- + + +## Documentation Issue + +**Location**: + + +## Issue Type + +- [ ] Typo or grammar error +- [ ] Outdated information +- [ ] Missing documentation +- [ ] Unclear explanation +- [ ] Broken links +- [ ] Missing examples +- [ ] Other (specify below) + +## Description + + +## Current Content + +``` +Current text here +``` + +## Suggested Improvement + +``` +Suggested text here +``` + +## Additional Context + + +## Standards Alignment +- [ ] Follows MokoStandards documentation guidelines +- [ ] Uses en_US/en_GB localization +- [ ] Includes proper SPDX headers where applicable + +## Checklist +- [ ] I have searched for similar documentation issues +- [ ] I have provided a clear description +- [ ] I have suggested an improvement (if applicable) diff --git a/.mokogitea/ISSUE_TEMPLATE/feature_request.md b/.mokogitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7b76dc9 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,51 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement +title: '[FEATURE] ' +labels: 'enhancement' +assignees: '' + +--- + + +## Feature Description +A clear and concise description of the feature you'd like to see. + +## Problem or Use Case +Describe the problem this feature would solve or the use case it addresses. +Ex. I'm always frustrated when [...] + +## Proposed Solution +A clear and concise description of what you want to happen. + +## Alternative Solutions +A clear and concise description of any alternative solutions or features you've considered. + +## Benefits +Describe how this feature would benefit users: +- Who would use this feature? +- What problems does it solve? +- What value does it add? + +## Implementation Details (Optional) +If you have ideas about how this could be implemented, share them here: +- Technical approach +- Files/components that might need changes +- Any concerns or challenges you foresee + +## Additional Context +Add any other context, mockups, or screenshots about the feature request here. + +## Relevant Standards +Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)? +- [ ] Accessibility (WCAG 2.1 AA) +- [ ] Localization (en_US/en_GB) +- [ ] Security best practices +- [ ] Code quality standards +- [ ] Other: [specify] + +## Checklist +- [ ] I have searched for similar feature requests before creating this one +- [ ] I have clearly described the use case and benefits +- [ ] I have considered alternative solutions +- [ ] This feature aligns with the project's goals and scope diff --git a/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md new file mode 100644 index 0000000..d808f79 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/joomla_issue.md @@ -0,0 +1,87 @@ +--- +name: Joomla Extension Issue +about: Report an issue with a Joomla extension +title: '[JOOMLA] ' +labels: 'joomla' +assignees: '' + +--- + + +## Issue Type +- [ ] Component Issue +- [ ] Module Issue +- [ ] Plugin Issue +- [ ] Template Issue + +## Extension Details +- **Extension Name**: [e.g., moko-cassiopeia] +- **Extension Version**: [e.g., 1.2.3] +- **Extension Type**: [Component / Module / Plugin / Template] + +## Joomla Environment +- **Joomla Version**: [e.g., 4.4.0, 5.0.0] +- **PHP Version**: [e.g., 8.1.0] +- **Database**: [MySQL / PostgreSQL / MariaDB] +- **Database Version**: [e.g., 8.0] +- **Server**: [Apache / Nginx / IIS] +- **Hosting**: [Shared / VPS / Dedicated / Cloud] + +## Issue Description +Provide a clear and detailed description of the issue. + +## Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Configure '...' +4. See error + +## Expected Behavior +What you expected to happen. + +## Actual Behavior +What actually happened. + +## Error Messages +``` +# Paste any error messages from Joomla error logs +# Location: administrator/logs/error.php +``` + +## Browser Console Errors +```javascript +// Paste any JavaScript console errors (F12 in browser) +``` + +## Screenshots +Add screenshots to help explain the issue. + +## Configuration +```ini +# Paste extension configuration (sanitize sensitive data) +``` + +## Installed Extensions +List other installed extensions that might conflict: +- Extension 1 (version) +- Extension 2 (version) + +## Template Overrides +- [ ] Using template overrides +- [ ] Custom CSS +- [ ] Custom JavaScript + +## Additional Context +- **Multilingual Site**: [Yes / No] +- **Cache Enabled**: [Yes / No] +- **Debug Mode**: [Yes / No] +- **SEF URLs**: [Yes / No] + +## Checklist +- [ ] I have cleared Joomla cache +- [ ] I have disabled other extensions to test for conflicts +- [ ] I have checked Joomla error logs +- [ ] I have tested with a default Joomla template +- [ ] I have checked browser console for JavaScript errors +- [ ] I have searched for similar issues +- [ ] I am using a supported Joomla version diff --git a/.mokogitea/ISSUE_TEMPLATE/question.md b/.mokogitea/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..3175013 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/question.md @@ -0,0 +1,82 @@ +--- +name: Question +about: Ask a question about usage, features, or best practices +title: '[QUESTION] ' +labels: ['question'] +assignees: ['jmiller'] +--- + + +## Question + +**Your question:** + + +## Context + +**What are you trying to accomplish?** + + +**What have you already tried?** + + +**Category**: +- [ ] Script usage +- [ ] Configuration +- [ ] Workflow setup +- [ ] Documentation interpretation +- [ ] Best practices +- [ ] Integration +- [ ] Other: __________ + +## Environment (if relevant) + +**Your setup**: +- Operating System: +- Version: + +## What You've Researched + +**Documentation reviewed**: +- [ ] README.md +- [ ] Project documentation +- [ ] Other (specify): __________ + +**Similar issues/questions found**: +- # +- # + +## Expected Outcome + +**What result are you hoping for?** + + +## Code/Configuration Samples + +**Relevant code or configuration** (if applicable): + +```bash +# Your code here +``` + +## Additional Context + +**Any other relevant information:** + + +**Screenshots** (if helpful): + + +## Urgency + +- [ ] Urgent (blocking work) +- [ ] Normal (can work on other things meanwhile) +- [ ] Low priority (just curious) + +## Checklist + +- [ ] I have searched existing issues and discussions +- [ ] I have reviewed relevant documentation +- [ ] I have provided sufficient context +- [ ] I have included code/configuration samples if relevant +- [ ] This is a genuine question (not a bug report or feature request) diff --git a/.mokogitea/ISSUE_TEMPLATE/rfc.md b/.mokogitea/ISSUE_TEMPLATE/rfc.md new file mode 100644 index 0000000..6f09af7 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/rfc.md @@ -0,0 +1,126 @@ +--- +name: Request for Comments (RFC) +about: Propose a significant change for community discussion +title: '[RFC] ' +labels: 'rfc, discussion' +assignees: '' + +--- + + +## RFC Summary +One-paragraph summary of the proposal. + +## Motivation +Why are we doing this? What use cases does it support? What is the expected outcome? + +## Detailed Design +### Overview +Provide a detailed explanation of the proposed change. + +### API Changes (if applicable) +```php +// Before +function oldApi($param1) { } + +// After +function newApi($param1, $param2) { } +``` + +### User Experience Changes +Describe how users will interact with this change. + +### Implementation Approach +High-level implementation strategy. + +## Drawbacks +Why should we *not* do this? + +## Alternatives +What other designs have been considered? What is the impact of not doing this? + +### Alternative 1 +- Description +- Trade-offs + +### Alternative 2 +- Description +- Trade-offs + +## Adoption Strategy +How will existing users adopt this? Is this a breaking change? + +### Migration Guide +```bash +# Steps to migrate +``` + +### Deprecation Timeline +- **Announcement**: +- **Deprecation**: +- **Removal**: + +## Unresolved Questions +- Question 1 +- Question 2 + +## Future Possibilities +What future work does this enable? + +## Impact Assessment +### Performance +Expected performance impact. + +### Security +Security considerations and implications. + +### Compatibility +- **Backward Compatible**: [Yes / No] +- **Breaking Changes**: [List] + +### Maintenance +Long-term maintenance considerations. + +## Community Input +### Stakeholders +- [ ] Core team +- [ ] Module developers +- [ ] End users +- [ ] Enterprise customers + +### Feedback Period +**Duration**: [e.g., 2 weeks] +**Deadline**: [date] + +## Implementation Timeline +### Phase 1: Design +- [ ] RFC discussion +- [ ] Design finalization +- [ ] Approval + +### Phase 2: Implementation +- [ ] Core implementation +- [ ] Tests +- [ ] Documentation + +### Phase 3: Release +- [ ] Beta release +- [ ] Feedback collection +- [ ] Stable release + +## Success Metrics +How will we measure success? +- Metric 1 +- Metric 2 + +## References +- Related RFCs: +- External documentation: +- Prior art: + +## Open Questions for Community +1. Question 1? +2. Question 2? + +--- +**Note**: This RFC is open for community discussion. Please provide feedback in the comments below. diff --git a/.mokogitea/ISSUE_TEMPLATE/security.md b/.mokogitea/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..f57b284 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/security.md @@ -0,0 +1,51 @@ +--- +name: Security Vulnerability Report +about: Report a security vulnerability (use only for non-critical issues) +title: '[SECURITY] ' +labels: 'security' +assignees: '' + +--- + + +## ⚠️ IMPORTANT: Private Disclosure Required + +**For critical security vulnerabilities, DO NOT use this template.** +Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure. + +Use this template only for: +- Security improvements +- Non-critical security suggestions +- Security documentation updates + +--- + +## Security Issue + +**Severity**: + + +## Description + + +## Affected Components + + +## Suggested Mitigation + + +## Standards Reference +Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)? +- [ ] SPDX license identifiers +- [ ] Secret management +- [ ] Dependency security +- [ ] Access control +- [ ] Other: [specify] + +## Additional Context + + +## Checklist +- [ ] This is NOT a critical vulnerability requiring private disclosure +- [ ] I have reviewed the SECURITY.md policy +- [ ] I have provided sufficient detail for evaluation diff --git a/.mokogitea/ISSUE_TEMPLATE/version.md b/.mokogitea/ISSUE_TEMPLATE/version.md new file mode 100644 index 0000000..6328421 --- /dev/null +++ b/.mokogitea/ISSUE_TEMPLATE/version.md @@ -0,0 +1,24 @@ +--- +name: Version Bump +about: Request or track a version change +title: '[VERSION] ' +labels: 'version, type: version' +assignees: 'jmiller' +--- + +## Version Change + +**Current version**: +**Requested version**: +**Change type**: + +## Reason + + + +## Checklist + +- [ ] README.md `VERSION:` field updated +- [ ] CHANGELOG.md entry added +- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: ``) +- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow diff --git a/.gitea/workflows/auto-release.yml b/.mokogitea/auto-release.yml similarity index 80% rename from .gitea/workflows/auto-release.yml rename to .mokogitea/auto-release.yml index 0bc7775..279bc5e 100644 --- a/.gitea/workflows/auto-release.yml +++ b/.mokogitea/auto-release.yml @@ -151,13 +151,22 @@ jobs: sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" fi + # Promote [Unreleased] section in CHANGELOG.md to new version + if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then + sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "2i ## [Unreleased]" CHANGELOG.md + sed -i "3i \\ " CHANGELOG.md + echo "CHANGELOG promoted to [${VERSION}]" + fi + # Commit and push git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" git add -A git diff --cached --quiet || { - git commit -m "chore(version): bump ${CURRENT} → ${VERSION} (minor) [skip ci]" + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" git push origin HEAD:main 2>&1 } @@ -311,6 +320,7 @@ jobs: # -- STEP 5: Write updates.xml (Joomla update server) --------------------- - name: "Step 5: Write updates.xml" + id: updates if: >- steps.version.outputs.skip != 'true' && steps.check.outputs.already_released != 'true' @@ -334,20 +344,44 @@ jobs: TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini + if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then + INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" + fi + # Fallbacks [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" [ -z "$EXT_TYPE" ] && EXT_TYPE="component" # Derive element if not in manifest: - # 1. Try XML filename (e.g. mokowaas.xml → mokowaas) - # 2. Fall back to repo name (lowercased) + # 1. plugin="xxx" attribute (plugins) + # 2. module="xxx" attribute (modules) + # 3. XML filename (components, packages) + # 4. Repo name fallback (templates, anything else) if [ -z "$EXT_ELEMENT" ]; then - EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') # If filename is generic (templateDetails, manifest), use repo name - case "$EXT_ELEMENT" in - templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + case "$FNAME" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + *) EXT_ELEMENT="$FNAME" ;; esac fi + # Final fallback + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + + # Save for Steps 7, 8, 8b + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" + echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" # Build client tag: plugins and frontend modules need site CLIENT_TAG="" @@ -374,7 +408,18 @@ jobs: PHP_TAG="${PHP_MINIMUM}" fi - DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${EXT_ELEMENT}-${VERSION}.zip" + # Build TYPE_PREFIX for download URL + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" # -- Build update entry for a given stability tag @@ -469,21 +514,32 @@ jobs: MAJOR="${{ steps.version.outputs.major }}" API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - # Auto-detect extension element for release naming - MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) - EXT_ELEMENT="" - if [ -n "$MANIFEST" ]; then - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) - [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') - case "$EXT_ELEMENT" in templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; esac - else + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) [ -z "$NOTES" ] && NOTES="Release ${VERSION}" - RELEASE_NAME="${EXT_ELEMENT} ${VERSION} (stable)" + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" # Delete existing release if present (overwrite, not append) EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ @@ -533,9 +589,28 @@ jobs: MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) [ -z "$MANIFEST" ] && exit 0 - EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml) - ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip" - TAR_NAME="${EXT_ELEMENT}-${VERSION}.tar.gz" + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" # -- Build install packages from src/ ---------------------------- SOURCE_DIR="src" @@ -675,6 +750,73 @@ jobs: echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + # -- STEP 8b: Update release description with changelog + SHA ---------------- + - name: "Step 8b: Update release body with changelog and SHA" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Build TYPE_PREFIX to match Step 8's ZIP naming + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # Get SHA from the built files + SHA256_ZIP="" + [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR="" + [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # Extract latest changelog entry (strip the ## header to avoid duplicate) + CHANGELOG="" + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') + [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) + fi + + # Build release body (single header, no duplicate from changelog) + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" + if [ -n "$CHANGELOG" ]; then + BODY="${BODY}${CHANGELOG}\n\n" + fi + BODY="${BODY}---\n\n### Checksums\n\n" + BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" + [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" + [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" + + # Get release ID and update body + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + python3 -c " + import json, urllib.request + body = '''$(printf '%b' "$BODY")''' + data = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=data, + headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, + method='PATCH' + ) + urllib.request.urlopen(req) + " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY + fi + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- - name: "Step 9: Mirror release to GitHub" if: >- @@ -764,6 +906,26 @@ jobs: done echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY + # -- STEP 11: Reset dev branch from main ------------------------------------ + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + # -- Summary -------------------------------------------------------------- - name: Pipeline Summary if: always() diff --git a/.gitea/workflows/cascade-dev.yml b/.mokogitea/cascade-dev.yml similarity index 100% rename from .gitea/workflows/cascade-dev.yml rename to .mokogitea/cascade-dev.yml diff --git a/.gitea/workflows/ci-joomla.yml b/.mokogitea/ci-joomla.yml similarity index 84% rename from .gitea/workflows/ci-joomla.yml rename to .mokogitea/ci-joomla.yml index 17284d1..28cee48 100644 --- a/.gitea/workflows/ci-joomla.yml +++ b/.mokogitea/ci-joomla.yml @@ -375,3 +375,76 @@ jobs: else echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT diff --git a/.gitea/workflows/cleanup.yml b/.mokogitea/cleanup.yml similarity index 100% rename from .gitea/workflows/cleanup.yml rename to .mokogitea/cleanup.yml diff --git a/.gitea/workflows/deploy-manual.yml b/.mokogitea/deploy-manual.yml similarity index 100% rename from .gitea/workflows/deploy-manual.yml rename to .mokogitea/deploy-manual.yml diff --git a/.mokogitea/dispatch-css-sync.yml b/.mokogitea/dispatch-css-sync.yml new file mode 100644 index 0000000..fe233e7 --- /dev/null +++ b/.mokogitea/dispatch-css-sync.yml @@ -0,0 +1,111 @@ +# When MokoOnyx CSS changes hit main: +# 1. Sync base CSS to Template-Client-WaaS (the single source for clients) +# 2. If new CSS variables were added, create issues on individual client repos +name: Sync CSS to Client Template + +on: + push: + branches: [main] + paths: + - 'src/media/templates/site/mokoonyx/css/**' + - 'media/templates/site/mokoonyx/css/**' + +permissions: + contents: read + +jobs: + sync: + name: Sync to Template and Notify Clients + runs-on: ubuntu-latest + steps: + - name: Checkout MokoOnyx + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Sync CSS to Template-Client-WaaS + env: + GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${{ github.server_url }}/api/v1" + AUTH="Authorization: token ${GITEA_TOKEN}" + TEMPLATE="MokoConsulting/Template-Client-WaaS" + + CSS_DIR="src/media/templates/site/mokoonyx/css" + [ ! -d "$CSS_DIR" ] && CSS_DIR="media/templates/site/mokoonyx/css" + + # Sync base CSS files only (user.css and *.custom.css are client-owned) + find "$CSS_DIR" -name "*.css" -not -name "user.css" -not -name "*.custom.css" | while read -r file; do + rel_path="src/media/templates/site/mokoonyx/css/${file#${CSS_DIR}/}" + content_b64=$(base64 -w0 "$file") + sha=$(curl -sf -H "$AUTH" "${API}/repos/${TEMPLATE}/contents/${rel_path}" | jq -r '.sha // empty') + + if [ -n "$sha" ]; then + curl -sf -X PUT -H "$AUTH" -H "Content-Type: application/json" \ + "${API}/repos/${TEMPLATE}/contents/${rel_path}" \ + -d "{\"content\": \"${content_b64}\", \"sha\": \"${sha}\", \"message\": \"chore: sync CSS from MokoOnyx\"}" \ + -o /dev/null && echo "Updated: ${rel_path}" + else + curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \ + "${API}/repos/${TEMPLATE}/contents/${rel_path}" \ + -d "{\"content\": \"${content_b64}\", \"message\": \"chore: sync CSS from MokoOnyx\"}" \ + -o /dev/null && echo "Created: ${rel_path}" + fi + done + + - name: Extract all CSS variables from MokoOnyx base + id: vars + env: + GITEA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${{ github.server_url }}/api/v1" + AUTH="Authorization: token ${GITEA_TOKEN}" + + CSS_DIR="src/media/templates/site/mokoonyx/css" + [ ! -d "$CSS_DIR" ] && CSS_DIR="media/templates/site/mokoonyx/css" + + # Get ALL variables defined in MokoOnyx base CSS (excluding custom files) + ALL_VARS=$(find "$CSS_DIR" -name "*.css" -not -name "*.custom.css" -not -name "user.css" -exec grep -ohE '\-\-[a-z][a-z0-9-]+' {} \; | sort -u) + echo "$ALL_VARS" > /tmp/all_vars.txt + echo "Total base variables: $(wc -l < /tmp/all_vars.txt)" + + # Check each client repo for missing variables + CLIENTS=( + "ClarksvilleFurs/client-waas-clarksvillefurs" + "KiddieLand/client-waas-kiddieland" + "VexCreations/client-waas-vexcreations" + ) + + for repo in "${CLIENTS[@]}"; do + echo "=== Checking ${repo} ===" + MISSING="" + + for theme in "dark" "light"; do + FILE_PATH="src/media/templates/site/mokoonyx/css/theme/${theme}.custom.css" + CLIENT_CSS=$(curl -sf -H "$AUTH" "${API}/repos/${repo}/contents/${FILE_PATH}" | jq -r '.content // empty' | base64 -d 2>/dev/null || echo "") + + if [ -z "$CLIENT_CSS" ]; then + MISSING="$MISSING\nAll variables missing from ${theme}.custom.css (file not found)" + continue + fi + + # Find variables in base that are NOT in client custom file + while read -r var; do + [ -z "$var" ] && continue + if ! echo "$CLIENT_CSS" | grep -qF "$var"; then + MISSING="$MISSING\n- \`${var}\` missing from ${theme}.custom.css" + fi + done < /tmp/all_vars.txt + done + + if [ -n "$MISSING" ]; then + BODY="Your theme custom files are missing CSS variables defined in MokoOnyx base.\n\n## Missing Variables\n${MISSING}\n\n## Action\n\nAdd these variables to your \`dark.custom.css\` and/or \`light.custom.css\` with appropriate values for your theme.\n\nBase CSS reference: ${{ github.server_url }}/MokoConsulting/MokoOnyx/src/branch/main/src/media/templates/site/mokoonyx/css" + + curl -sf -X POST -H "$AUTH" -H "Content-Type: application/json" \ + "${API}/repos/${repo}/issues" \ + -d "$(jq -n --arg t "chore: CSS variables out of sync with MokoOnyx" --arg b "$BODY" '{title:$t,body:$b}')" \ + -o /dev/null && echo "Issue created: ${repo}" + else + echo " All variables present" + fi + done diff --git a/.mokogitea/gitleaks.yml b/.mokogitea/gitleaks.yml new file mode 100644 index 0000000..b29f881 --- /dev/null +++ b/.mokogitea/gitleaks.yml @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/gitleaks.yml.template +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: Secret Scanning + +on: + pull_request: + branches: + - main + - 'dev/**' + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + gitleaks: + name: Gitleaks Secret Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Gitleaks + run: | + GITLEAKS_VERSION="8.21.2" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true diff --git a/.gitea/workflows/notify.yml b/.mokogitea/notify.yml similarity index 100% rename from .gitea/workflows/notify.yml rename to .mokogitea/notify.yml diff --git a/.mokogitea/pr-branch-check.yml b/.mokogitea/pr-branch-check.yml new file mode 100644 index 0000000..b8d9742 --- /dev/null +++ b/.mokogitea/pr-branch-check.yml @@ -0,0 +1,90 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Enforces branch merge policy: +# feature/* → dev only +# fix/* → dev only +# hotfix/* → dev or main (emergency) +# dev → main only +# alpha/* → dev only +# beta/* → dev only +# rc/* → main only + +name: Branch Policy Check + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + check-target: + name: Verify merge target + runs-on: ubuntu-latest + steps: + - name: Check branch policy + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/pr-check.yml b/.mokogitea/pr-check.yml similarity index 100% rename from .gitea/workflows/pr-check.yml rename to .mokogitea/pr-check.yml diff --git a/.gitea/workflows/pre-release.yml b/.mokogitea/pre-release.yml similarity index 92% rename from .gitea/workflows/pre-release.yml rename to .mokogitea/pre-release.yml index 4969383..30c9bcf 100644 --- a/.gitea/workflows/pre-release.yml +++ b/.mokogitea/pre-release.yml @@ -278,7 +278,7 @@ jobs: f.write(content) PYEOF - # Commit and push + # Commit and push to current branch if ! git diff --quiet updates.xml 2>/dev/null; then git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.name "gitea-actions[bot]" @@ -287,6 +287,28 @@ jobs: git push origin HEAD 2>&1 || echo "WARNING: push failed" fi + - name: "Sync updates.xml to all branches" + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + # Sync updates.xml to main and dev (whichever isn't current) + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + + echo "Syncing updates.xml → ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + - name: "Delete lesser pre-release channels (cascade)" continue-on-error: true run: | diff --git a/.gitea/workflows/repo-health.yml b/.mokogitea/repo-health.yml similarity index 100% rename from .gitea/workflows/repo-health.yml rename to .mokogitea/repo-health.yml diff --git a/.gitea/workflows/security-audit.yml b/.mokogitea/security-audit.yml similarity index 100% rename from .gitea/workflows/security-audit.yml rename to .mokogitea/security-audit.yml diff --git a/.gitea/workflows/update-server.yml b/.mokogitea/update-server.yml similarity index 100% rename from .gitea/workflows/update-server.yml rename to .mokogitea/update-server.yml diff --git a/.mokogitea/workflows/auto-release.yml b/.mokogitea/workflows/auto-release.yml new file mode 100644 index 0000000..84fc701 --- /dev/null +++ b/.mokogitea/workflows/auto-release.yml @@ -0,0 +1,1007 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/universal/auto-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Universal build & release � detects platform from manifest.xml +# +# +========================================================================+ +# | UNIVERSAL BUILD & RELEASE PIPELINE | +# +========================================================================+ +# | | +# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. | +# | | +# | Platform-specific: | +# | joomla: XML manifest, updates.xml, type-prefixed packages | +# | dolibarr: mod*.class.php, update.txt, dev version reset | +# | generic: README-only, no update stream | +# | | +# +========================================================================+ + +name: "Universal: Build & Release" + +on: + pull_request: + types: [closed] + branches: + - main + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + release: + name: Build & Release Pipeline + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}' + run: | + # Ensure PHP + Composer are available + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + cd /tmp/mokostandards-api + composer install --no-dev --no-interaction --quiet + + + # -- PLATFORM DETECTION --------------------------------------------------- + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + echo "Platform detected: ${PLATFORM}" + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + # -- STEP 1: Read version ----------------------------------------------- + - name: "Step 1: Read version from README.md" + id: version + run: | + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null) + if [ -z "$VERSION" ]; then + echo "No VERSION in README.md — skipping release" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # Derive major.minor for branch naming (patches update existing branch) + MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') + PATCH=$(echo "$VERSION" | awk -F. '{print $3}') + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}') + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" + echo "minor=$MINOR" >> "$GITHUB_OUTPUT" + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" + echo "release_tag=stable" >> "$GITHUB_OUTPUT" + echo "stability=stable" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then + echo "is_minor=true" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (first release for this minor — full pipeline)" + else + echo "is_minor=false" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION (patch — platform version + badges only)" + fi + + # -- STEP 1b: Bump minor version (stable = minor bump, reset patch) ------ + - name: "Step 1b: Bump minor version for stable release" + if: steps.version.outputs.skip != 'true' + id: bump + run: | + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && { echo "skip=true" >> "$GITHUB_OUTPUT"; exit 0; } + + MAJOR=$((10#$(echo "$CURRENT" | cut -d. -f1))) + MINOR=$((10#$(echo "$CURRENT" | cut -d. -f2))) + + # Minor bump, reset patch. Rollover if minor > 99 + MINOR=$((MINOR + 1)) + if [ $MINOR -gt 99 ]; then + MINOR=0 + MAJOR=$((MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.00" $MAJOR $MINOR) + TODAY=$(date +%Y-%m-%d) + + echo "Stable bump: ${CURRENT} → ${VERSION} (minor)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update platform-specific manifest + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + [ -n "$MANIFEST_VER" ] && sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE" + fi + echo "${VERSION}" > update.txt + ;; + *) ;; + esac + + # Promote [Unreleased] section in CHANGELOG.md to new version + if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then + sed -i "s|## \[Unreleased\]|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "s|## Unreleased|## [${VERSION}] --- ${TODAY}|" CHANGELOG.md + sed -i "2i ## [Unreleased]" CHANGELOG.md + sed -i "3i \\ " CHANGELOG.md + echo "CHANGELOG promoted to [${VERSION}]" + fi + + # Commit and push + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD:main 2>&1 + } + + # Override version output for rest of pipeline + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "major=$(printf "%02d" $MAJOR)" >> "$GITHUB_OUTPUT" + + - name: Check if already released + if: steps.version.outputs.skip != 'true' + id: check + run: | + TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + + TAG_EXISTS=false + BRANCH_EXISTS=false + + git rev-parse "$TAG" >/dev/null 2>&1 && TAG_EXISTS=true + git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH" && BRANCH_EXISTS=true + + echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" + + # Tag and branch may persist across patch releases — never skip + echo "already_released=false" >> "$GITHUB_OUTPUT" + + # -- SANITY CHECKS ------------------------------------------------------- + - name: "Sanity: Pre-release validation" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + ERRORS=0 + + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # -- Version drift check (must pass before release) -------- + README_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + if [ "$README_VER" != "$VERSION" ]; then + echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check CHANGELOG version matches + CL_VER=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' CHANGELOG.md 2>/dev/null | head -1) + if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then + echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + + # Check composer.json version if present + if [ -f "composer.json" ]; then + COMP_VER=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' composer.json 2>/dev/null | head -1) + if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then + echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + fi + + # Common checks + if [ ! -f "LICENSE" ]; then + echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY + fi + + if [ ! -d "src" ] && [ ! -d "htdocs" ]; then + echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY + else + echo "- Source directory present" >> $GITHUB_STEP_SUMMARY + fi + + # -- Platform-specific checks -------- + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + XML_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then + echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null) + echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY + else + echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY + fi ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1) + if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then + echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + else + echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi + if [ ! -f "update.txt" ]; then + echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS+1)) + fi ;; + *) echo "- Generic platform � no manifest checks" >> $GITHUB_STEP_SUMMARY ;; + esac + + echo "" >> $GITHUB_STEP_SUMMARY + if [ "$ERRORS" -gt 0 ]; then + echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY + else + echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 2: Create or update version/XX.YY archive branch --------------- + # Always runs — every version change on main archives to version/XX.YY + - name: "Step 2: Version archive branch" + if: steps.check.outputs.already_released != 'true' + run: | + BRANCH="${{ steps.version.outputs.branch }}" + IS_MINOR="${{ steps.version.outputs.is_minor }}" + PATCH="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}') + + # Check if branch exists + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + git push origin HEAD:"$BRANCH" --force + echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY + else + git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" + git push origin "$BRANCH" --force + echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 3: Set platform version ---------------------------------------- + - name: "Step 3: Set platform version" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + php /tmp/mokostandards-api/cli/version_set_platform.php \ + --path . --version "$VERSION" --branch main + + # -- STEP 4: Update version badges ---------------------------------------- + - name: "Step 4: Update version badges" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + find . -name "*.md" ! -path "./.git/*" ! -path "./vendor/*" | while read -r f; do + if grep -q '\[VERSION:' "$f" 2>/dev/null; then + sed -i "s/\[VERSION:[[:space:]]*[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{2\}\]/[VERSION: ${VERSION}]/" "$f" + fi + done + + # -- STEP 5: Write updates.xml (Joomla update server) --------------------- + - name: "Step 5: Write update stream" + id: updates + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + REPO="${{ github.repository }}" + + # -- Parse extension metadata from XML manifest ---------------- + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "Warning: No Joomla XML manifest found — skipping updates.xml" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Extract fields using sed (portable — no grep -P) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # If EXT_NAME is a language key (e.g. PLG_SYSTEM_MOKOJGDPC), resolve from .ini + if echo "$EXT_NAME" | grep -qE '^[A-Z_]+$'; then + INI_NAME=$(find . -name "*.sys.ini" -path "*/en-GB/*" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -z "$INI_NAME" ] && INI_NAME=$(find . -name "*.sys.ini" -exec grep -h "^${EXT_NAME}=" {} \; 2>/dev/null | head -1 | cut -d'"' -f2) + [ -n "$INI_NAME" ] && EXT_NAME="$INI_NAME" + fi + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: + # 1. plugin="xxx" attribute (plugins) + # 2. module="xxx" attribute (modules) + # 3. XML filename (components, packages) + # 4. Repo name fallback (templates, anything else) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*module="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + fi + if [ -z "$EXT_ELEMENT" ]; then + FNAME=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + # If filename is generic (templateDetails, manifest), use repo name + case "$FNAME" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + *) EXT_ELEMENT="$FNAME" ;; + esac + fi + # Final fallback + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + + # Save for Steps 7, 8, 8b + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "ext_name=${EXT_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_type=${EXT_TYPE}" >> "$GITHUB_OUTPUT" + echo "ext_folder=${EXT_FOLDER}" >> "$GITHUB_OUTPUT" + + # Build client tag: plugins and frontend modules need site + CLIENT_TAG="" + if [ -n "$EXT_CLIENT" ]; then + CLIENT_TAG="${EXT_CLIENT}" + elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then + CLIENT_TAG="site" + fi + + # Build folder tag for plugins (required for Joomla to match the update) + FOLDER_TAG="" + if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then + FOLDER_TAG="${EXT_FOLDER}" + fi + + # Build targetplatform (fallback to Joomla 5 if not in manifest) + if [ -z "$TARGET_PLATFORM" ]; then + TARGET_PLATFORM=$(printf '' "/") + fi + + # Build php_minimum tag + PHP_TAG="" + if [ -n "$PHP_MINIMUM" ]; then + PHP_TAG="${PHP_MINIMUM}" + fi + + # Build TYPE_PREFIX for download URL + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" + + # -- Build update entry for a given stability tag + build_entry() { + local TAG_NAME="$1" + printf '%s\n' ' ' + printf '%s\n' " ${EXT_NAME}" + printf '%s\n' " ${EXT_NAME} update" + printf '%s\n' " ${EXT_ELEMENT}" + printf '%s\n' " ${EXT_TYPE}" + printf '%s\n' " ${VERSION}" + [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" + [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" + printf '%s\n' " ${TAG_NAME}" + printf '%s\n' " ${INFO_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${DOWNLOAD_URL}" + printf '%s\n' ' ' + printf '%s\n' " ${TARGET_PLATFORM}" + [ -n "$PHP_TAG" ] && printf '%s\n' " ${PHP_TAG}" + printf '%s\n' ' Moko Consulting' + printf '%s\n' ' https://mokoconsulting.tech' + printf '%s\n' ' ' + } + + # -- Write updates.xml with cascading channels + # Stable release updates ALL channels (development, alpha, beta, rc, stable) + { + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' "" + printf '%s\n' '' + build_entry "development" + build_entry "alpha" + build_entry "beta" + build_entry "rc" + build_entry "stable" + printf '%s\n' '' + } > updates.xml + + echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY + + # -- Commit all changes --------------------------------------------------- + - name: Commit release changes + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.already_released != 'true' + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + # Set push URL with token for branch-protected repos + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git commit -m "chore(release): build ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " + git push -u origin HEAD + + # -- STEP 6: Create tag --------------------------------------------------- + - name: "Step 6: Create git tag" + if: >- + steps.version.outputs.skip != 'true' && + steps.check.outputs.tag_exists != 'true' && + steps.version.outputs.is_minor == 'true' + run: | + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + # Only create the major release tag if it doesn't exist yet + if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git tag "$RELEASE_TAG" + git push origin "$RELEASE_TAG" + echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + else + echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY + fi + echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 7: Create or update Gitea Release -------------------------------- + - name: "Step 7: Gitea Release" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + BRANCH="${{ steps.version.outputs.branch }}" + MAJOR="${{ steps.version.outputs.major }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Reuse metadata from Step 5 (single source of truth) + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_NAME="${{ steps.updates.outputs.ext_name }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Fallbacks if Step 5 was skipped + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + [ -z "$EXT_NAME" ] && EXT_NAME="${GITEA_REPO}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + + # Build release name: "Pretty Name VERSION (type_element-VERSION)" + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + RELEASE_NAME="${EXT_NAME} ${VERSION} (${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION})" + + # Delete existing release if present (overwrite, not append) + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true) + + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/tags/${RELEASE_TAG}" 2>/dev/null || true + echo "Deleted previous stable release (id: ${EXISTING_ID})" + fi + + # Create fresh release + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_NAME}', + 'body': '''## ${VERSION} ($(date +%Y-%m-%d))\n${NOTES}''', + 'target_commitish': '${BRANCH}' + }))")" + echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ + - name: "Step 8: Build package and update checksum" + if: >- + steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # All ZIPs upload to the major release tag (vXX) + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + echo "No release ${RELEASE_TAG} found — skipping ZIP upload" + exit 0 + fi + + # Find extension element name from manifest + MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '/dev/null | head -1 || true) + [ -z "$MANIFEST" ] && exit 0 + + # Reuse element from Step 5, with same fallback chain + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(sed -n 's/.*plugin="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + [ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + # ZIP name: type_folder_element-VERSION (e.g. plg_system_mokojgdpc-01.01.00.zip) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # -- Build install packages from src/ ---------------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; } + + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + + # ZIP package + cd "$SOURCE_DIR" + zip -r "/tmp/${ZIP_NAME}" . -x $EXCLUDES + cd .. + + # tar.gz package + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + ZIP_SIZE=$(stat -c%s "/tmp/${ZIP_NAME}" 2>/dev/null || stat -f%z "/tmp/${ZIP_NAME}" 2>/dev/null || echo "unknown") + TAR_SIZE=$(stat -c%s "/tmp/${TAR_NAME}" 2>/dev/null || stat -f%z "/tmp/${TAR_NAME}" 2>/dev/null || echo "unknown") + + # -- Calculate SHA-256 for both ---------------------------------- + SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # -- Delete existing assets with same name before uploading ------ + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_NAME}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # -- Upload both to release tag ---------------------------------- + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${ZIP_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + + # -- Update updates.xml with both download formats --------------- + if [ -f "updates.xml" ]; then + ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" + TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" + + # Use Python to update only the stable entry's downloads + sha256 + export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" + python3 << 'PYEOF' + import re, os + + with open("updates.xml") as f: + content = f.read() + + zip_url = os.environ["PY_ZIP_URL"] + tar_url = os.environ["PY_TAR_URL"] + sha = os.environ["PY_SHA"] + + # Find the stable update block and replace its downloads + sha256 + def replace_stable(m): + block = m.group(0) + # Replace downloads block + new_downloads = ( + " \n" + f" {zip_url}\n" + " " + ) + block = re.sub(r' .*?', new_downloads, block, flags=re.DOTALL) + # Add or replace sha256 + if '' in block: + block = re.sub(r' .*?', f' {sha}', block) + else: + block = block.replace('', f'\n {sha}') + return block + + content = re.sub( + r' .*?stable.*?', + replace_stable, + content, + flags=re.DOTALL + ) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + CURRENT_BRANCH="${{ github.ref_name }}" + git add updates.xml + git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " || true + git push || true + + # Sync updates.xml to main via direct API (always runs — may be on version/XX branch) + GA_TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') + + if [ -n "$FILE_SHA" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/contents/updates.xml" \ + -d "$(jq -n \ + --arg content "$CONTENT" \ + --arg sha "$FILE_SHA" \ + --arg msg "chore: sync updates.xml ${VERSION} [skip ci]" \ + --arg branch "main" \ + '{content: $content, sha: $sha, message: $msg, branch: $branch}' + )" > /dev/null 2>&1 \ + && echo "updates.xml synced to main via API" \ + || echo "WARNING: failed to sync updates.xml to main" + else + echo "WARNING: could not get updates.xml SHA from main" + fi + fi + + echo "### Packages" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY + echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY + echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY + echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY + + # -- STEP 8b: Update release description with changelog + SHA ---------------- + - name: "Step 8b: Update release body with changelog and SHA" + if: steps.version.outputs.skip != 'true' + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + EXT_ELEMENT="${{ steps.updates.outputs.ext_element }}" + EXT_TYPE="${{ steps.updates.outputs.ext_type }}" + EXT_FOLDER="${{ steps.updates.outputs.ext_folder }}" + + # Build TYPE_PREFIX to match Step 8's ZIP naming + TYPE_PREFIX="" + case "${EXT_TYPE}" in + plugin) TYPE_PREFIX="plg_${EXT_FOLDER}_" ;; + module) TYPE_PREFIX="mod_" ;; + component) TYPE_PREFIX="com_" ;; + template) TYPE_PREFIX="tpl_" ;; + library) TYPE_PREFIX="lib_" ;; + package) TYPE_PREFIX="pkg_" ;; + esac + ZIP_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.zip" + TAR_NAME="${TYPE_PREFIX}${EXT_ELEMENT}-${VERSION}.tar.gz" + + # Get SHA from the built files + SHA256_ZIP="" + [ -f "/tmp/${ZIP_NAME}" ] && SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) + SHA256_TAR="" + [ -f "/tmp/${TAR_NAME}" ] && SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) + + # Extract latest changelog entry (strip the ## header to avoid duplicate) + CHANGELOG="" + if [ -f "CHANGELOG.md" ]; then + CHANGELOG=$(sed -n "/^## \[*${VERSION}/,/^## \[*[0-9]/p" CHANGELOG.md | sed '$d' | sed '1d') + [ -z "$CHANGELOG" ] && CHANGELOG=$(sed -n '/^## /,/^## /p' CHANGELOG.md | sed '$d' | sed '1d' | head -30) + fi + + # Build release body (single header, no duplicate from changelog) + BODY="## ${VERSION} ($(date +%Y-%m-%d))\n\n" + if [ -n "$CHANGELOG" ]; then + BODY="${BODY}${CHANGELOG}\n\n" + fi + BODY="${BODY}---\n\n### Checksums\n\n" + BODY="${BODY}| File | SHA-256 |\n|------|--------|\n" + [ -n "$SHA256_ZIP" ] && BODY="${BODY}| \`${ZIP_NAME}\` | \`${SHA256_ZIP}\` |\n" + [ -n "$SHA256_TAR" ] && BODY="${BODY}| \`${TAR_NAME}\` | \`${SHA256_TAR}\` |\n" + + # Get release ID and update body + RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + python3 -c " + import json, urllib.request + body = '''$(printf '%b' "$BODY")''' + data = json.dumps({'body': body}).encode() + req = urllib.request.Request( + '${API_BASE}/releases/${RELEASE_ID}', + data=data, + headers={'Authorization': 'token ${{ secrets.GA_TOKEN }}', 'Content-Type': 'application/json'}, + method='PATCH' + ) + urllib.request.urlopen(req) + " 2>/dev/null && echo "Release body updated with changelog + SHA" >> $GITHUB_STEP_SUMMARY + fi + + # -- STEP 9: Mirror to GitHub (stable only) -------------------------------- + - name: "Step 9: Mirror release to GitHub" + if: >- + steps.version.outputs.skip != 'true' && + steps.version.outputs.stability == 'stable' && + secrets.GH_TOKEN != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + RELEASE_TAG="${{ steps.version.outputs.release_tag }}" + MAJOR="${{ steps.version.outputs.major }}" + BRANCH="${{ steps.version.outputs.branch }}" + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + + NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true) + [ -z "$NOTES" ] && NOTES="Release ${VERSION}" + echo "$NOTES" > /tmp/release_notes.md + + EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true) + + if [ -z "$EXISTING" ]; then + gh release create "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" \ + --notes-file /tmp/release_notes.md \ + --target "$BRANCH" || true + else + gh release edit "$RELEASE_TAG" \ + --repo "$GH_REPO" \ + --title "v${MAJOR} (latest: ${VERSION})" || true + fi + + # Upload assets to GitHub mirror + for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do + if [ -f "$PKG" ]; then + _RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty") + [ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true + fi + done + echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY + + # -- STEP 10: Sync main branch to GitHub mirror ---------------------------- + - name: "Step 10: Push main to GitHub mirror" + if: >- + steps.version.outputs.skip != 'true' && + secrets.GH_TOKEN != '' + continue-on-error: true + run: | + GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}" + GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1) + GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2) + git remote add github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \ + git remote set-url github "https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" + git fetch origin main --depth=1 + git push github origin/main:refs/heads/main --force 2>/dev/null \ + && echo "main branch pushed to GitHub mirror" \ + || echo "WARNING: GitHub mirror push failed" + + # -- Clean up lesser pre-releases (cascade) --------------------------------- + # stable → deletes all | rc → beta,alpha,dev | beta → alpha,dev | alpha → dev + - name: "Delete lesser pre-release channels" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Stable deletes all pre-release channels + TAGS_TO_DELETE="development alpha beta release-candidate" + + DELETED=0 + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + DELETED=$((DELETED + 1)) + fi + done + echo "Cleaned up ${DELETED} pre-release channel(s)" >> $GITHUB_STEP_SUMMARY + + # -- STEP 11: Reset dev branch from main ------------------------------------ + - name: "Step 11: Delete and recreate dev branch from main" + if: steps.version.outputs.skip != 'true' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + + # Delete dev branch + curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch" + + # Recreate dev from main (now includes version bump + changelog promotion) + curl -sf -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/branches" \ + -d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main" + + echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY + + + # -- Dolibarr post-release: Reset dev version ----------------------------- + - name: "Dolibarr: Reset dev version" + if: >- + steps.version.outputs.skip != 'true' && + steps.platform.outputs.platform == 'dolibarr' && + steps.platform.outputs.mod_file != '' + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") + FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true) + FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true) + if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then + UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/") + ENCODED=$(echo "$UPDATED" | base64 -w0) + curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \ + -d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true + fi + + # -- Summary -------------------------------------------------------------- + - name: Pipeline Summary + if: always() + run: | + VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}" + PLATFORM="${{ steps.platform.outputs.platform }}" + if [ "${{ steps.version.outputs.skip }}" = "true" ]; then + echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY + echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then + echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml new file mode 100644 index 0000000..4dbb135 --- /dev/null +++ b/.mokogitea/workflows/cascade-dev.yml @@ -0,0 +1,213 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/cascade-dev.yml.template +# VERSION: 02.00.00 +# BRIEF: Forward-merge main → all open branches after every push to main +# +# +========================================================================+ +# | CASCADE MAIN → ALL BRANCHES | +# +========================================================================+ +# | | +# | Triggers on every push to main (PR merges, bot commits, etc.) | +# | | +# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | +# | 2. For each: create PR (main → branch), auto-merge if clean | +# | 3. On conflict: leave PR open for manual resolution | +# | | +# +========================================================================+ + +name: "Universal: Cascade Main → Dev" + +on: + push: + branches: + - main + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + pull-requests: write + +jobs: + cascade: + name: Cascade main → branches + runs-on: ubuntu-latest + if: >- + !contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, '[skip cascade]') + + steps: + - name: Discover target branches + id: branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + # Fetch all branches (paginated) + PAGE=1 + ALL_BRANCHES="" + while true; do + BATCH=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?page=${PAGE}&limit=50" \ + | jq -r '.[].name // empty') + [ -z "$BATCH" ] && break + ALL_BRANCHES="$ALL_BRANCHES $BATCH" + PAGE=$((PAGE + 1)) + done + + # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* + TARGETS="" + for BRANCH in $ALL_BRANCHES; do + case "$BRANCH" in + dev|dev/*|rc/*|beta/*|alpha/*) + TARGETS="$TARGETS $BRANCH" + ;; + esac + done + + TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace + + if [ -z "$TARGETS" ]; then + echo "targets=" >> "$GITHUB_OUTPUT" + echo "ℹ️ No cascade target branches found" + else + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + COUNT=$(echo "$TARGETS" | wc -w) + echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" + fi + + - name: Cascade to all target branches + if: steps.branches.outputs.targets != '' + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + SHORT_SHA="${GITHUB_SHA:0:7}" + TARGETS="${{ steps.branches.outputs.targets }}" + + SUCCESS=0 + CONFLICTS=0 + SKIPPED=0 + FAILED=0 + + for BRANCH in $TARGETS; do + echo "" + echo "═══ main → ${BRANCH} ═══" + + # Check if branch is already up to date + ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') + RESPONSE=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/compare/${ENCODED_BRANCH}...main") + + AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') + + if [ "$AHEAD" -eq 0 ]; then + echo " ✅ Already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " ℹ️ main is ${AHEAD} commit(s) ahead" + + # Check for existing cascade PR + EXISTING=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") + + EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + PR_NUMBER="" + + if [ "$EXISTING_COUNT" -gt 0 ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') + echo " ℹ️ Reusing existing PR #${PR_NUMBER}" + else + # Create cascade PR + PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", + \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", + \"head\": \"main\", + \"base\": \"${BRANCH}\" + }" \ + "${API}/pulls") + + HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) + BODY=$(echo "$PR_RESPONSE" | sed '$d') + PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') + + if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then + MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) + echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" + FAILED=$((FAILED + 1)) + continue + fi + + echo " ✅ Created PR #${PR_NUMBER}" + fi + + # Try auto-merge + PR_DATA=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls/${PR_NUMBER}") + + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') + + if [ "$MERGEABLE" != "true" ]; then + echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + continue + fi + + MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"Do\": \"merge\", + \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", + \"delete_branch_after_merge\": false + }" \ + "${API}/pulls/${PR_NUMBER}/merge") + + MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) + + if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then + echo " ✅ Merged — ${BRANCH} is in sync" + SUCCESS=$((SUCCESS + 1)) + else + MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') + echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + fi + done + + # Summary + echo "" + echo "════════════════════════════════════════" + echo " ✅ Merged: ${SUCCESS}" + echo " ⚠️ Conflicts: ${CONFLICTS}" + echo " ⏭️ Up to date: ${SKIPPED}" + echo " ❌ Failed: ${FAILED}" + echo "════════════════════════════════════════" + + if [ "$FAILED" -gt 0 ]; then + exit 1 + fi diff --git a/.mokogitea/workflows/ci-joomla.yml b/.mokogitea/workflows/ci-joomla.yml new file mode 100644 index 0000000..5c66f14 --- /dev/null +++ b/.mokogitea/workflows/ci-joomla.yml @@ -0,0 +1,450 @@ +# Copyright (C) 2026 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow.Template +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/ci-joomla.yml.template +# VERSION: 04.06.00 +# BRIEF: CI workflow for Joomla extensions — lint, validate, test + +name: "Joomla: Extension CI" + +on: + pull_request: + branches: + - main + - 'dev/**' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + lint-and-validate: + name: Lint & Validate + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: | + php -v && composer --version + + - name: Clone MokoStandards + env: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + run: | + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: PHP syntax check + run: | + ERRORS=0 + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + FOUND=1 + while IFS= read -r -d '' FILE; do + OUTPUT=$(php -l "$FILE" 2>&1) + if echo "$OUTPUT" | grep -q "Parse error"; then + echo "::error file=${FILE}::${OUTPUT}" + ERRORS=$((ERRORS + 1)) + fi + done < <(find "$DIR" -name "*.php" -print0) + fi + done + echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY + if [ "${ERRORS}" -gt 0 ]; then + echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY + fi + + - name: XML manifest validation + run: | + echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Find the extension manifest (XML with /dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found (XML file with \`> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Validate well-formed XML + php -r " + \$xml = @simplexml_load_file('$MANIFEST'); + if (\$xml === false) { + echo 'INVALID'; + exit(1); + } + echo 'VALID'; + " > /tmp/xml_result 2>&1 + XML_RESULT=$(cat /tmp/xml_result) + if [ "$XML_RESULT" != "VALID" ]; then + echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY + fi + + # Check required tags: name, version, author, namespace (Joomla 5+) + for TAG in name version author namespace; do + if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then + echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY + fi + done + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check language files referenced in manifest + run: | + echo "### Language File Check" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -n "$MANIFEST" ]; then + # Extract language file references from manifest + LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true) + if [ -z "$LANG_FILES" ]; then + echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY + else + while IFS= read -r LANG_FILE; do + LANG_FILE=$(echo "$LANG_FILE" | xargs) + if [ -z "$LANG_FILE" ]; then + continue + fi + # Check in common locations + FOUND=0 + for BASE in "." "src" "htdocs"; do + if [ -f "${BASE}/${LANG_FILE}" ]; then + FOUND=1 + break + fi + done + if [ "$FOUND" -eq 0 ]; then + echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY + fi + done <<< "$LANG_FILES" + fi + else + echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY + fi + + if [ "${ERRORS}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check index.html files in directories + run: | + echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY + MISSING=0 + CHECKED=0 + + for DIR in src/ htdocs/; do + if [ -d "$DIR" ]; then + while IFS= read -r -d '' SUBDIR; do + CHECKED=$((CHECKED + 1)) + if [ ! -f "${SUBDIR}/index.html" ]; then + echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY + MISSING=$((MISSING + 1)) + fi + done < <(find "$DIR" -type d -print0) + fi + done + + if [ "${CHECKED}" -eq 0 ]; then + echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY + elif [ "${MISSING}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY + fi + + release-readiness: + name: Release Readiness Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.base_ref == 'main' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Validate release readiness + run: | + echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + ERRORS=0 + + # Extract version from README.md + README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1) + if [ -z "$README_VERSION" ]; then + echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Find the extension manifest + MANIFEST="" + for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do + if grep -q "/dev/null; then + MANIFEST="$XML_FILE" + break + fi + done + + if [ -z "$MANIFEST" ]; then + echo "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY + + # Check matches README VERSION + MANIFEST_VERSION=$(grep -oP '\K[^<]+' "$MANIFEST" | head -1) + if [ -z "$MANIFEST_VERSION" ]; then + echo "No \`\` tag in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then + echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Check extension type, element, client attributes + EXT_TYPE=$(grep -oP ']*\btype="\K[^"]+' "$MANIFEST" | head -1) + if [ -z "$EXT_TYPE" ]; then + echo "Missing \`type\` attribute on \`\` tag." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + else + echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY + fi + + # Element check (component/module/plugin name) + HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_ELEMENT" -eq 0 ]; then + echo "Missing \`\` or \`\` in manifest." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Client attribute for site/admin modules and plugins + if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then + HAS_CLIENT=$(grep -cP ']*\bclient=' "$MANIFEST" 2>/dev/null || echo "0") + if [ "$HAS_CLIENT" -eq 0 ]; then + echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + fi + fi + + # Check updates.xml exists + if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then + echo "Update XML present." >> $GITHUB_STEP_SUMMARY + else + echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + # Check CHANGELOG.md exists + if [ -f "CHANGELOG.md" ]; then + echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY + else + echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY + ERRORS=$((ERRORS + 1)) + fi + + echo "" >> $GITHUB_STEP_SUMMARY + if [ $ERRORS -gt 0 ]; then + echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY + fi + + test: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + needs: lint-and-validate + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP ${{ matrix.php }} + run: | + php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install \ + --no-interaction \ + --prefer-dist \ + --optimize-autoloader + else + echo "No composer.json found — skipping dependency install" + fi + + - name: Run tests + run: | + echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY + if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then + vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log + EXIT=${PIPESTATUS[0]} + if [ $EXIT -eq 0 ]; then + echo "All tests passed." >> $GITHUB_STEP_SUMMARY + else + echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT + else + echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY + fi + + static-analysis: + name: PHPStan Analysis + runs-on: ubuntu-latest + needs: lint-and-validate + continue-on-error: true + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: php -v && composer --version + + - name: Install dependencies + env: + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + if [ -f "composer.json" ]; then + composer install --no-interaction --prefer-dist --optimize-autoloader + fi + + - name: Install PHPStan + run: | + if ! command -v vendor/bin/phpstan &> /dev/null; then + composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \ + composer global require phpstan/phpstan --no-interaction + fi + + - name: Run PHPStan + run: | + echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY + PHPSTAN="vendor/bin/phpstan" + if [ ! -f "$PHPSTAN" ]; then + PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan + fi + + # Determine source directory + SRC_DIR="" + for DIR in src/ htdocs/ lib/; do + if [ -d "$DIR" ]; then + SRC_DIR="$DIR" + break + fi + done + + if [ -z "$SRC_DIR" ]; then + echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Use repo phpstan.neon if present, otherwise use baseline config + ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table" + if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then + echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY + else + ARGS="$ARGS --level=3" + echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY + fi + + $PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt + EXIT=${PIPESTATUS[0]} + + if [ $EXIT -eq 0 ]; then + echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY + else + ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some") + echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + exit $EXIT diff --git a/.mokogitea/workflows/cleanup.yml b/.mokogitea/workflows/cleanup.yml new file mode 100644 index 0000000..3a81856 --- /dev/null +++ b/.mokogitea/workflows/cleanup.yml @@ -0,0 +1,87 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Maintenance +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/cleanup.yml +# VERSION: 01.00.00 +# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs + +name: "Universal: Repository Cleanup" + +on: + schedule: + - cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC + workflow_dispatch: + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + +jobs: + cleanup: + name: Clean Merged Branches + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Delete merged branches + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Merged Branch Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + + # List branches via API + BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?limit=50" | jq -r '.[].name') + + DELETED=0 + for BRANCH in $BRANCHES; do + # Skip protected branches + case "$BRANCH" in + main|master|develop|release/*|hotfix/*) continue ;; + esac + + # Check if branch is merged into main + if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then + echo " Deleting merged branch: ${BRANCH}" + curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches/${BRANCH}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + fi + done + + echo "Deleted ${DELETED} merged branch(es)" + + - name: Clean old workflow runs + env: + GA_TOKEN: ${{ secrets.GA_TOKEN }} + run: | + echo "=== Workflow Run Cleanup ===" + API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" + CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ) + + # Get old completed runs + RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \ + "${API}/actions/runs?status=completed&limit=50" | \ + jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null) + + DELETED=0 + for RUN_ID in $RUNS; do + curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \ + "${API}/actions/runs/${RUN_ID}" 2>/dev/null || true + DELETED=$((DELETED + 1)) + done + + echo "Deleted ${DELETED} old workflow run(s)" diff --git a/.mokogitea/workflows/deploy-manual.yml b/.mokogitea/workflows/deploy-manual.yml new file mode 100644 index 0000000..bb133ed --- /dev/null +++ b/.mokogitea/workflows/deploy-manual.yml @@ -0,0 +1,126 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API +# PATH: /templates/workflows/joomla/deploy-manual.yml.template +# VERSION: 04.07.00 +# BRIEF: Manual SFTP deploy to dev server for Joomla repos + +name: "Universal: Deploy to Dev (Manual)" + +on: + workflow_dispatch: + inputs: + clear_remote: + description: 'Delete all remote files before uploading' + required: false + default: 'false' + type: boolean + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +permissions: + contents: read + +jobs: + deploy: + name: SFTP Deploy to Dev + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Setup PHP + run: | + php -v && composer --version + + - name: Setup MokoStandards tools + env: + GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }} + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' + run: | + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api 2>/dev/null || true + if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then + cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Check FTP configuration + id: check + env: + HOST: ${{ vars.DEV_FTP_HOST }} + PATH_VAR: ${{ vars.DEV_FTP_PATH }} + PORT: ${{ vars.DEV_FTP_PORT }} + run: | + if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then + echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "host=$HOST" >> "$GITHUB_OUTPUT" + + REMOTE="${PATH_VAR%/}" + echo "remote=$REMOTE" >> "$GITHUB_OUTPUT" + + [ -z "$PORT" ] && PORT="22" + echo "port=$PORT" >> "$GITHUB_OUTPUT" + + - name: Deploy via SFTP + if: steps.check.outputs.skip != 'true' + env: + SFTP_KEY: ${{ secrets.DEV_FTP_KEY }} + SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + SFTP_USER: ${{ vars.DEV_FTP_USERNAME }} + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; } + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \ + > /tmp/sftp-config.json + + if [ -n "$SFTP_KEY" ]; then + echo "$SFTP_KEY" > /tmp/deploy_key + chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json + fi + + DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json) + [ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote) + + PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}" + else + php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}" + fi + + rm -f /tmp/deploy_key /tmp/sftp-config.json + + - name: Summary + if: always() + run: | + if [ "${{ steps.check.outputs.skip }}" = "true" ]; then + echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY + else + echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.mokogitea/workflows/gitleaks.yml b/.mokogitea/workflows/gitleaks.yml new file mode 100644 index 0000000..0c07612 --- /dev/null +++ b/.mokogitea/workflows/gitleaks.yml @@ -0,0 +1,96 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/gitleaks.yml.template +# VERSION: 01.00.00 +# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens +# +# +========================================================================+ +# | SECRET SCANNING | +# +========================================================================+ +# | | +# | Scans commits for leaked secrets using Gitleaks. | +# | | +# | - PR scan: only new commits in the PR | +# | - Scheduled: full repo scan weekly | +# | - Alerts via ntfy on findings | +# | | +# +========================================================================+ + +name: "Universal: Secret Scanning" + +on: + pull_request: + branches: + - main + - 'dev/**' + schedule: + - cron: '0 5 * * 1' # Weekly Monday 05:00 UTC + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + gitleaks: + name: Gitleaks Secret Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Gitleaks + run: | + GITLEAKS_VERSION="8.21.2" + curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + gitleaks version + + - name: Scan for secrets + id: scan + run: | + echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY + ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json" + + if [ "${{ github.event_name }}" = "pull_request" ]; then + # Scan only PR commits + ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY + else + echo "Full repository scan" >> $GITHUB_STEP_SUMMARY + fi + + if gitleaks detect $ARGS 2>&1; then + echo "result=clean" >> "$GITHUB_OUTPUT" + echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY + else + echo "result=found" >> "$GITHUB_OUTPUT" + FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown") + echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Notify on findings + if: failure() && steps.scan.outputs.result == 'found' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} — secrets detected in code" \ + -H "Tags: rotating_light,key" \ + -H "Priority: urgent" \ + -d "Gitleaks found potential secrets. Review and rotate credentials immediately." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true diff --git a/.mokogitea/workflows/notify.yml b/.mokogitea/workflows/notify.yml new file mode 100644 index 0000000..463a900 --- /dev/null +++ b/.mokogitea/workflows/notify.yml @@ -0,0 +1,71 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Notifications +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/notify.yml +# VERSION: 01.00.00 +# BRIEF: Push notifications via ntfy on release success or workflow failure + +name: "Universal: Notifications" + +on: + workflow_run: + workflows: + - "Joomla Build & Release" + - "Joomla Extension CI" + - "Deploy" + - "Cascade Main → Dev" + types: + - completed + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }} + +jobs: + notify: + name: Send Notification + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Notify on success (releases only) + if: >- + github.event.workflow_run.conclusion == 'success' && + contains(github.event.workflow_run.name, 'Release') + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} released" \ + -H "Tags: white_check_mark,package" \ + -H "Priority: default" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} completed successfully." \ + "${NTFY_URL}/${NTFY_TOPIC}" + + - name: Notify on failure + if: github.event.workflow_run.conclusion == 'failure' + run: | + REPO="${{ github.event.repository.name }}" + WORKFLOW="${{ github.event.workflow_run.name }}" + URL="${{ github.event.workflow_run.html_url }}" + + curl -sS \ + -H "Title: ${REPO} workflow failed" \ + -H "Tags: x,warning" \ + -H "Priority: high" \ + -H "Click: ${URL}" \ + -d "${WORKFLOW} failed. Check the run for details." \ + "${NTFY_URL}/${NTFY_TOPIC}" diff --git a/.mokogitea/workflows/pr-check.yml b/.mokogitea/workflows/pr-check.yml new file mode 100644 index 0000000..99e063f --- /dev/null +++ b/.mokogitea/workflows/pr-check.yml @@ -0,0 +1,196 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.CI +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/universal/pr-check.yml.template +# VERSION: 05.00.00 +# BRIEF: PR gate — branch policy + code validation before merge + +name: "Universal: PR Check" + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # ── Branch Policy ────────────────────────────────────────────────────── + branch-policy: + name: Branch Policy + runs-on: ubuntu-latest + steps: + - name: Check branch merge target + run: | + HEAD="${{ github.head_ref }}" + BASE="${{ github.base_ref }}" + + echo "PR: ${HEAD} → ${BASE}" + + ALLOWED=true + REASON="" + + case "$HEAD" in + feature/*|feat/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Feature branches must target 'dev', not '${BASE}'" + fi + ;; + fix/*|bugfix/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Fix branches must target 'dev', not '${BASE}'" + fi + ;; + hotfix/*) + if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'" + fi + ;; + alpha/*|beta/*) + if [ "$BASE" != "dev" ]; then + ALLOWED=false + REASON="Pre-release branches must target 'dev', not '${BASE}'" + fi + ;; + rc/*) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Release candidate branches must target 'main', not '${BASE}'" + fi + ;; + dev) + if [ "$BASE" != "main" ]; then + ALLOWED=false + REASON="Dev branch can only merge into 'main', not '${BASE}'" + fi + ;; + esac + + if [ "$ALLOWED" = false ]; then + echo "::error::${REASON}" + echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "${REASON}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY + echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY + echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "Branch policy: OK (${HEAD} → ${BASE})" + echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY + + # ── Code Validation ──────────────────────────────────────────────────── + validate: + name: Validate PR + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1 + fi + + - name: PHP syntax check + if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr' + run: | + ERRORS=0 + while IFS= read -r -d '' file; do + if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then + ERRORS=$((ERRORS + 1)) + fi + done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0) + echo "PHP lint: ${ERRORS} error(s)" + [ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; } + + - name: Validate platform manifest + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "::warning::No Joomla manifest found (WaaS site)" + exit 0 + fi + echo "Manifest: ${MANIFEST}" + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; } + fi + for ELEMENT in name version description; do + grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; } + done + echo "Joomla manifest valid" + ;; + dolibarr) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + if [ -z "$MOD_FILE" ]; then + echo "::error::No mod*.class.php found" + exit 1 + fi + echo "Dolibarr module: ${MOD_FILE}" + ;; + *) + echo "Generic platform — no manifest validation" + ;; + esac + + - name: Check update stream format + run: | + PLATFORM="${{ steps.platform.outputs.platform }}" + case "$PLATFORM" in + joomla) + if [ -f "updates.xml" ]; then + if command -v php &> /dev/null; then + php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; } + fi + echo "updates.xml valid" + fi + ;; + dolibarr) + [ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt" + ;; + esac + + - name: Verify package source + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::warning::No src/ or htdocs/ directory" + exit 0 + fi + FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l) + echo "Source: ${FILE_COUNT} files" + [ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; } diff --git a/.mokogitea/workflows/pre-release.yml b/.mokogitea/workflows/pre-release.yml new file mode 100644 index 0000000..c70ea7d --- /dev/null +++ b/.mokogitea/workflows/pre-release.yml @@ -0,0 +1,384 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Release +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /templates/workflows/universal/pre-release.yml.template +# VERSION: 05.00.00 +# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch + +name: "Universal: Pre-Release" + +on: + workflow_dispatch: + inputs: + stability: + description: 'Pre-release channel' + required: true + type: choice + options: + - development + - alpha + - beta + - release-candidate + +permissions: + contents: write + +env: + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +jobs: + build: + name: "Build Pre-Release (${{ inputs.stability }})" + runs-on: release + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GA_TOKEN }} + + - name: Setup PHP + run: | + if ! command -v php &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip >/dev/null 2>&1 + fi + + - name: Detect platform + id: platform + run: | + # Read platform from XML manifest ( tag) or plain text fallback + PLATFORM=$(sed -n 's/.*\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1) + [ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]') + [ -z "$PLATFORM" ] && PLATFORM="generic" + echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT" + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '/dev/null | head -1) + MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1) + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT" + + - name: Resolve metadata + id: meta + run: | + STABILITY="${{ inputs.stability }}" + + case "$STABILITY" in + development) SUFFIX="-dev"; TAG="development" ;; + alpha) SUFFIX="-alpha"; TAG="alpha" ;; + beta) SUFFIX="-beta"; TAG="beta" ;; + release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;; + esac + + # Read and bump patch version (with rollover) + CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1) + [ -z "$CURRENT" ] && CURRENT="00.00.00" + + MAJOR=$(echo "$CURRENT" | cut -d. -f1) + MINOR=$(echo "$CURRENT" | cut -d. -f2) + PATCH=$(echo "$CURRENT" | cut -d. -f3) + + # Patch bump with rollover: ZZ=99 → bump minor, YY=99 → bump major + NEW_PATCH=$((10#$PATCH + 1)) + NEW_MINOR=$((10#$MINOR)) + NEW_MAJOR=$((10#$MAJOR)) + + if [ $NEW_PATCH -gt 99 ]; then + NEW_PATCH=0 + NEW_MINOR=$((NEW_MINOR + 1)) + fi + if [ $NEW_MINOR -gt 99 ]; then + NEW_MINOR=0 + NEW_MAJOR=$((NEW_MAJOR + 1)) + fi + + VERSION=$(printf "%02d.%02d.%02d" $NEW_MAJOR $NEW_MINOR $NEW_PATCH) + TODAY=$(date +%Y-%m-%d) + + echo "Bumping: ${CURRENT} → ${VERSION} (patch)" + + # Update README.md + sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md + + # Update platform-specific manifest + PLATFORM="${{ steps.platform.outputs.platform }}" + MANIFEST="${{ steps.platform.outputs.manifest }}" + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + case "$PLATFORM" in + joomla) + if [ -n "$MANIFEST" ]; then + MANIFEST_VER=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + sed -i "s|${MANIFEST_VER}|${VERSION}|" "$MANIFEST" + sed -i "s|[^<]*|${TODAY}|" "$MANIFEST" + fi + ;; + dolibarr) + if [ -n "$MOD_FILE" ]; then + sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE" + fi + ;; + *) ;; + esac + + # Commit version bump + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git" + git add -A + git diff --cached --quiet || { + git commit -m "chore(version): bump ${CURRENT} → ${VERSION} [skip ci]" + git push origin HEAD 2>&1 + } + + # Auto-detect element (platform-aware) + case "$PLATFORM" in + joomla) + MANIFEST="${{ steps.platform.outputs.manifest }}" + EXT_ELEMENT="" + if [ -n "$MANIFEST" ]; then + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1) + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest) EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + ;; + dolibarr) + MOD_FILE="${{ steps.platform.outputs.mod_file }}" + if [ -n "$MOD_FILE" ]; then + MOD_BASENAME=$(basename "$MOD_FILE" .class.php) + EXT_ELEMENT=$(echo "$MOD_BASENAME" | sed 's/^mod//' | tr '[:upper:]' '[:lower:]') + else + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + fi + ;; + *) + EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') + ;; + esac + + ZIP_NAME="${EXT_ELEMENT}-${VERSION}${SUFFIX}.zip" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" + echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT" + echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT" + + echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ===" + + - name: Build package + run: | + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ ! -d "$SOURCE_DIR" ]; then + echo "::error::No src/ or htdocs/ directory" + exit 1 + fi + + mkdir -p build/package + rsync -a \ + --exclude='sftp-config*' \ + --exclude='.ftpignore' \ + --exclude='*.ppk' \ + --exclude='*.pem' \ + --exclude='*.key' \ + --exclude='.env*' \ + --exclude='*.local' \ + --exclude='.build-trigger' \ + "${SOURCE_DIR}/" build/package/ + + - name: Create ZIP + id: zip + run: | + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + cd build/package + zip -r "../${ZIP_NAME}" . + cd .. + + SHA256=$(sha256sum "${ZIP_NAME}" | cut -d' ' -f1) + echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" + echo "ZIP: ${ZIP_NAME} (SHA: ${SHA256:0:16}...)" + + - name: Create or replace Gitea release + id: release + run: | + TAG="${{ steps.meta.outputs.tag }}" + VERSION="${{ steps.meta.outputs.version }}" + STABILITY="${{ steps.meta.outputs.stability }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + EXT_ELEMENT="${{ steps.meta.outputs.ext_element }}" + TOKEN="${{ secrets.GA_TOKEN }}" + API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + BRANCH=$(git branch --show-current) + + BODY="## ${VERSION} ($(date +%Y-%m-%d)) + **Channel:** ${STABILITY} + **SHA-256:** \`${SHA256}\`" + + # Delete existing release + EXISTING_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API}/releases/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null) + if [ -n "$EXISTING_ID" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/releases/${EXISTING_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API}/tags/${TAG}" 2>/dev/null || true + fi + + # Create release + RELEASE_ID=$(curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${API}/releases" \ + -d "$(jq -n \ + --arg tag "$TAG" \ + --arg target "$BRANCH" \ + --arg name "${EXT_ELEMENT} ${VERSION} (${STABILITY})" \ + --arg body "$BODY" \ + '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: true}' + )" | jq -r '.id') + + echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + + # Upload ZIP + curl -sS -X POST -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + "${API}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ + --data-binary "@build/${ZIP_NAME}" + + echo "Released: ${EXT_ELEMENT} ${VERSION} (${STABILITY})" + + - name: Update updates.xml + if: steps.platform.outputs.platform == 'joomla' + run: | + STABILITY="${{ steps.meta.outputs.stability }}" + VERSION="${{ steps.meta.outputs.version }}" + SHA256="${{ steps.zip.outputs.sha256 }}" + ZIP_NAME="${{ steps.meta.outputs.zip_name }}" + TAG="${{ steps.meta.outputs.tag }}" + DATE=$(date +%Y-%m-%d) + + if [ ! -f "updates.xml" ]; then + echo "No updates.xml — skipping" + exit 0 + fi + + export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ + PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ + PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" + python3 << 'PYEOF' + import re, os + + stability = os.environ["PY_STABILITY"] + version = os.environ["PY_VERSION"] + sha256 = os.environ["PY_SHA256"] + zip_name = os.environ["PY_ZIP_NAME"] + tag = os.environ["PY_TAG"] + date = os.environ["PY_DATE"] + gitea_org = os.environ["PY_GITEA_ORG"] + gitea_repo = os.environ["PY_GITEA_REPO"] + download_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" + + with open("updates.xml", "r") as f: + content = f.read() + + # Map stability to XML tag name + tag_map = {"development": "development", "alpha": "alpha", "beta": "beta", "release-candidate": "rc"} + xml_tag = tag_map.get(stability, stability) + + pattern = r"((?:(?!).)*?" + re.escape(xml_tag) + r".*?)" + match = re.search(pattern, content, re.DOTALL) + if match: + block = match.group(1) + updated = re.sub(r"[^<]*", f"{version}", block) + updated = re.sub(r"[^<]*", f"{date}", updated) + if "" in updated: + updated = re.sub(r"[^<]*", f"{sha256}", updated) + else: + updated = updated.replace("", f"\n {sha256}") + updated = re.sub(r"(]*>)[^<]*()", rf"\g<1>{download_url}\g<2>", updated) + content = content.replace(block, updated) + print(f"Updated {xml_tag} channel: version={version}") + else: + print(f"WARNING: No {xml_tag} block in updates.xml") + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit and push to current branch + if ! git diff --quiet updates.xml 2>/dev/null; then + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]" + git push origin HEAD 2>&1 || echo "WARNING: push failed" + fi + + - name: "Sync updates.xml to all branches" + if: steps.platform.outputs.platform == 'joomla' + run: | + CURRENT_BRANCH="${{ github.ref_name }}" + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + + # Sync updates.xml to main and dev (whichever isn't current) + for BRANCH in main dev; do + [ "$BRANCH" = "$CURRENT_BRANCH" ] && continue + + echo "Syncing updates.xml → ${BRANCH}" + git fetch origin "${BRANCH}" 2>/dev/null || continue + git checkout "origin/${BRANCH}" -- . 2>/dev/null || continue + git checkout "${CURRENT_BRANCH}" -- updates.xml + if ! git diff --quiet updates.xml 2>/dev/null; then + git add updates.xml + git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]" + git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed" + fi + git checkout "${CURRENT_BRANCH}" 2>/dev/null + done + + - name: "Delete lesser pre-release channels (cascade)" + continue-on-error: true + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + TOKEN="${{ secrets.GA_TOKEN }}" + STABILITY="${{ steps.meta.outputs.stability }}" + + # Cascade: rc → beta,alpha,dev | beta → alpha,dev | alpha → dev | dev → nothing + case "$STABILITY" in + release-candidate) TAGS_TO_DELETE="beta alpha development" ;; + beta) TAGS_TO_DELETE="alpha development" ;; + alpha) TAGS_TO_DELETE="development" ;; + *) TAGS_TO_DELETE="" ;; + esac + + [ -z "$TAGS_TO_DELETE" ] && exit 0 + + for TAG in $TAGS_TO_DELETE; do + RELEASE_ID=$(curl -sS -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/tags/${TAG}" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "None" ]; then + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/releases/${RELEASE_ID}" 2>/dev/null || true + curl -sS -X DELETE -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/tags/${TAG}" 2>/dev/null || true + echo "Deleted: ${TAG} (id: ${RELEASE_ID})" + fi + done diff --git a/.mokogitea/workflows/repo-health.yml b/.mokogitea/workflows/repo-health.yml new file mode 100644 index 0000000..e5e1c73 --- /dev/null +++ b/.mokogitea/workflows/repo-health.yml @@ -0,0 +1,766 @@ +# ============================================================================ +# Copyright (C) 2025 Moko Consulting +# +# This file is part of a Moko Consulting project. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Validation +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/repo_health.yml.template +# VERSION: 04.06.00 +# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts. +# ============================================================================ + +name: "Joomla: Repo Health" + +concurrency: + group: repo-health-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +on: + workflow_dispatch: + inputs: + profile: + description: 'Validation profile: all, release, scripts, or repo' + required: true + default: all + type: choice + options: + - all + - release + - scripts + - repo + pull_request: + push: + +permissions: + contents: read + +env: + # Release policy - Repository Variables Only + RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX + RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX + + # Scripts governance policy + SCRIPTS_REQUIRED_DIRS: + SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate + + # Repo health policy + REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.gitea/workflows/ + REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/ + REPO_DISALLOWED_DIRS: + REPO_DISALLOWED_FILES: TODO.md,todo.md + + # Extended checks toggles + EXTENDED_CHECKS: "true" + + # File / directory variables + DOCS_INDEX: docs/docs-index.md + SCRIPT_DIR: scripts + WORKFLOWS_DIR: .gitea/workflows + SHELLCHECK_PATTERN: '*.sh' + SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml' + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + access_check: + name: Access control + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + + outputs: + allowed: ${{ steps.perm.outputs.allowed }} + permission: ${{ steps.perm.outputs.permission }} + + steps: + - name: Check actor permission (admin only) + id: perm + env: + TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + set -euo pipefail + ALLOWED=false + PERMISSION=unknown + METHOD="" + + # Hardcoded authorized users — always allowed + case "$ACTOR" in + jmiller|gitea-actions[bot]) + ALLOWED=true + PERMISSION=admin + METHOD="hardcoded allowlist" + ;; + *) + # Detect platform and check permissions via API + API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}" + RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \ + "${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}') + PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown") + if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then + ALLOWED=true + fi + METHOD="collaborator API" + ;; + esac + + echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" + echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" + + { + echo "## Access Authorization" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| **Actor** | \`${ACTOR}\` |" + echo "| **Repository** | \`${REPO}\` |" + echo "| **Permission** | \`${PERMISSION}\` |" + echo "| **Method** | ${METHOD} |" + echo "| **Authorized** | ${ALLOWED} |" + echo "" + if [ "$ALLOWED" = "true" ]; then + echo "${ACTOR} authorized (${METHOD})" + else + echo "${ACTOR} is NOT authorized. Requires admin or maintain role." + fi + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Deny execution when not permitted + if: ${{ steps.perm.outputs.allowed != 'true' }} + run: | + set -euo pipefail + printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}" + exit 1 + + release_config: + name: Release configuration + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Guardrails release vars + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }} + DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes release validation' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}" + IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}" + + missing=() + missing_optional=() + + for k in "${required[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing+=("${k}") + done + + for k in "${optional[@]}"; do + v="${!k:-}" + [ -z "${v}" ] && missing_optional+=("${k}") + done + + { + printf '%s\n' '### Release configuration (Repository Variables)' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Variable | Status |' + printf '%s\n' '|---|---|' + printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |" + printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repository variables' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#missing[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repository variables' + for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + { + printf '%s\n' '### Repository variables validation result' + printf '%s\n' 'Status: OK' + printf '%s\n' 'All required repository variables present.' + printf '%s\n' '' + printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + scripts_governance: + name: Scripts governance + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Scripts folder checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes scripts governance' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + if [ ! -d "${SCRIPT_DIR}" ]; then + { + printf '%s\n' '### Scripts governance' + printf '%s\n' 'Status: OK (advisory)' + printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}" + IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}" + + missing_dirs=() + unapproved_dirs=() + + for d in "${required_dirs[@]}"; do + req="${d%/}" + [ ! -d "${req}" ] && missing_dirs+=("${req}/") + done + + while IFS= read -r d; do + allowed=false + for a in "${allowed_dirs[@]}"; do + a_norm="${a%/}" + [ "${d%/}" = "${a_norm}" ] && allowed=true + done + [ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/") + done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##') + + { + printf '%s\n' '### Scripts governance' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Area | Status | Notes |' + printf '%s\n' '|---|---|---|' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Required directories | Warning | Missing required subfolders |' + else + printf '%s\n' '| Required directories | OK | All required subfolders present |' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |' + else + printf '%s\n' '| Directory policy | OK | No unapproved directories |' + fi + + printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |' + printf '\n' + + if [ "${#missing_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Missing required script directories:' + for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Missing required script directories: none.' + printf '\n' + fi + + if [ "${#unapproved_dirs[@]}" -gt 0 ]; then + printf '%s\n' 'Unapproved script directories detected:' + for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + else + printf '%s\n' 'Unapproved script directories detected: none.' + printf '\n' + fi + + printf '%s\n' 'Scripts governance completed in advisory mode.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + repo_health: + name: Repository health + needs: access_check + if: ${{ needs.access_check.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + + - name: Repository health checks + env: + PROFILE_RAW: ${{ github.event.inputs.profile }} + run: | + set -euo pipefail + + profile="${PROFILE_RAW:-all}" + case "${profile}" in + all|release|scripts|repo) ;; + *) + printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}" + exit 1 + ;; + esac + + if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' 'Status: SKIPPED' + printf '%s\n' 'Reason: profile excludes repository health' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + # Source directory: src/ or htdocs/ (either is valid) + if [ -d "src" ]; then + SOURCE_DIR="src" + elif [ -d "htdocs" ]; then + SOURCE_DIR="htdocs" + else + missing_required+=("src/ or htdocs/ (source directory required)") + fi + + IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}" + IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}" + IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}" + IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES}" + + missing_required=() + missing_optional=() + + for item in "${required_artifacts[@]}"; do + if printf '%s' "${item}" | grep -q '/$'; then + d="${item%/}" + [ ! -d "${d}" ] && missing_required+=("${item}") + else + [ ! -f "${item}" ] && missing_required+=("${item}") + fi + done + + for f in "${optional_files[@]}"; do + if printf '%s' "${f}" | grep -q '/$'; then + d="${f%/}" + [ ! -d "${d}" ] && missing_optional+=("${f}") + else + [ ! -f "${f}" ] && missing_optional+=("${f}") + fi + done + + for d in "${disallowed_dirs[@]}"; do + d_norm="${d%/}" + [ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)") + done + + for f in "${disallowed_files[@]}"; do + [ -f "${f}" ] && missing_required+=("${f} (disallowed)") + done + + git fetch origin --prune + + dev_paths=() + dev_branches=() + + while IFS= read -r b; do + name="${b#origin/}" + if [ "${name}" = 'dev' ]; then + dev_branches+=("${name}") + else + dev_paths+=("${name}") + fi + done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//') + + if [ "${#dev_paths[@]}" -eq 0 ]; then + missing_required+=("dev/* branch (e.g. dev/01.00.00)") + fi + + if [ "${#dev_branches[@]}" -gt 0 ]; then + missing_required+=("invalid branch dev (must be dev/)") + fi + + content_warnings=() + + if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md missing '# Changelog' header") + fi + + if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then + content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)") + fi + + if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then + content_warnings+=("LICENSE does not look like a GPL text") + fi + + if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then + content_warnings+=("README.md missing expected brand keyword") + fi + + export PROFILE_RAW="${profile}" + export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")" + export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")" + export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")" + + report_json="$(python3 - <<'PY' + import json + import os + + profile = os.environ.get('PROFILE_RAW') or 'all' + + missing_required = os.environ.get('MISSING_REQUIRED', '').splitlines() if os.environ.get('MISSING_REQUIRED') else [] + missing_optional = os.environ.get('MISSING_OPTIONAL', '').splitlines() if os.environ.get('MISSING_OPTIONAL') else [] + content_warnings = os.environ.get('CONTENT_WARNINGS', '').splitlines() if os.environ.get('CONTENT_WARNINGS') else [] + + out = { + 'profile': profile, + 'missing_required': [x for x in missing_required if x], + 'missing_optional': [x for x in missing_optional if x], + 'content_warnings': [x for x in content_warnings if x], + } + + print(json.dumps(out, indent=2)) + PY + )" + + { + printf '%s\n' '### Repository health' + printf '%s\n' "Profile: ${profile}" + printf '%s\n' '| Metric | Value |' + printf '%s\n' '|---|---|' + printf '%s\n' "| Missing required | ${#missing_required[@]} |" + printf '%s\n' "| Missing optional | ${#missing_optional[@]} |" + printf '%s\n' "| Content warnings | ${#content_warnings[@]} |" + printf '\n' + + printf '%s\n' '### Guardrails report (JSON)' + printf '%s\n' '```json' + printf '%s\n' "${report_json}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${#missing_required[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing required repo artifacts' + for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done + printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + exit 1 + fi + + if [ "${#missing_optional[@]}" -gt 0 ]; then + { + printf '%s\n' '### Missing optional repo artifacts' + for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + if [ "${#content_warnings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Repo content warnings' + for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + # -- Joomla-specific checks -- + joomla_findings=() + + MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '/dev/null | head -1 || true)" + if [ -z "${MANIFEST}" ]; then + joomla_findings+=("Joomla XML manifest not found (no *.xml with tag)") + else + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then + joomla_findings+=("XML manifest: type attribute missing or invalid") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP '' "${MANIFEST}"; then + joomla_findings+=("XML manifest: tag missing") + fi + if ! grep -qP ' missing (required for Joomla 5+)") + fi + fi + + INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)" + if [ "${INI_COUNT}" -eq 0 ]; then + joomla_findings+=("No .ini language files found") + fi + + if [ ! -f 'updates.xml' ]; then + joomla_findings+=("updates.xml missing in root (required for Joomla update server)") + fi + + INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site") + for dir in "${INDEX_DIRS[@]}"; do + if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then + joomla_findings+=("${dir}/index.html missing (directory listing protection)") + fi + done + + if [ "${#joomla_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' '| Check | Status |' + printf '%s\n' '|---|---|' + for f in "${joomla_findings[@]}"; do + printf '%s\n' "| ${f} | Warning |" + done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + else + { + printf '%s\n' '### Joomla extension checks' + printf '%s\n' 'All Joomla-specific checks passed.' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + extended_enabled="${EXTENDED_CHECKS:-true}" + extended_findings=() + + if [ "${extended_enabled}" = 'true' ]; then + if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then + : + else + extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)") + fi + + if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then + bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)" + if [ -n "${bad_refs}" ]; then + extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt") + { + printf '%s\n' '### Workflow pinning advisory' + printf '%s\n' 'Found uses: entries pinned to main/master:' + printf '%s\n' '```' + printf '%s\n' "${bad_refs}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -f "${DOCS_INDEX}" ]; then + missing_links="$(python3 - <<'PY' + import os + import re + + idx = os.environ.get('DOCS_INDEX', 'docs/docs-index.md') + base = os.getcwd() + + bad = [] + pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') + + with open(idx, 'r', encoding='utf-8') as f: + for line in f: + for m in pat.findall(line): + link = m.strip() + if link.startswith('http://') or link.startswith('https://') or link.startswith('#') or link.startswith('mailto:'): + continue + if link.startswith('/'): + rel = link.lstrip('/') + else: + rel = os.path.normpath(os.path.join(os.path.dirname(idx), link)) + rel = rel.split('#', 1)[0] + rel = rel.split('?', 1)[0] + if not rel: + continue + p = os.path.join(base, rel) + if not os.path.exists(p): + bad.append(rel) + + print('\n'.join(sorted(set(bad)))) + PY + )" + if [ -n "${missing_links}" ]; then + extended_findings+=("docs/docs-index.md contains broken relative links") + { + printf '%s\n' '### Docs index link integrity' + printf '%s\n' 'Broken relative links:' + while IFS= read -r l; do [ -n "${l}" ] && printf '%s\n' "- ${l}"; done <<< "${missing_links}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + if [ -d "${SCRIPT_DIR}" ]; then + if ! command -v shellcheck >/dev/null 2>&1; then + sudo apt-get update -qq + sudo apt-get install -y shellcheck >/dev/null + fi + + sc_out='' + while IFS= read -r shf; do + [ -z "${shf}" ] && continue + out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)" + if [ -n "${out_one}" ]; then + sc_out="${sc_out}${out_one}\n" + fi + done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort) + + if [ -n "${sc_out}" ]; then + extended_findings+=("ShellCheck warnings detected (advisory)") + sc_head="$(printf '%s' "${sc_out}" | head -n 200)" + { + printf '%s\n' '### ShellCheck (advisory)' + printf '%s\n' '```' + printf '%s\n' "${sc_head}" + printf '%s\n' '```' + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + spdx_missing=() + IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}" + spdx_args=() + for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done + + while IFS= read -r f; do + [ -z "${f}" ] && continue + if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then + spdx_missing+=("${f}") + fi + done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true) + + if [ "${#spdx_missing[@]}" -gt 0 ]; then + extended_findings+=("SPDX header missing in some tracked files (advisory)") + { + printf '%s\n' '### SPDX header advisory' + printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):' + for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + stale_cutoff_days=180 + stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)" + if [ -n "${stale_branches}" ]; then + extended_findings+=("Stale remote branches detected (advisory)") + { + printf '%s\n' '### Git hygiene advisory' + printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):" + while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}" + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + fi + + { + printf '%s\n' '### Guardrails coverage matrix' + printf '%s\n' '| Domain | Status | Notes |' + printf '%s\n' '|---|---|---|' + printf '%s\n' '| Access control | OK | Admin-only execution gate |' + printf '%s\n' '| Release variables | OK | Repository variables validation |' + printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |' + printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |' + printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |' + if [ "${extended_enabled}" = 'true' ]; then + if [ "${#extended_findings[@]}" -gt 0 ]; then + printf '%s\n' '| Extended checks | Warning | See extended findings below |' + else + printf '%s\n' '| Extended checks | OK | No findings |' + fi + else + printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |' + fi + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + + if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then + { + printf '%s\n' '### Extended findings (advisory)' + for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done + printf '\n' + } >> "${GITHUB_STEP_SUMMARY}" + fi + + printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}" diff --git a/.mokogitea/workflows/security-audit.yml b/.mokogitea/workflows/security-audit.yml new file mode 100644 index 0000000..789325a --- /dev/null +++ b/.mokogitea/workflows/security-audit.yml @@ -0,0 +1,82 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Security +# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards +# PATH: /.gitea/workflows/security-audit.yml +# VERSION: 01.00.00 +# BRIEF: Dependency vulnerability scanning for composer and npm packages + +name: "Universal: Security Audit" + +on: + schedule: + - cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC + pull_request: + branches: + - main + paths: + - 'composer.json' + - 'composer.lock' + - 'package.json' + - 'package-lock.json' + workflow_dispatch: + +permissions: + contents: read + +env: + NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }} + NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }} + +jobs: + audit: + name: Dependency Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Composer audit + if: hashFiles('composer.lock') != '' + run: | + echo "=== Composer Security Audit ===" + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq + sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1 + fi + composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt + RESULT=$? + if [ $RESULT -ne 0 ]; then + echo "::warning::Composer vulnerabilities found" + echo "composer_vulnerable=true" >> "$GITHUB_ENV" + else + echo "No known vulnerabilities in composer dependencies" + fi + + - name: NPM audit + if: hashFiles('package-lock.json') != '' + run: | + echo "=== NPM Security Audit ===" + npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true + if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then + echo "No known vulnerabilities in npm dependencies" + else + echo "::warning::NPM vulnerabilities found" + echo "npm_vulnerable=true" >> "$GITHUB_ENV" + fi + + - name: Notify on vulnerabilities + if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true' + run: | + REPO="${{ github.event.repository.name }}" + curl -sS \ + -H "Title: ${REPO} has vulnerable dependencies" \ + -H "Tags: lock,warning" \ + -H "Priority: high" \ + -d "Security audit found vulnerabilities. Review dependency updates." \ + "${NTFY_URL}/${NTFY_TOPIC}" || true diff --git a/.mokogitea/workflows/update-server.yml b/.mokogitea/workflows/update-server.yml new file mode 100644 index 0000000..6e617f6 --- /dev/null +++ b/.mokogitea/workflows/update-server.yml @@ -0,0 +1,464 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: Gitea.Workflow +# INGROUP: MokoStandards.Joomla +# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API +# PATH: /templates/workflows/joomla/update-server.yml.template +# VERSION: 04.06.00 +# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries +# +# Writes updates.xml with multiple entries: +# - stable on push to main (from auto-release) +# - rc on push to rc/** +# - development on push to dev or dev/** +# +# Joomla filters by user's "Minimum Stability" setting. + +name: "Joomla: Update Server" + +on: + push: + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + pull_request: + types: [closed] + branches: + - 'dev' + - 'dev/**' + - 'alpha/**' + - 'beta/**' + - 'rc/**' + paths: + - 'src/**' + - 'htdocs/**' + workflow_dispatch: + inputs: + stability: + description: 'Stability tag' + required: true + default: 'development' + type: choice + options: + - development + - alpha + - beta + - rc + - stable + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} + GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} + GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} + +permissions: + contents: write + +jobs: + update-xml: + name: Update updates.xml + runs-on: release + if: >- + github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + token: ${{ secrets.GA_TOKEN }} + fetch-depth: 0 + + - name: Setup MokoStandards tools + env: + MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} + MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting + COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' + run: | + if ! command -v composer &> /dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 + fi + git clone --depth 1 --branch main --quiet \ + "https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ + /tmp/mokostandards-api 2>/dev/null || true + if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then + cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true + fi + + - name: Generate updates.xml entry + id: update + run: | + BRANCH="${{ github.ref_name }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0") + + # Auto-bump patch on all branches (dev, alpha, beta, rc) + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) + if [ -n "$BUMPED" ]; then + VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") + git add -A + git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ + --author="gitea-actions[bot] " 2>/dev/null || true + git push 2>/dev/null || true + fi + + # Determine stability from branch or input + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + STABILITY="${{ inputs.stability }}" + elif [[ "$BRANCH" == rc/* ]]; then + STABILITY="rc" + elif [[ "$BRANCH" == beta/* ]]; then + STABILITY="beta" + elif [[ "$BRANCH" == alpha/* ]]; then + STABILITY="alpha" + elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then + STABILITY="development" + else + STABILITY="stable" + fi + + echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT" + + # Parse manifest (portable — no grep -P) + MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" ! -path "./build/*" -exec grep -l '/dev/null | head -1) + if [ -z "$MANIFEST" ]; then + echo "No Joomla manifest found — skipping" + exit 0 + fi + + # Extract fields using sed (works on all runners) + EXT_NAME=$(sed -n 's/.*\([^<]*\)<\/name>.*/\1/p' "$MANIFEST" | head -1) + EXT_TYPE=$(sed -n 's/.*]*type="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_ELEMENT=$(sed -n 's/.*\([^<]*\)<\/element>.*/\1/p' "$MANIFEST" | head -1) + EXT_CLIENT=$(sed -n 's/.*]*client="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_FOLDER=$(sed -n 's/.*]*group="\([^"]*\)".*/\1/p' "$MANIFEST" | head -1) + EXT_VERSION=$(sed -n 's/.*\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1) + TARGET_PLATFORM=$(sed -n 's/.*\(\).*/\1/p' "$MANIFEST" | head -1) + PHP_MINIMUM=$(sed -n 's/.*\([^<]*\)<\/php_minimum>.*/\1/p' "$MANIFEST" | head -1) + + # Fallbacks + [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" + [ -z "$EXT_TYPE" ] && EXT_TYPE="component" + + # Derive element if not in manifest: try XML filename, then repo name + if [ -z "$EXT_ELEMENT" ]; then + EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') + case "$EXT_ELEMENT" in + templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;; + esac + fi + + # Use manifest version if README version is empty + [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" + + [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '' "/") + + CLIENT_TAG="" + [ -n "$EXT_CLIENT" ] && CLIENT_TAG="${EXT_CLIENT}" + [ -z "$CLIENT_TAG" ] && ([ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]) && CLIENT_TAG="site" + + FOLDER_TAG="" + [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ] && FOLDER_TAG="${EXT_FOLDER}" + + PHP_TAG="" + [ -n "$PHP_MINIMUM" ] && PHP_TAG="${PHP_MINIMUM}" + + # Version suffix for non-stable + DISPLAY_VERSION="$VERSION" + case "$STABILITY" in + development) DISPLAY_VERSION="${VERSION}-dev" ;; + alpha) DISPLAY_VERSION="${VERSION}-alpha" ;; + beta) DISPLAY_VERSION="${VERSION}-beta" ;; + rc) DISPLAY_VERSION="${VERSION}-rc" ;; + esac + + MAJOR=$(echo "$VERSION" | awk -F. '{print $1}') + + # Each stability level has its own release tag + case "$STABILITY" in + development) RELEASE_TAG="development" ;; + alpha) RELEASE_TAG="alpha" ;; + beta) RELEASE_TAG="beta" ;; + rc) RELEASE_TAG="release-candidate" ;; + *) RELEASE_TAG="v${MAJOR}" ;; + esac + + PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" + DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" + INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" + + # -- Build install packages (ZIP + tar.gz) -------------------- + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + if [ -d "$SOURCE_DIR" ]; then + EXCLUDES=".ftpignore sftp-config* *.ppk *.pem *.key .env*" + TAR_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz" + + cd "$SOURCE_DIR" + zip -r "/tmp/${PACKAGE_NAME}" . -x $EXCLUDES + cd .. + tar -czf "/tmp/${TAR_NAME}" -C "$SOURCE_DIR" \ + --exclude='.ftpignore' --exclude='sftp-config*' \ + --exclude='*.ppk' --exclude='*.pem' --exclude='*.key' --exclude='.env*' . + + SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) + + # Ensure release exists on Gitea + RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + + if [ -z "$RELEASE_ID" ]; then + # Create release + RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/releases" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'tag_name': '${RELEASE_TAG}', + 'name': '${RELEASE_TAG} (${DISPLAY_VERSION})', + 'body': '${STABILITY} release', + 'prerelease': True, + 'target_commitish': 'main' + }))")" 2>/dev/null || true) + RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true) + fi + + if [ -n "$RELEASE_ID" ]; then + # Delete existing assets with same name before uploading + ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]") + for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do + ASSET_ID=$(echo "$ASSETS" | python3 -c " + import sys,json + assets = json.load(sys.stdin) + for a in assets: + if a['name'] == '${ASSET_FILE}': + print(a['id']); break + " 2>/dev/null || true) + if [ -n "$ASSET_ID" ]; then + curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true + fi + done + + # Upload both formats + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${PACKAGE_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true + + curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @"/tmp/${TAR_NAME}" \ + "${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true + fi + + echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY + else + SHA256="" + fi + + # -- Build the new entry (canonical format matching release.yml) -- + NEW_ENTRY="" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_NAME} ${STABILITY} build.\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_ELEMENT}\n" + NEW_ENTRY="${NEW_ENTRY} ${EXT_TYPE}\n" + [ -n "$CLIENT_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${CLIENT_TAG}\n" + [ -n "$FOLDER_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${FOLDER_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} ${VERSION}\n" + NEW_ENTRY="${NEW_ENTRY} $(date +%Y-%m-%d)\n" + NEW_ENTRY="${NEW_ENTRY} https://git.mokoconsulting.tech/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + NEW_ENTRY="${NEW_ENTRY} ${DOWNLOAD_URL}\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} ${SHA256}\n" + NEW_ENTRY="${NEW_ENTRY} ${STABILITY}\n" + NEW_ENTRY="${NEW_ENTRY} Moko Consulting\n" + NEW_ENTRY="${NEW_ENTRY} https://mokoconsulting.tech\n" + NEW_ENTRY="${NEW_ENTRY} \n" + [ -n "$PHP_MINIMUM" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_MINIMUM}\n" + NEW_ENTRY="${NEW_ENTRY} " + + # -- Write new entry to temp file -------------------------------- + printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml + + # -- Merge into updates.xml ---------------------------------------- + # Cascade: stable→all | rc→rc+lower | beta→beta+lower | alpha→alpha+dev | dev→dev + CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development" + TARGETS="" + for entry in $CASCADE_MAP; do + key="${entry%%:*}" + vals="${entry#*:}" + if [ "$key" = "${STABILITY}" ]; then + TARGETS="$vals" + break + fi + done + [ -z "$TARGETS" ] && TARGETS="${STABILITY}" + + echo "Cascade: ${STABILITY} → ${TARGETS}" + + # Create updates.xml if missing + if [ ! -f "updates.xml" ]; then + printf '%s\n' "" > updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + printf '%s\n' "" >> updates.xml + fi + + # Update existing blocks or create missing ones + export PY_TARGETS="$TARGETS" PY_VERSION="$VERSION" PY_DATE="$(date +%Y-%m-%d)" + python3 << 'PYEOF' + import re, os + + targets = os.environ["PY_TARGETS"].split(",") + version = os.environ["PY_VERSION"] + date = os.environ["PY_DATE"] + + with open("updates.xml") as f: + content = f.read() + with open("/tmp/new_entry.xml") as f: + new_entry_template = f.read() + + for tag in targets: + tag = tag.strip() + # Build entry with this tag's name + new_entry = re.sub(r"[^<]*", f"{tag}", new_entry_template) + + # Try to find existing block (handles both single-line and multi-line ) + block_pattern = r"((?:(?!).)*?" + re.escape(tag) + r".*?)" + match = re.search(block_pattern, content, re.DOTALL) + + if match: + # Update in place — replace entire block + content = content.replace(match.group(1), new_entry.strip()) + print(f" UPDATED: {tag} → {version}") + else: + # Create — insert before + content = content.replace("", "\n" + new_entry.strip() + "\n\n") + print(f" CREATED: {tag} → {version}") + + # Clean up excessive blank lines + content = re.sub(r"\n{3,}", "\n\n", content) + + with open("updates.xml", "w") as f: + f.write(content) + PYEOF + + # Commit + git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" + git config --local user.name "gitea-actions[bot]" + git add updates.xml + git diff --cached --quiet || { + git commit -m "chore: update updates.xml (${STABILITY}: ${DISPLAY_VERSION}) [skip ci]" \ + --author="gitea-actions[bot] " + git push + } + + # -- Sync updates.xml to main (for non-main branches) ---------------------- + - name: Sync updates.xml to main + if: github.ref_name != 'main' + run: | + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + GA_TOKEN="${{ secrets.GA_TOKEN }}" + + FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ + "${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true) + + if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then + CONTENT=$(base64 -w0 updates.xml) + curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/contents/updates.xml" \ + -d "$(python3 -c "import json; print(json.dumps({ + 'content': '${CONTENT}', + 'sha': '${FILE_SHA}', + 'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]', + 'branch': 'main' + }))")" > /dev/null 2>&1 \ + && echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \ + || echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY + else + echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY + fi + + - name: SFTP deploy to dev server + if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' + env: + DEV_HOST: ${{ vars.DEV_FTP_HOST }} + DEV_PATH: ${{ vars.DEV_FTP_PATH }} + DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }} + DEV_USER: ${{ vars.DEV_FTP_USERNAME }} + DEV_PORT: ${{ vars.DEV_FTP_PORT }} + DEV_KEY: ${{ secrets.DEV_FTP_KEY }} + DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} + run: | + # -- Permission check: admin or maintain role required -------- + ACTOR="${{ github.actor }}" + REPO="${{ github.repository }}" + API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" + + PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ + "${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ + python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read") + case "$PERMISSION" in + admin|maintain|write) ;; + *) + echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write" + exit 0 + ;; + esac + + [ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; } + + SOURCE_DIR="src" + [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" + [ ! -d "$SOURCE_DIR" ] && exit 0 + + PORT="${DEV_PORT:-22}" + REMOTE="${DEV_PATH%/}" + [ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}" + + printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \ + "$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json + if [ -n "$DEV_KEY" ]; then + echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key + printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json + else + printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json + fi + + PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) + if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then + php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json + fi + rm -f /tmp/deploy_key /tmp/sftp-config.json + echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY + + - name: Summary + if: always() + run: | + echo "## Joomla Update Server" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`${DISPLAY_VERSION}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Element | \`${EXT_ELEMENT}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Download | [ZIP](${DOWNLOAD_URL}) |" >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de5655..bbee9d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Smart Visitor Detection** — Pushes anonymised visitor properties (login status, user group, page type) to the dataLayer for Google Analytics / Tag Manager. Sets GA4 `user_properties` for persistent session-scoped dimensions. No PII is sent. Default enabled when GTM or GA4 is active. - **Auto-cascade workflow** — Forward-merges `main` → `dev` after every push; auto-creates a PR on conflict +- **Component/print-view stylesheet** — Dedicated `component.css` replaces `template.css` in the component view with print-optimised styles using theme variables +- **Print-view GA4 tracking** — Component view sends `content_group=print_view` to Google Analytics for tracking print/modal usage +- **Custom light theme in component view** — Component view now loads `light.custom.css` when configured +- **Changelog auto-bump in auto-release** — `## [Unreleased]` is automatically promoted to the release version on stable release, with a fresh `## [Unreleased]` section inserted above -## [03.10.00] - 2026-04-18 — Bridge Release (MokoOnyx → MokoOnyx) +### Changed +- **Custom head params replaced with user files** — Removed `custom_head_start` / `custom_head_end` template params in favour of `user.css` and `user.js` (loaded via Web Asset Manager) +- **User override files added to .gitignore** — `user.css` and `user.js` are client-repo only; not committed to the template repo + +### Removed +- **Migration tab** — Removed MokoCassiopeia migration fieldset and associated language strings from template params +- **Migration description** — Removed migration callout and "formerly MokoCassiopeia" reference from template description +- **Custom head fields** — Removed `custom_head_start` / `custom_head_end` fields and `Custom Code` fieldset from template configuration + +## [03.10.00] - 2026-04-18 ### Important -- **Template Rename** — MokoOnyx is being renamed to **MokoOnyx**. This bridge release automatically migrates your template settings, menu assignments, and files to the new name. MokoOnyx can be safely uninstalled after this update. +- **Template Consolidation** — This release finalised the MokoOnyx identity, adding automatic migration from legacy MokoCassiopeia installations. Settings, menu assignments, and files are imported on first page load. ### Added - **Offline page redesign** — Full-viewport background from Joomla offline_image or header background, glass card overlay, centered logo with glow, login accordion, copyright footer @@ -38,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Favicon multi-format support** — Now handles PNG, JPEG, GIF, WebP, BMP (not just PNG) - **Theme variables** — `--theme-fab-bg`, `--theme-fab-color`, `--theme-fab-btn-bg`, `--theme-fab-border`, `--offline-card-bg` - **Footer CSS variables** — Added to CSS Variables reference tab -- **Bridge migration script** — `helper/bridge.php` handles automatic MokoOnyx → MokoOnyx migration +- **Bridge migration script** — `helper/bridge.php` handles automatic MokoCassiopeia → MokoOnyx migration - **Dedicated release runner** — Release workflows run on isolated `release` label runner - **Runner fleet** — 3 CI + 1 release runner (12 concurrent jobs) @@ -67,10 +80,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [Unreleased] - 2026-04-02 +## [03.09.03] - 2026-04-02 ### Added - - **Favicon configuration** — New "Favicon" tab in template config; upload a PNG and all favicon sizes are auto-generated via PHP GD (ICO, Apple Touch Icon 180px, Android Chrome 192/512px, site.webmanifest) - **Module overrides** — 11 new `default.php` layout overrides for Joomla core modules: `mod_custom`, `mod_articles_latest`, `mod_articles_popular`, `mod_articles_news`, `mod_articles_category`, `mod_breadcrumbs`, `mod_footer`, `mod_login`, `mod_finder`, `mod_tags_popular`, `mod_tags_similar`, `mod_related_items` - **Module title support** — All module overrides respect `$module->showtitle`, `header_tag`, `header_class`, and `moduleclass_sfx` parameters @@ -79,10 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Hero mobile breakpoint** — Photo background hidden on mobile (≤767.98px), hero card becomes full-bleed (100dvh, no border-radius) - **CSS fallback values** — 1365 `var()` calls in template.css now include inline fallback values - **Card border-radius** — `.card` now has `.25rem` fallback on `var(--card-border-radius)` -- **Usage section in README** — Added missing "Usage" section required by MokoStandards ### Changed - - **Button backgrounds** — `--btn-bg: transparent` changed to `var(--body-bg)` in dark and light themes - **Offcanvas close button** — `.offcanvas-header .btn-close` now gets `background-color` from `--offcanvas-bg` - **Custom template sync** — Both `dark.custom.css` and `light.custom.css` now contain all variables from their standard counterparts (was missing 223 variables) @@ -90,13 +100,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Legacy CSS cleanup** — Removed vendor prefixes (`-webkit-box`, `-ms-flexbox`) from `.overlay` rules, replaced with modern flexbox ### Removed - - **FILE INFORMATION headers** — Stripped DEFGROUP/INGROUP/PATH/VERSION/BRIEF metadata from all PHP, CSS, JS, INI, and HTML files (kept in XML and README per policy) - **Mobile overrides** — Deleted 26 `mobile.php` layout files and their empty parent directories - **Joomla-specific gitignore entries** — Removed ~700 lines of Joomla CMS core paths from `.gitignore` (not applicable to a template repository) ### Fixed - - **CI: composer install** — Workflow `standards-compliance.yml` now conditionally runs `composer install` only when `composer.json` exists - **CI: YAML syntax** — Fixed invalid YAML in `auto-update-sha.yml` caused by multiline commit message in run block diff --git a/Makefile b/Makefile index 2263186..16e381c 100644 --- a/Makefile +++ b/Makefile @@ -121,8 +121,22 @@ clean: ## Clean build artifacts @rm -rf $(BUILD_DIR) $(DIST_DIR) @echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)" +MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform)) +MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js + +.PHONY: minify +minify: ## Minify CSS/JS assets (requires terser + clean-css) + @echo "$(COLOR_BLUE)Minifying assets...$(COLOR_RESET)" + @if [ -f "$(MINIFY_SCRIPT)" ]; then \ + node "$(MINIFY_SCRIPT)" $(SRC_DIR); \ + elif [ -f "scripts/minify.js" ]; then \ + node scripts/minify.js; \ + else \ + echo "$(COLOR_YELLOW)⚠ No minify script found$(COLOR_RESET)"; \ + fi + .PHONY: build -build: clean ## Build template installable ZIP from src/ +build: clean minify ## Build template installable ZIP from src/ @echo "$(COLOR_BLUE)Building $(EXTENSION_NAME) v$(EXTENSION_VERSION)...$(COLOR_RESET)" @mkdir -p $(BUILD_DIR)/package $(DIST_DIR) @cp -r $(SRC_DIR)/* $(BUILD_DIR)/package/ diff --git a/README.md b/README.md index 017d977..7a25634 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,118 @@ - - # MokoOnyx -> **MokoOnyx** is the successor to MokoCassiopeia. On install, it automatically migrates your settings, content references, and custom files. After installing, MokoCassiopeia can be safely uninstalled. +MokoOnyx - Joomla site template (successor to MokoCassiopeia) -**A Modern, Lightweight Joomla Template Based on Cassiopeia** +![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green?style=flat-square) ![Wiki](https://img.shields.io/badge/wiki-MokoOnyx-blue?style=flat-square) -[![Version](https://img.shields.io/gitea/v/release/MokoConsulting/MokoOnyx?gitea_url=https%3A%2F%2Fgit.mokoconsulting.tech&logo=gitea&logoColor=white&label=version)](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable) -[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white)](LICENSE) -[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) -[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) -MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)** -- all while maintaining minimal core template overrides for maximum upgrade compatibility. - -## Features - -- **Built on Cassiopeia**: Extends Joomla's default template with minimal overrides -- **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons -- **Bootstrap 5**: Extended utility classes and responsive grid system -- **Template Overrides**: Includes overrides for all core Joomla modules, Community Builder, and DPCalendar -- **Dark Mode Support**: Built-in light/dark mode toggle with system preference detection -- **Google Tag Manager / GA4**: Optional analytics integrations with smart visitor detection (login status, user group, page type) -- **Table of Contents**: Automatic TOC generation for long articles - -## Requirements - -- **Joomla**: 5.x or 6.x -- **PHP**: 8.1 or higher - -## Installation - -Download the latest `mokoonyx-{version}.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases) and install via Joomla's Extension Manager. - -## License - -This project is licensed under the **GNU General Public License v3.0** - see the [LICENSE](./LICENSE) file for details. +A modern, lightweight Joomla site template built on Cassiopeia with Font Awesome 7, Bootstrap 5, dark mode, and analytics integrations. --- -**Made with love by [Moko Consulting](https://mokoconsulting.tech)** +| | | +|---|---| +| **Type** | Joomla Site Template | +| **Version** | 02.01.06 | +| **Joomla** | 5.x / 6.x | +| **PHP** | 8.1+ | +| **License** | GPL-3.0-or-later | +| **Replaces** | MokoCassiopeia (auto-migrates on install) | +| **Repository** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) (primary) | + +--- + +## Overview + +MokoOnyx is an enhancement layer built on Joomla's Cassiopeia template. It maintains minimal core overrides for maximum upgrade compatibility while adding modern features and integrations. + +On install, MokoOnyx automatically migrates settings, content references, and custom files from MokoCassiopeia. After installing, MokoCassiopeia can be safely uninstalled. + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **Font Awesome 7** | Fully integrated into Joomla's asset manager with 2,000+ icons | +| **Bootstrap 5** | Extended utility classes and responsive grid system | +| **Dark Mode** | Built-in light/dark toggle with system preference detection | +| **Table of Contents** | Automatic TOC generation for long articles | +| **GTM / GA4** | Google Tag Manager and Analytics integration with smart visitor detection (login status, user group, page type) | +| **Template Overrides** | Overrides for all core Joomla modules, Community Builder, and DPCalendar | +| **Cassiopeia Base** | Minimal core overrides for maximum Joomla upgrade compatibility | + +--- + +## Installation + +1. Download the latest `mokoonyx-{version}.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases) +2. Install via Joomla's Extension Manager (Extensions > Manage > Install) +3. Set MokoOnyx as the default site template (System > Site Templates) +4. If migrating from MokoCassiopeia, settings are auto-migrated on install + +### Requirements + +- Joomla 5.x or 6.x +- PHP 8.1 or higher + +--- + +## Directory Structure + +| Directory | Purpose | +|-----------|---------| +| `html/` | Template overrides for core modules and extensions | +| `css/` | Compiled stylesheets including dark mode | +| `js/` | JavaScript for TOC, dark mode toggle, analytics | +| `images/` | Template images and icons | +| `language/` | Language files | + +--- + +## Configuration + +MokoOnyx template parameters are configured in the Joomla admin under System > Site Templates > MokoOnyx. + +Key parameters include: +- **Dark Mode**: Enable/disable, set default mode +- **Font Awesome**: Enable/disable icon library loading +- **Google Tag Manager**: GTM container ID +- **GA4**: Measurement ID and tracking options +- **Table of Contents**: Auto-generate TOC for articles with heading threshold + +--- + +## Related Wikis + +| Repo | Purpose | +|------|---------| +| [Template-Client-WaaS](https://git.mokoconsulting.tech/MokoConsulting/Template-Client-WaaS/wiki) | Client site template (extends MokoOnyx) | +| [MokoWaaS](https://git.mokoconsulting.tech/MokoConsulting/MokoWaaS/wiki) | WaaS system plugin | +| [joomla-api-mcp](https://git.mokoconsulting.tech/MokoConsulting/joomla-api-mcp/wiki) | Joomla Web Services API MCP | +| [deploy-mcp](https://git.mokoconsulting.tech/MokoConsulting/deploy-mcp/wiki) | Git-based deployment MCP | + +--- + +> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects. + +--- + + + +--- + +## Documentation + +Full documentation is available on the [Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki). + +## Contributing + +See the wiki for development guidelines and contribution instructions. + +## License + +This project is licensed under the GNU General Public License v3.0 or later -- see the [LICENSE](LICENSE) file. + +--- + +*[Moko Consulting](https://mokoconsulting.tech) -- [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)* diff --git a/src/component.php b/src/component.php index fb329e4..84fbcd8 100644 --- a/src/component.php +++ b/src/component.php @@ -47,15 +47,26 @@ $pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : ''; // Template/Media path $templatePath = 'media/templates/site/mokoonyx'; -// Core template CSS -$wa->useStyle('template.base'); // css/template.css +// Component / print-view CSS (replaces template.css for this view) +$wa->useStyle('template.component'); // css/component.css -// Component always uses light theme only (no theme switching) +// Light theme only (no theme switching in component view) $wa->useStyle('template.light.standard'); // css/theme/light.standard.css +// Load custom light palette if selected in template configuration and file exists +$params_LightColorName = (string) $this->params->get('colorLightName', 'standard'); +if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/light.custom.css')) +{ + $wa->useStyle('template.light.custom'); +} + // Load Osaka font for site title $wa->useStyle('template.font.osaka'); +// Load user assets last (after all other styles and scripts) +$wa->useStyle('template.user'); // css/user.css +$wa->useScript('user.js'); // js/user.js + // Brand: logo from params OR siteTitle // ------------------------------------- $brandHtml = ''; @@ -129,7 +140,7 @@ if ($logoFile !== '') { }); (function(id){ if (/^G-/.test(id)) { - gtag('config', id, { 'anonymize_ip': true }); + gtag('config', id, { 'anonymize_ip': true, 'content_group': 'print_view' }); } else if (/^UA-/.test(id)) { gtag('config', id, { 'anonymize_ip': true }); console.warn('Using a UA- ID. Universal Analytics is sunset; consider migrating to GA4.'); @@ -137,6 +148,12 @@ if ($logoFile !== '') { console.warn('Unrecognized Google Analytics ID format:', id); } })(''); + gtag('event', 'page_view', { + 'page_title': document.title, + 'content_group': 'print_view', + 'custom_map': {'dimension1': 'template_view'}, + 'template_view': 'component' + }); diff --git a/src/error.php b/src/error.php index 34dcc3a..9a797c0 100644 --- a/src/error.php +++ b/src/error.php @@ -27,8 +27,6 @@ $params_googletagmanager = $params->get('googletagmanager', false); $params_googletagmanagerid = $params->get('googletagmanagerid', ''); $params_googleanalytics = $params->get('googleanalytics', false); $params_googleanalyticsid = $params->get('googleanalyticsid', ''); -$params_custom_head_start = $params->get('custom_head_start', ''); -$params_custom_head_end = $params->get('custom_head_end', ''); $params_developmentmode = $params->get('developmentmode', false); // ------------------ Params ------------------ @@ -154,7 +152,6 @@ $wa->useScript('user.js'); // js/user.js - - diff --git a/src/html/com_content/article/toc-right.php b/src/html/com_content/article/toc-right.php index a2bc9c6..1c6652d 100644 --- a/src/html/com_content/article/toc-right.php +++ b/src/html/com_content/article/toc-right.php @@ -90,6 +90,10 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations')); item->event->afterDisplayContent; ?> + item->jcfields)) : ?> + $this->item]); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> item->tags->itemTags); ?> @@ -132,4 +136,77 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations')); margin-bottom: 1.5rem; } } + +/* Article Metadata Footer */ +.article-metadata-footer { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 2px solid var(--cassiopeia-color-border, #dee2e6); +} + +.article-metadata-footer__group { + margin-bottom: 1.25rem; +} + +.article-metadata-footer__group:last-child { + margin-bottom: 0; +} + +.article-metadata-footer__group-title { + font-size: 0.8125rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--cassiopeia-color-text, #212529); + margin-bottom: 0.5rem; + padding-bottom: 0.25rem; + border-bottom: 1px solid var(--cassiopeia-color-border, #dee2e6); +} + +.article-metadata-footer__fields { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.5rem 1.5rem; + margin: 0; +} + +.article-metadata-footer__field { + display: flex; + flex-direction: column; +} + +.article-metadata-footer__label { + font-size: 0.75rem; + font-weight: 600; + color: var(--cassiopeia-color-text, #6c757d); + opacity: 0.7; + margin-bottom: 0.125rem; +} + +.article-metadata-footer__value { + font-size: 0.875rem; + color: var(--cassiopeia-color-text, #212529); + margin: 0; + word-break: break-word; +} + +.article-metadata-footer__value a { + color: var(--cassiopeia-color-link, #0d6efd); + text-decoration: none; +} + +.article-metadata-footer__value a:hover { + text-decoration: underline; +} + +/* Full-width for textarea fields */ +.article-metadata-footer__field[data-field-type="textarea"] { + grid-column: 1 / -1; +} + +@media (max-width: 575.98px) { + .article-metadata-footer__fields { + grid-template-columns: 1fr; + } +} diff --git a/src/html/layouts/mokoonyx/article-metadata.php b/src/html/layouts/mokoonyx/article-metadata.php new file mode 100644 index 0000000..418f164 --- /dev/null +++ b/src/html/layouts/mokoonyx/article-metadata.php @@ -0,0 +1,76 @@ + + * + * This file is part of a Moko Consulting project. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * FILE INFORMATION + * DEFGROUP: Joomla.Template.Site + * INGROUP: MokoOnyx.Layouts + * REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx + * PATH: /src/html/layouts/mokoonyx/article-metadata.php + * VERSION: 03.09.04 + * BRIEF: Article metadata footer layout -- renders jcfields grouped by field group + */ + +defined('_JEXEC') or die; + +/** + * Layout: mokoonyx.article-metadata + * + * Renders custom fields as a styled metadata footer, mirroring the + * MokoStandards wiki metadata footer pattern. Fields are grouped + * by their field group and rendered dynamically -- adding new fields + * or groups requires no template changes. + * + * @var array $displayData Expects ['item' => ArticleObject] + */ + +$item = $displayData['item'] ?? null; + +if (!$item || empty($item->jcfields)) { + return; +} + +// Group fields by their group title. Fields without a group go under "Article Metadata". +$groups = []; + +foreach ($item->jcfields as $field) { + // Skip fields with no value set + if ($field->rawvalue === '' || $field->rawvalue === null) { + continue; + } + + $groupTitle = $field->group_title ?: 'Article Metadata'; + $groups[$groupTitle][] = $field; +} + +if (empty($groups)) { + return; +} +?> + diff --git a/src/html/layouts/mokoonyx/index.html b/src/html/layouts/mokoonyx/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/src/html/layouts/mokoonyx/index.html @@ -0,0 +1 @@ + diff --git a/src/index.php b/src/index.php index 00a1653..aea6227 100644 --- a/src/index.php +++ b/src/index.php @@ -38,8 +38,6 @@ $params_googleanalytics = $this->params->get('googleanalytics', false); $params_googleanalyticsid = $this->params->get('googleanalyticsid', null); $params_googlesitekey = $this->params->get('googlesitekey', null); $params_visitordetection = $this->params->get('googlevisitordetection', true); -$params_custom_head_start = $this->params->get('custom_head_start', null); -$params_custom_head_end = $this->params->get('custom_head_end', null); $params_developmentmode = $this->params->get('developmentmode', false) || $app->get('debug', false); $params_favicon_source = (string) $this->params->get('favicon_source', ''); @@ -110,14 +108,9 @@ if ($params_favicon_source) { require_once __DIR__ . '/helper/minify.php'; MokoMinifyHelper::sync(JPATH_ROOT . '/' . $templatePath, (bool) $params_developmentmode); -// Core template CSS + JS — use minified when not in development mode -if ($params_developmentmode) { - $wa->useStyle('template.base'); // css/template.css - $wa->useScript('template.js'); // js/template.js -} else { - $wa->useStyle('template.base.min'); // css/template.min.css - $wa->useScript('template.js.min'); // js/template.min.js -} +// Core template CSS + JS — Joomla auto-serves .min when debug is off +$wa->useStyle('template.base'); // css/template.css (or .min.css) +$wa->useScript('template.js'); // js/template.js (or .min.js) // Load Osaka font for site title $wa->useStyle('template.font.osaka'); @@ -227,7 +220,7 @@ if ($faKitCode !== '') { } else { // Load local FA7 Free — all.css via WebAsset // Resolve the actual filesystem path: media dir (Joomla install) or template dir (SFTP deploy) - $faCssFile = $params_developmentmode ? 'vendor/fa7free/css/all.css' : 'vendor/fa7free/css/all.min.css'; + $faCssFile = 'vendor/fa7free/css/all.min.css'; // vendor ships minified only $faCandidates = [ $templatePath . '/' . $faCssFile, // media/templates/site/mokoonyx/... 'templates/site/' . $this->template . '/media/' . $faCssFile, // templates/site/mokoonyx/media/... @@ -258,19 +251,18 @@ if ($faKitCode !== '') { $params_leftIcon = htmlspecialchars($this->params->get('drawerLeftIcon', 'fa-solid fa-chevron-left'), ENT_COMPAT, 'UTF-8'); $params_rightIcon = htmlspecialchars($this->params->get('drawerRightIcon', 'fa-solid fa-chevron-right'), ENT_COMPAT, 'UTF-8'); -// Load theme palette stylesheets — minified when not in development mode -$suffix = $params_developmentmode ? '' : '.min'; -$wa->useStyle('template.light.standard' . $suffix); -$wa->useStyle('template.dark.standard' . $suffix); +// Load theme palette stylesheets — Joomla auto-serves .min when debug is off +$wa->useStyle('template.light.standard'); +$wa->useStyle('template.dark.standard'); // Load custom palettes only if selected in template configuration AND files exist if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/light.custom.css')) { - $wa->useStyle('template.light.custom' . $suffix); + $wa->useStyle('template.light.custom'); } if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokoonyx/css/theme/dark.custom.css')) { - $wa->useStyle('template.dark.custom' . $suffix); + $wa->useStyle('template.dark.custom'); } // Load user assets last (after all other styles and scripts) @@ -280,7 +272,6 @@ $wa->useScript('user.js'); // js/user.js - @@ -327,7 +318,6 @@ $wa->useScript('user.js'); // js/user.js - + + This file is part of a Moko Consulting project. + + SPDX-License-Identifier: GPL-3.0-or-later + */ + +/* ========================================================================== + Component / Print-view stylesheet + Used by component.php (tmpl=component) for modals, popups, and print. + Consumes theme variables from light.standard.css (always light in this view). + ========================================================================== */ + +/* ---- Screen: minimal chrome for modal / popup views ---- */ + +body { + margin: 0; + padding: 1.5rem; + font-family: var(--body-font-family); + font-size: var(--body-font-size, 1rem); + font-weight: var(--body-font-weight, 400); + line-height: var(--body-line-height, 1.5); + color: var(--body-color, #22262a); + background: var(--body-bg, #fff); +} + +.navbar-brand { + display: flex; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: var(--border-width, 1px) var(--border-style, solid) var(--border-color, #dfe3e7); +} + +.navbar-brand .brand-logo { + text-decoration: none; + color: var(--color-primary, #112855); +} + +.navbar-brand .logo { + max-height: 48px; + width: auto; +} + +.navbar-brand .site-title { + font-size: var(--navbar-brand-font-size, 1.25rem); + font-weight: 600; + color: var(--color-primary, #112855); +} + +a { + color: var(--link-color, #224faa); + text-decoration: var(--link-decoration, underline); +} + +a:hover { + color: var(--link-hover-color, #424077); +} + +h1, h2, h3, h4, h5, h6 { + color: var(--heading-color, inherit); + margin-top: 0; + margin-bottom: 0.5rem; +} + +img { + max-width: 100%; + height: auto; +} + +table { + border-collapse: collapse; + width: 100%; + color: var(--table-color, var(--body-color)); + background-color: var(--table-bg, transparent); +} + +th, td { + padding: 0.5rem; + border: var(--border-width, 1px) var(--border-style, solid) var(--table-border-color, var(--border-color, #dfe3e7)); + text-align: left; + vertical-align: top; +} + +th { + font-weight: 600; + color: var(--emphasis-color, #000); +} + +.container-footer { + margin-top: 2rem; + padding-top: 0.75rem; + border-top: var(--border-width, 1px) var(--border-style, solid) var(--border-color, #dfe3e7); + font-size: 0.85rem; + color: var(--muted-color, #6d757e); +} + +pre, code { + font-family: var(--font-monospace); + color: var(--code-color-ink, #e93f8e); +} + +pre { + background: var(--tertiary-bg, #f9fafb); + padding: 1rem; + border-radius: var(--border-radius, .25rem); + overflow-x: auto; +} + +blockquote { + padding: 0.5rem 1rem; + margin: 0 0 1rem; + border-left: 4px solid var(--accent-color-primary, #3f8ff0); + color: var(--secondary-color, #22262abf); +} + +hr { + border: 0; + border-top: var(--border-width, 1px) solid var(--hr-color, #dfe3e7); + margin: 1rem 0; +} + +/* ---- Print ---- */ + +@media print { + @page { + margin: 1.5cm; + size: auto; + } + + *, + *::before, + *::after { + color: #000 !important; + background: transparent !important; + box-shadow: none !important; + text-shadow: none !important; + } + + body { + padding: 0; + font-size: 11pt; + line-height: 1.4; + } + + .navbar-brand { + border-bottom: 1px solid #000; + margin-bottom: 0.75cm; + padding-bottom: 0.25cm; + } + + .navbar-brand .logo { + max-height: 36px; + } + + .container-footer, + .alert, + .btn, + iframe, + video, + audio, + [data-module], + #debug, + .joomla-script-options { + display: none !important; + } + + a[href] { + text-decoration: underline !important; + } + + a[href]::after { + content: " (" attr(href) ")"; + font-size: 0.8em; + font-weight: normal; + } + + a[href^="#"]::after, + a[href^="javascript:"]::after, + a[href^="mailto:"]::after { + content: ""; + } + + h1, h2, h3, h4 { + page-break-after: avoid; + orphans: 3; + widows: 3; + } + + img, table, figure, pre, blockquote { + page-break-inside: avoid; + } + + thead { + display: table-header-group; + } + + tr { + page-break-inside: avoid; + } + + p, h2, h3 { + orphans: 3; + widows: 3; + } + + table { + border-color: #000 !important; + } + + th, td { + border-color: #000 !important; + } + + pre { + border: 1px solid #999; + white-space: pre-wrap; + word-wrap: break-word; + } + + blockquote { + border-left-color: #000 !important; + } +} diff --git a/src/media/css/template.css b/src/media/css/template.css index 64e0cce..65927b1 100644 --- a/src/media/css/template.css +++ b/src/media/css/template.css @@ -16386,7 +16386,7 @@ body:not(.has-sidebar-right) .site-grid .container-component { .nav-tabs+.tab-content { padding: 0.9375rem; - background: var(--body-color, #e6ebf1); + background: var(--body-bg, #e6ebf1); border: 1px solid; border-color: hsl(210, 14%, 89%); border-radius: 0 0 0.25rem 0.25rem; diff --git a/src/media/vendor/fa7free/css/all.css b/src/media/vendor/fa7free/css/all.css deleted file mode 100644 index 70bbfcc..0000000 --- a/src/media/vendor/fa7free/css/all.css +++ /dev/null @@ -1,10663 +0,0 @@ -/*! - * Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2025 Fonticons, Inc. - */ -.fa-solid, -.fa-regular, -.fa-brands, -.fa-classic, -.fas, -.far, -.fab, -.fa { - --_fa-family: var(--fa-family, var(--fa-style-family, "Font Awesome 7 Free")); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - display: var(--fa-display, inline-block); - font-family: var(--_fa-family); - font-feature-settings: normal; - font-style: normal; - font-synthesis: none; - font-variant: normal; - font-weight: var(--fa-style, 900); - line-height: 1; - text-align: center; - text-rendering: auto; - width: var(--fa-width, 1.25em); -} - -:is(.fas, -.far, -.fab, -.fa-solid, -.fa-regular, -.fa-brands, -.fa-classic, -.fa)::before { - content: var(--fa)/""; -} - -@supports not (content: ""/"") { - :is(.fas, - .far, - .fab, - .fa-solid, - .fa-regular, - .fa-brands, - .fa-classic, - .fa)::before { - content: var(--fa); - } -} -.fa-1x { - font-size: 1em; -} - -.fa-2x { - font-size: 2em; -} - -.fa-3x { - font-size: 3em; -} - -.fa-4x { - font-size: 4em; -} - -.fa-5x { - font-size: 5em; -} - -.fa-6x { - font-size: 6em; -} - -.fa-7x { - font-size: 7em; -} - -.fa-8x { - font-size: 8em; -} - -.fa-9x { - font-size: 9em; -} - -.fa-10x { - font-size: 10em; -} - -.fa-2xs { - font-size: calc(10 / 16 * 1em); /* converts a 10px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 10 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 10 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-xs { - font-size: calc(12 / 16 * 1em); /* converts a 12px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 12 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 12 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-sm { - font-size: calc(14 / 16 * 1em); /* converts a 14px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 14 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 14 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-lg { - font-size: calc(20 / 16 * 1em); /* converts a 20px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 20 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 20 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-xl { - font-size: calc(24 / 16 * 1em); /* converts a 24px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 24 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 24 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-2xl { - font-size: calc(32 / 16 * 1em); /* converts a 32px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 32 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 32 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-width-auto { - --fa-width: auto; -} - -.fa-fw, -.fa-width-fixed { - --fa-width: 1.25em; -} - -.fa-ul { - list-style-type: none; - margin-inline-start: var(--fa-li-margin, 2.5em); - padding-inline-start: 0; -} -.fa-ul > li { - position: relative; -} - -.fa-li { - inset-inline-start: calc(-1 * var(--fa-li-width, 2em)); - position: absolute; - text-align: center; - width: var(--fa-li-width, 2em); - line-height: inherit; -} - -/* Heads Up: Bordered Icons will not be supported in the future! - - This feature will be deprecated in the next major release of Font Awesome (v8)! - - You may continue to use it in this version *v7), but it will not be supported in Font Awesome v8. -*/ -/* Notes: -* --@{v.$css-prefix}-border-width = 1/16 by default (to render as ~1px based on a 16px default font-size) -* --@{v.$css-prefix}-border-padding = - ** 3/16 for vertical padding (to give ~2px of vertical whitespace around an icon considering it's vertical alignment) - ** 4/16 for horizontal padding (to give ~4px of horizontal whitespace around an icon) -*/ -.fa-border { - border-color: var(--fa-border-color, #eee); - border-radius: var(--fa-border-radius, 0.1em); - border-style: var(--fa-border-style, solid); - border-width: var(--fa-border-width, 0.0625em); - box-sizing: var(--fa-border-box-sizing, content-box); - padding: var(--fa-border-padding, 0.1875em 0.25em); -} - -.fa-pull-left, -.fa-pull-start { - float: inline-start; - margin-inline-end: var(--fa-pull-margin, 0.3em); -} - -.fa-pull-right, -.fa-pull-end { - float: inline-end; - margin-inline-start: var(--fa-pull-margin, 0.3em); -} - -.fa-beat { - animation-name: fa-beat; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, ease-in-out); -} - -.fa-bounce { - animation-name: fa-bounce; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); -} - -.fa-fade { - animation-name: fa-fade; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); -} - -.fa-beat-fade { - animation-name: fa-beat-fade; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); -} - -.fa-flip { - animation-name: fa-flip; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, ease-in-out); -} - -.fa-shake { - animation-name: fa-shake; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, linear); -} - -.fa-spin { - animation-name: fa-spin; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 2s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, linear); -} - -.fa-spin-reverse { - --fa-animation-direction: reverse; -} - -.fa-pulse, -.fa-spin-pulse { - animation-name: fa-spin; - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, steps(8)); -} - -@media (prefers-reduced-motion: reduce) { - .fa-beat, - .fa-bounce, - .fa-fade, - .fa-beat-fade, - .fa-flip, - .fa-pulse, - .fa-shake, - .fa-spin, - .fa-spin-pulse { - animation: none !important; - transition: none !important; - } -} -@keyframes fa-beat { - 0%, 90% { - transform: scale(1); - } - 45% { - transform: scale(var(--fa-beat-scale, 1.25)); - } -} -@keyframes fa-bounce { - 0% { - transform: scale(1, 1) translateY(0); - } - 10% { - transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - } - 30% { - transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - } - 50% { - transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - } - 57% { - transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - } - 64% { - transform: scale(1, 1) translateY(0); - } - 100% { - transform: scale(1, 1) translateY(0); - } -} -@keyframes fa-fade { - 50% { - opacity: var(--fa-fade-opacity, 0.4); - } -} -@keyframes fa-beat-fade { - 0%, 100% { - opacity: var(--fa-beat-fade-opacity, 0.4); - transform: scale(1); - } - 50% { - opacity: 1; - transform: scale(var(--fa-beat-fade-scale, 1.125)); - } -} -@keyframes fa-flip { - 50% { - transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - } -} -@keyframes fa-shake { - 0% { - transform: rotate(-15deg); - } - 4% { - transform: rotate(15deg); - } - 8%, 24% { - transform: rotate(-18deg); - } - 12%, 28% { - transform: rotate(18deg); - } - 16% { - transform: rotate(-22deg); - } - 20% { - transform: rotate(22deg); - } - 32% { - transform: rotate(-12deg); - } - 36% { - transform: rotate(12deg); - } - 40%, 100% { - transform: rotate(0deg); - } -} -@keyframes fa-spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} -.fa-rotate-90 { - transform: rotate(90deg); -} - -.fa-rotate-180 { - transform: rotate(180deg); -} - -.fa-rotate-270 { - transform: rotate(270deg); -} - -.fa-flip-horizontal { - transform: scale(-1, 1); -} - -.fa-flip-vertical { - transform: scale(1, -1); -} - -.fa-flip-both, -.fa-flip-horizontal.fa-flip-vertical { - transform: scale(-1, -1); -} - -.fa-rotate-by { - transform: rotate(var(--fa-rotate-angle, 0)); -} - -.fa-stack { - display: inline-block; - height: 2em; - line-height: 2em; - position: relative; - vertical-align: middle; - width: 2.5em; -} - -.fa-stack-1x, -.fa-stack-2x { - --fa-width: 100%; - inset: 0; - position: absolute; - text-align: center; - width: var(--fa-width); - z-index: var(--fa-stack-z-index, auto); -} - -.fa-stack-1x { - line-height: inherit; -} - -.fa-stack-2x { - font-size: 2em; -} - -.fa-inverse { - color: var(--fa-inverse, #fff); -} - -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.fa-0 { - --fa: "\30 "; -} - -.fa-1 { - --fa: "\31 "; -} - -.fa-2 { - --fa: "\32 "; -} - -.fa-3 { - --fa: "\33 "; -} - -.fa-4 { - --fa: "\34 "; -} - -.fa-5 { - --fa: "\35 "; -} - -.fa-6 { - --fa: "\36 "; -} - -.fa-7 { - --fa: "\37 "; -} - -.fa-8 { - --fa: "\38 "; -} - -.fa-9 { - --fa: "\39 "; -} - -.fa-exclamation { - --fa: "\!"; -} - -.fa-hashtag { - --fa: "\#"; -} - -.fa-dollar-sign { - --fa: "\$"; -} - -.fa-dollar { - --fa: "\$"; -} - -.fa-usd { - --fa: "\$"; -} - -.fa-percent { - --fa: "\%"; -} - -.fa-percentage { - --fa: "\%"; -} - -.fa-asterisk { - --fa: "\*"; -} - -.fa-plus { - --fa: "\+"; -} - -.fa-add { - --fa: "\+"; -} - -.fa-less-than { - --fa: "\<"; -} - -.fa-equals { - --fa: "\="; -} - -.fa-greater-than { - --fa: "\>"; -} - -.fa-question { - --fa: "\?"; -} - -.fa-at { - --fa: "\@"; -} - -.fa-a { - --fa: "A"; -} - -.fa-b { - --fa: "B"; -} - -.fa-c { - --fa: "C"; -} - -.fa-d { - --fa: "D"; -} - -.fa-e { - --fa: "E"; -} - -.fa-f { - --fa: "F"; -} - -.fa-g { - --fa: "G"; -} - -.fa-h { - --fa: "H"; -} - -.fa-i { - --fa: "I"; -} - -.fa-j { - --fa: "J"; -} - -.fa-k { - --fa: "K"; -} - -.fa-l { - --fa: "L"; -} - -.fa-m { - --fa: "M"; -} - -.fa-n { - --fa: "N"; -} - -.fa-o { - --fa: "O"; -} - -.fa-p { - --fa: "P"; -} - -.fa-q { - --fa: "Q"; -} - -.fa-r { - --fa: "R"; -} - -.fa-s { - --fa: "S"; -} - -.fa-t { - --fa: "T"; -} - -.fa-u { - --fa: "U"; -} - -.fa-v { - --fa: "V"; -} - -.fa-w { - --fa: "W"; -} - -.fa-x { - --fa: "X"; -} - -.fa-y { - --fa: "Y"; -} - -.fa-z { - --fa: "Z"; -} - -.fa-faucet { - --fa: "\e005"; -} - -.fa-faucet-drip { - --fa: "\e006"; -} - -.fa-house-chimney-window { - --fa: "\e00d"; -} - -.fa-house-signal { - --fa: "\e012"; -} - -.fa-temperature-arrow-down { - --fa: "\e03f"; -} - -.fa-temperature-down { - --fa: "\e03f"; -} - -.fa-temperature-arrow-up { - --fa: "\e040"; -} - -.fa-temperature-up { - --fa: "\e040"; -} - -.fa-trailer { - --fa: "\e041"; -} - -.fa-bacteria { - --fa: "\e059"; -} - -.fa-bacterium { - --fa: "\e05a"; -} - -.fa-box-tissue { - --fa: "\e05b"; -} - -.fa-hand-holding-medical { - --fa: "\e05c"; -} - -.fa-hand-sparkles { - --fa: "\e05d"; -} - -.fa-hands-bubbles { - --fa: "\e05e"; -} - -.fa-hands-wash { - --fa: "\e05e"; -} - -.fa-handshake-slash { - --fa: "\e060"; -} - -.fa-handshake-alt-slash { - --fa: "\e060"; -} - -.fa-handshake-simple-slash { - --fa: "\e060"; -} - -.fa-head-side-cough { - --fa: "\e061"; -} - -.fa-head-side-cough-slash { - --fa: "\e062"; -} - -.fa-head-side-mask { - --fa: "\e063"; -} - -.fa-head-side-virus { - --fa: "\e064"; -} - -.fa-house-chimney-user { - --fa: "\e065"; -} - -.fa-house-laptop { - --fa: "\e066"; -} - -.fa-laptop-house { - --fa: "\e066"; -} - -.fa-lungs-virus { - --fa: "\e067"; -} - -.fa-people-arrows { - --fa: "\e068"; -} - -.fa-people-arrows-left-right { - --fa: "\e068"; -} - -.fa-plane-slash { - --fa: "\e069"; -} - -.fa-pump-medical { - --fa: "\e06a"; -} - -.fa-pump-soap { - --fa: "\e06b"; -} - -.fa-shield-virus { - --fa: "\e06c"; -} - -.fa-sink { - --fa: "\e06d"; -} - -.fa-soap { - --fa: "\e06e"; -} - -.fa-stopwatch-20 { - --fa: "\e06f"; -} - -.fa-shop-slash { - --fa: "\e070"; -} - -.fa-store-alt-slash { - --fa: "\e070"; -} - -.fa-store-slash { - --fa: "\e071"; -} - -.fa-toilet-paper-slash { - --fa: "\e072"; -} - -.fa-users-slash { - --fa: "\e073"; -} - -.fa-virus { - --fa: "\e074"; -} - -.fa-virus-slash { - --fa: "\e075"; -} - -.fa-viruses { - --fa: "\e076"; -} - -.fa-vest { - --fa: "\e085"; -} - -.fa-vest-patches { - --fa: "\e086"; -} - -.fa-arrow-trend-down { - --fa: "\e097"; -} - -.fa-arrow-trend-up { - --fa: "\e098"; -} - -.fa-arrow-up-from-bracket { - --fa: "\e09a"; -} - -.fa-austral-sign { - --fa: "\e0a9"; -} - -.fa-baht-sign { - --fa: "\e0ac"; -} - -.fa-bitcoin-sign { - --fa: "\e0b4"; -} - -.fa-bolt-lightning { - --fa: "\e0b7"; -} - -.fa-book-bookmark { - --fa: "\e0bb"; -} - -.fa-camera-rotate { - --fa: "\e0d8"; -} - -.fa-cedi-sign { - --fa: "\e0df"; -} - -.fa-chart-column { - --fa: "\e0e3"; -} - -.fa-chart-gantt { - --fa: "\e0e4"; -} - -.fa-clapperboard { - --fa: "\e131"; -} - -.fa-clover { - --fa: "\e139"; -} - -.fa-code-compare { - --fa: "\e13a"; -} - -.fa-code-fork { - --fa: "\e13b"; -} - -.fa-code-pull-request { - --fa: "\e13c"; -} - -.fa-colon-sign { - --fa: "\e140"; -} - -.fa-cruzeiro-sign { - --fa: "\e152"; -} - -.fa-display { - --fa: "\e163"; -} - -.fa-dong-sign { - --fa: "\e169"; -} - -.fa-elevator { - --fa: "\e16d"; -} - -.fa-filter-circle-xmark { - --fa: "\e17b"; -} - -.fa-florin-sign { - --fa: "\e184"; -} - -.fa-folder-closed { - --fa: "\e185"; -} - -.fa-franc-sign { - --fa: "\e18f"; -} - -.fa-guarani-sign { - --fa: "\e19a"; -} - -.fa-gun { - --fa: "\e19b"; -} - -.fa-hands-clapping { - --fa: "\e1a8"; -} - -.fa-house-user { - --fa: "\e1b0"; -} - -.fa-home-user { - --fa: "\e1b0"; -} - -.fa-indian-rupee-sign { - --fa: "\e1bc"; -} - -.fa-indian-rupee { - --fa: "\e1bc"; -} - -.fa-inr { - --fa: "\e1bc"; -} - -.fa-kip-sign { - --fa: "\e1c4"; -} - -.fa-lari-sign { - --fa: "\e1c8"; -} - -.fa-litecoin-sign { - --fa: "\e1d3"; -} - -.fa-manat-sign { - --fa: "\e1d5"; -} - -.fa-mask-face { - --fa: "\e1d7"; -} - -.fa-mill-sign { - --fa: "\e1ed"; -} - -.fa-money-bills { - --fa: "\e1f3"; -} - -.fa-naira-sign { - --fa: "\e1f6"; -} - -.fa-notdef { - --fa: "\e1fe"; -} - -.fa-panorama { - --fa: "\e209"; -} - -.fa-peseta-sign { - --fa: "\e221"; -} - -.fa-peso-sign { - --fa: "\e222"; -} - -.fa-plane-up { - --fa: "\e22d"; -} - -.fa-rupiah-sign { - --fa: "\e23d"; -} - -.fa-stairs { - --fa: "\e289"; -} - -.fa-timeline { - --fa: "\e29c"; -} - -.fa-truck-front { - --fa: "\e2b7"; -} - -.fa-turkish-lira-sign { - --fa: "\e2bb"; -} - -.fa-try { - --fa: "\e2bb"; -} - -.fa-turkish-lira { - --fa: "\e2bb"; -} - -.fa-vault { - --fa: "\e2c5"; -} - -.fa-wand-magic-sparkles { - --fa: "\e2ca"; -} - -.fa-magic-wand-sparkles { - --fa: "\e2ca"; -} - -.fa-wheat-awn { - --fa: "\e2cd"; -} - -.fa-wheat-alt { - --fa: "\e2cd"; -} - -.fa-wheelchair-move { - --fa: "\e2ce"; -} - -.fa-wheelchair-alt { - --fa: "\e2ce"; -} - -.fa-bangladeshi-taka-sign { - --fa: "\e2e6"; -} - -.fa-bowl-rice { - --fa: "\e2eb"; -} - -.fa-person-pregnant { - --fa: "\e31e"; -} - -.fa-house-chimney { - --fa: "\e3af"; -} - -.fa-home-lg { - --fa: "\e3af"; -} - -.fa-house-crack { - --fa: "\e3b1"; -} - -.fa-house-medical { - --fa: "\e3b2"; -} - -.fa-cent-sign { - --fa: "\e3f5"; -} - -.fa-plus-minus { - --fa: "\e43c"; -} - -.fa-sailboat { - --fa: "\e445"; -} - -.fa-section { - --fa: "\e447"; -} - -.fa-shrimp { - --fa: "\e448"; -} - -.fa-brazilian-real-sign { - --fa: "\e46c"; -} - -.fa-chart-simple { - --fa: "\e473"; -} - -.fa-diagram-next { - --fa: "\e476"; -} - -.fa-diagram-predecessor { - --fa: "\e477"; -} - -.fa-diagram-successor { - --fa: "\e47a"; -} - -.fa-earth-oceania { - --fa: "\e47b"; -} - -.fa-globe-oceania { - --fa: "\e47b"; -} - -.fa-bug-slash { - --fa: "\e490"; -} - -.fa-file-circle-plus { - --fa: "\e494"; -} - -.fa-shop-lock { - --fa: "\e4a5"; -} - -.fa-virus-covid { - --fa: "\e4a8"; -} - -.fa-virus-covid-slash { - --fa: "\e4a9"; -} - -.fa-anchor-circle-check { - --fa: "\e4aa"; -} - -.fa-anchor-circle-exclamation { - --fa: "\e4ab"; -} - -.fa-anchor-circle-xmark { - --fa: "\e4ac"; -} - -.fa-anchor-lock { - --fa: "\e4ad"; -} - -.fa-arrow-down-up-across-line { - --fa: "\e4af"; -} - -.fa-arrow-down-up-lock { - --fa: "\e4b0"; -} - -.fa-arrow-right-to-city { - --fa: "\e4b3"; -} - -.fa-arrow-up-from-ground-water { - --fa: "\e4b5"; -} - -.fa-arrow-up-from-water-pump { - --fa: "\e4b6"; -} - -.fa-arrow-up-right-dots { - --fa: "\e4b7"; -} - -.fa-arrows-down-to-line { - --fa: "\e4b8"; -} - -.fa-arrows-down-to-people { - --fa: "\e4b9"; -} - -.fa-arrows-left-right-to-line { - --fa: "\e4ba"; -} - -.fa-arrows-spin { - --fa: "\e4bb"; -} - -.fa-arrows-split-up-and-left { - --fa: "\e4bc"; -} - -.fa-arrows-to-circle { - --fa: "\e4bd"; -} - -.fa-arrows-to-dot { - --fa: "\e4be"; -} - -.fa-arrows-to-eye { - --fa: "\e4bf"; -} - -.fa-arrows-turn-right { - --fa: "\e4c0"; -} - -.fa-arrows-turn-to-dots { - --fa: "\e4c1"; -} - -.fa-arrows-up-to-line { - --fa: "\e4c2"; -} - -.fa-bore-hole { - --fa: "\e4c3"; -} - -.fa-bottle-droplet { - --fa: "\e4c4"; -} - -.fa-bottle-water { - --fa: "\e4c5"; -} - -.fa-bowl-food { - --fa: "\e4c6"; -} - -.fa-boxes-packing { - --fa: "\e4c7"; -} - -.fa-bridge { - --fa: "\e4c8"; -} - -.fa-bridge-circle-check { - --fa: "\e4c9"; -} - -.fa-bridge-circle-exclamation { - --fa: "\e4ca"; -} - -.fa-bridge-circle-xmark { - --fa: "\e4cb"; -} - -.fa-bridge-lock { - --fa: "\e4cc"; -} - -.fa-bridge-water { - --fa: "\e4ce"; -} - -.fa-bucket { - --fa: "\e4cf"; -} - -.fa-bugs { - --fa: "\e4d0"; -} - -.fa-building-circle-arrow-right { - --fa: "\e4d1"; -} - -.fa-building-circle-check { - --fa: "\e4d2"; -} - -.fa-building-circle-exclamation { - --fa: "\e4d3"; -} - -.fa-building-circle-xmark { - --fa: "\e4d4"; -} - -.fa-building-flag { - --fa: "\e4d5"; -} - -.fa-building-lock { - --fa: "\e4d6"; -} - -.fa-building-ngo { - --fa: "\e4d7"; -} - -.fa-building-shield { - --fa: "\e4d8"; -} - -.fa-building-un { - --fa: "\e4d9"; -} - -.fa-building-user { - --fa: "\e4da"; -} - -.fa-building-wheat { - --fa: "\e4db"; -} - -.fa-burst { - --fa: "\e4dc"; -} - -.fa-car-on { - --fa: "\e4dd"; -} - -.fa-car-tunnel { - --fa: "\e4de"; -} - -.fa-child-combatant { - --fa: "\e4e0"; -} - -.fa-child-rifle { - --fa: "\e4e0"; -} - -.fa-children { - --fa: "\e4e1"; -} - -.fa-circle-nodes { - --fa: "\e4e2"; -} - -.fa-clipboard-question { - --fa: "\e4e3"; -} - -.fa-cloud-showers-water { - --fa: "\e4e4"; -} - -.fa-computer { - --fa: "\e4e5"; -} - -.fa-cubes-stacked { - --fa: "\e4e6"; -} - -.fa-envelope-circle-check { - --fa: "\e4e8"; -} - -.fa-explosion { - --fa: "\e4e9"; -} - -.fa-ferry { - --fa: "\e4ea"; -} - -.fa-file-circle-exclamation { - --fa: "\e4eb"; -} - -.fa-file-circle-minus { - --fa: "\e4ed"; -} - -.fa-file-circle-question { - --fa: "\e4ef"; -} - -.fa-file-shield { - --fa: "\e4f0"; -} - -.fa-fire-burner { - --fa: "\e4f1"; -} - -.fa-fish-fins { - --fa: "\e4f2"; -} - -.fa-flask-vial { - --fa: "\e4f3"; -} - -.fa-glass-water { - --fa: "\e4f4"; -} - -.fa-glass-water-droplet { - --fa: "\e4f5"; -} - -.fa-group-arrows-rotate { - --fa: "\e4f6"; -} - -.fa-hand-holding-hand { - --fa: "\e4f7"; -} - -.fa-handcuffs { - --fa: "\e4f8"; -} - -.fa-hands-bound { - --fa: "\e4f9"; -} - -.fa-hands-holding-child { - --fa: "\e4fa"; -} - -.fa-hands-holding-circle { - --fa: "\e4fb"; -} - -.fa-heart-circle-bolt { - --fa: "\e4fc"; -} - -.fa-heart-circle-check { - --fa: "\e4fd"; -} - -.fa-heart-circle-exclamation { - --fa: "\e4fe"; -} - -.fa-heart-circle-minus { - --fa: "\e4ff"; -} - -.fa-heart-circle-plus { - --fa: "\e500"; -} - -.fa-heart-circle-xmark { - --fa: "\e501"; -} - -.fa-helicopter-symbol { - --fa: "\e502"; -} - -.fa-helmet-un { - --fa: "\e503"; -} - -.fa-hill-avalanche { - --fa: "\e507"; -} - -.fa-hill-rockslide { - --fa: "\e508"; -} - -.fa-house-circle-check { - --fa: "\e509"; -} - -.fa-house-circle-exclamation { - --fa: "\e50a"; -} - -.fa-house-circle-xmark { - --fa: "\e50b"; -} - -.fa-house-fire { - --fa: "\e50c"; -} - -.fa-house-flag { - --fa: "\e50d"; -} - -.fa-house-flood-water { - --fa: "\e50e"; -} - -.fa-house-flood-water-circle-arrow-right { - --fa: "\e50f"; -} - -.fa-house-lock { - --fa: "\e510"; -} - -.fa-house-medical-circle-check { - --fa: "\e511"; -} - -.fa-house-medical-circle-exclamation { - --fa: "\e512"; -} - -.fa-house-medical-circle-xmark { - --fa: "\e513"; -} - -.fa-house-medical-flag { - --fa: "\e514"; -} - -.fa-house-tsunami { - --fa: "\e515"; -} - -.fa-jar { - --fa: "\e516"; -} - -.fa-jar-wheat { - --fa: "\e517"; -} - -.fa-jet-fighter-up { - --fa: "\e518"; -} - -.fa-jug-detergent { - --fa: "\e519"; -} - -.fa-kitchen-set { - --fa: "\e51a"; -} - -.fa-land-mine-on { - --fa: "\e51b"; -} - -.fa-landmark-flag { - --fa: "\e51c"; -} - -.fa-laptop-file { - --fa: "\e51d"; -} - -.fa-lines-leaning { - --fa: "\e51e"; -} - -.fa-location-pin-lock { - --fa: "\e51f"; -} - -.fa-locust { - --fa: "\e520"; -} - -.fa-magnifying-glass-arrow-right { - --fa: "\e521"; -} - -.fa-magnifying-glass-chart { - --fa: "\e522"; -} - -.fa-mars-and-venus-burst { - --fa: "\e523"; -} - -.fa-mask-ventilator { - --fa: "\e524"; -} - -.fa-mattress-pillow { - --fa: "\e525"; -} - -.fa-mobile-retro { - --fa: "\e527"; -} - -.fa-money-bill-transfer { - --fa: "\e528"; -} - -.fa-money-bill-trend-up { - --fa: "\e529"; -} - -.fa-money-bill-wheat { - --fa: "\e52a"; -} - -.fa-mosquito { - --fa: "\e52b"; -} - -.fa-mosquito-net { - --fa: "\e52c"; -} - -.fa-mound { - --fa: "\e52d"; -} - -.fa-mountain-city { - --fa: "\e52e"; -} - -.fa-mountain-sun { - --fa: "\e52f"; -} - -.fa-oil-well { - --fa: "\e532"; -} - -.fa-people-group { - --fa: "\e533"; -} - -.fa-people-line { - --fa: "\e534"; -} - -.fa-people-pulling { - --fa: "\e535"; -} - -.fa-people-robbery { - --fa: "\e536"; -} - -.fa-people-roof { - --fa: "\e537"; -} - -.fa-person-arrow-down-to-line { - --fa: "\e538"; -} - -.fa-person-arrow-up-from-line { - --fa: "\e539"; -} - -.fa-person-breastfeeding { - --fa: "\e53a"; -} - -.fa-person-burst { - --fa: "\e53b"; -} - -.fa-person-cane { - --fa: "\e53c"; -} - -.fa-person-chalkboard { - --fa: "\e53d"; -} - -.fa-person-circle-check { - --fa: "\e53e"; -} - -.fa-person-circle-exclamation { - --fa: "\e53f"; -} - -.fa-person-circle-minus { - --fa: "\e540"; -} - -.fa-person-circle-plus { - --fa: "\e541"; -} - -.fa-person-circle-question { - --fa: "\e542"; -} - -.fa-person-circle-xmark { - --fa: "\e543"; -} - -.fa-person-dress-burst { - --fa: "\e544"; -} - -.fa-person-drowning { - --fa: "\e545"; -} - -.fa-person-falling { - --fa: "\e546"; -} - -.fa-person-falling-burst { - --fa: "\e547"; -} - -.fa-person-half-dress { - --fa: "\e548"; -} - -.fa-person-harassing { - --fa: "\e549"; -} - -.fa-person-military-pointing { - --fa: "\e54a"; -} - -.fa-person-military-rifle { - --fa: "\e54b"; -} - -.fa-person-military-to-person { - --fa: "\e54c"; -} - -.fa-person-rays { - --fa: "\e54d"; -} - -.fa-person-rifle { - --fa: "\e54e"; -} - -.fa-person-shelter { - --fa: "\e54f"; -} - -.fa-person-walking-arrow-loop-left { - --fa: "\e551"; -} - -.fa-person-walking-arrow-right { - --fa: "\e552"; -} - -.fa-person-walking-dashed-line-arrow-right { - --fa: "\e553"; -} - -.fa-person-walking-luggage { - --fa: "\e554"; -} - -.fa-plane-circle-check { - --fa: "\e555"; -} - -.fa-plane-circle-exclamation { - --fa: "\e556"; -} - -.fa-plane-circle-xmark { - --fa: "\e557"; -} - -.fa-plane-lock { - --fa: "\e558"; -} - -.fa-plate-wheat { - --fa: "\e55a"; -} - -.fa-plug-circle-bolt { - --fa: "\e55b"; -} - -.fa-plug-circle-check { - --fa: "\e55c"; -} - -.fa-plug-circle-exclamation { - --fa: "\e55d"; -} - -.fa-plug-circle-minus { - --fa: "\e55e"; -} - -.fa-plug-circle-plus { - --fa: "\e55f"; -} - -.fa-plug-circle-xmark { - --fa: "\e560"; -} - -.fa-ranking-star { - --fa: "\e561"; -} - -.fa-road-barrier { - --fa: "\e562"; -} - -.fa-road-bridge { - --fa: "\e563"; -} - -.fa-road-circle-check { - --fa: "\e564"; -} - -.fa-road-circle-exclamation { - --fa: "\e565"; -} - -.fa-road-circle-xmark { - --fa: "\e566"; -} - -.fa-road-lock { - --fa: "\e567"; -} - -.fa-road-spikes { - --fa: "\e568"; -} - -.fa-rug { - --fa: "\e569"; -} - -.fa-sack-xmark { - --fa: "\e56a"; -} - -.fa-school-circle-check { - --fa: "\e56b"; -} - -.fa-school-circle-exclamation { - --fa: "\e56c"; -} - -.fa-school-circle-xmark { - --fa: "\e56d"; -} - -.fa-school-flag { - --fa: "\e56e"; -} - -.fa-school-lock { - --fa: "\e56f"; -} - -.fa-sheet-plastic { - --fa: "\e571"; -} - -.fa-shield-cat { - --fa: "\e572"; -} - -.fa-shield-dog { - --fa: "\e573"; -} - -.fa-shield-heart { - --fa: "\e574"; -} - -.fa-square-nfi { - --fa: "\e576"; -} - -.fa-square-person-confined { - --fa: "\e577"; -} - -.fa-square-virus { - --fa: "\e578"; -} - -.fa-staff-snake { - --fa: "\e579"; -} - -.fa-rod-asclepius { - --fa: "\e579"; -} - -.fa-rod-snake { - --fa: "\e579"; -} - -.fa-staff-aesculapius { - --fa: "\e579"; -} - -.fa-sun-plant-wilt { - --fa: "\e57a"; -} - -.fa-tarp { - --fa: "\e57b"; -} - -.fa-tarp-droplet { - --fa: "\e57c"; -} - -.fa-tent { - --fa: "\e57d"; -} - -.fa-tent-arrow-down-to-line { - --fa: "\e57e"; -} - -.fa-tent-arrow-left-right { - --fa: "\e57f"; -} - -.fa-tent-arrow-turn-left { - --fa: "\e580"; -} - -.fa-tent-arrows-down { - --fa: "\e581"; -} - -.fa-tents { - --fa: "\e582"; -} - -.fa-toilet-portable { - --fa: "\e583"; -} - -.fa-toilets-portable { - --fa: "\e584"; -} - -.fa-tower-cell { - --fa: "\e585"; -} - -.fa-tower-observation { - --fa: "\e586"; -} - -.fa-tree-city { - --fa: "\e587"; -} - -.fa-trowel { - --fa: "\e589"; -} - -.fa-trowel-bricks { - --fa: "\e58a"; -} - -.fa-truck-arrow-right { - --fa: "\e58b"; -} - -.fa-truck-droplet { - --fa: "\e58c"; -} - -.fa-truck-field { - --fa: "\e58d"; -} - -.fa-truck-field-un { - --fa: "\e58e"; -} - -.fa-truck-plane { - --fa: "\e58f"; -} - -.fa-users-between-lines { - --fa: "\e591"; -} - -.fa-users-line { - --fa: "\e592"; -} - -.fa-users-rays { - --fa: "\e593"; -} - -.fa-users-rectangle { - --fa: "\e594"; -} - -.fa-users-viewfinder { - --fa: "\e595"; -} - -.fa-vial-circle-check { - --fa: "\e596"; -} - -.fa-vial-virus { - --fa: "\e597"; -} - -.fa-wheat-awn-circle-exclamation { - --fa: "\e598"; -} - -.fa-worm { - --fa: "\e599"; -} - -.fa-xmarks-lines { - --fa: "\e59a"; -} - -.fa-child-dress { - --fa: "\e59c"; -} - -.fa-child-reaching { - --fa: "\e59d"; -} - -.fa-file-circle-check { - --fa: "\e5a0"; -} - -.fa-file-circle-xmark { - --fa: "\e5a1"; -} - -.fa-person-through-window { - --fa: "\e5a9"; -} - -.fa-plant-wilt { - --fa: "\e5aa"; -} - -.fa-stapler { - --fa: "\e5af"; -} - -.fa-train-tram { - --fa: "\e5b4"; -} - -.fa-table-cells-column-lock { - --fa: "\e678"; -} - -.fa-table-cells-row-lock { - --fa: "\e67a"; -} - -.fa-web-awesome { - --fa: "\e682"; -} - -.fa-thumbtack-slash { - --fa: "\e68f"; -} - -.fa-thumb-tack-slash { - --fa: "\e68f"; -} - -.fa-table-cells-row-unlock { - --fa: "\e691"; -} - -.fa-chart-diagram { - --fa: "\e695"; -} - -.fa-comment-nodes { - --fa: "\e696"; -} - -.fa-file-fragment { - --fa: "\e697"; -} - -.fa-file-half-dashed { - --fa: "\e698"; -} - -.fa-hexagon-nodes { - --fa: "\e699"; -} - -.fa-hexagon-nodes-bolt { - --fa: "\e69a"; -} - -.fa-square-binary { - --fa: "\e69b"; -} - -.fa-pentagon { - --fa: "\e790"; -} - -.fa-non-binary { - --fa: "\e807"; -} - -.fa-spiral { - --fa: "\e80a"; -} - -.fa-mobile-vibrate { - --fa: "\e816"; -} - -.fa-single-quote-left { - --fa: "\e81b"; -} - -.fa-single-quote-right { - --fa: "\e81c"; -} - -.fa-bus-side { - --fa: "\e81d"; -} - -.fa-septagon { - --fa: "\e820"; -} - -.fa-heptagon { - --fa: "\e820"; -} - -.fa-martini-glass-empty { - --fa: "\f000"; -} - -.fa-glass-martini { - --fa: "\f000"; -} - -.fa-music { - --fa: "\f001"; -} - -.fa-magnifying-glass { - --fa: "\f002"; -} - -.fa-search { - --fa: "\f002"; -} - -.fa-heart { - --fa: "\f004"; -} - -.fa-star { - --fa: "\f005"; -} - -.fa-user { - --fa: "\f007"; -} - -.fa-user-alt { - --fa: "\f007"; -} - -.fa-user-large { - --fa: "\f007"; -} - -.fa-film { - --fa: "\f008"; -} - -.fa-film-alt { - --fa: "\f008"; -} - -.fa-film-simple { - --fa: "\f008"; -} - -.fa-table-cells-large { - --fa: "\f009"; -} - -.fa-th-large { - --fa: "\f009"; -} - -.fa-table-cells { - --fa: "\f00a"; -} - -.fa-th { - --fa: "\f00a"; -} - -.fa-table-list { - --fa: "\f00b"; -} - -.fa-th-list { - --fa: "\f00b"; -} - -.fa-check { - --fa: "\f00c"; -} - -.fa-xmark { - --fa: "\f00d"; -} - -.fa-close { - --fa: "\f00d"; -} - -.fa-multiply { - --fa: "\f00d"; -} - -.fa-remove { - --fa: "\f00d"; -} - -.fa-times { - --fa: "\f00d"; -} - -.fa-magnifying-glass-plus { - --fa: "\f00e"; -} - -.fa-search-plus { - --fa: "\f00e"; -} - -.fa-magnifying-glass-minus { - --fa: "\f010"; -} - -.fa-search-minus { - --fa: "\f010"; -} - -.fa-power-off { - --fa: "\f011"; -} - -.fa-signal { - --fa: "\f012"; -} - -.fa-signal-5 { - --fa: "\f012"; -} - -.fa-signal-perfect { - --fa: "\f012"; -} - -.fa-gear { - --fa: "\f013"; -} - -.fa-cog { - --fa: "\f013"; -} - -.fa-house { - --fa: "\f015"; -} - -.fa-home { - --fa: "\f015"; -} - -.fa-home-alt { - --fa: "\f015"; -} - -.fa-home-lg-alt { - --fa: "\f015"; -} - -.fa-clock { - --fa: "\f017"; -} - -.fa-clock-four { - --fa: "\f017"; -} - -.fa-road { - --fa: "\f018"; -} - -.fa-download { - --fa: "\f019"; -} - -.fa-inbox { - --fa: "\f01c"; -} - -.fa-arrow-rotate-right { - --fa: "\f01e"; -} - -.fa-arrow-right-rotate { - --fa: "\f01e"; -} - -.fa-arrow-rotate-forward { - --fa: "\f01e"; -} - -.fa-redo { - --fa: "\f01e"; -} - -.fa-arrows-rotate { - --fa: "\f021"; -} - -.fa-refresh { - --fa: "\f021"; -} - -.fa-sync { - --fa: "\f021"; -} - -.fa-rectangle-list { - --fa: "\f022"; -} - -.fa-list-alt { - --fa: "\f022"; -} - -.fa-lock { - --fa: "\f023"; -} - -.fa-flag { - --fa: "\f024"; -} - -.fa-headphones { - --fa: "\f025"; -} - -.fa-headphones-alt { - --fa: "\f025"; -} - -.fa-headphones-simple { - --fa: "\f025"; -} - -.fa-volume-off { - --fa: "\f026"; -} - -.fa-volume-low { - --fa: "\f027"; -} - -.fa-volume-down { - --fa: "\f027"; -} - -.fa-volume-high { - --fa: "\f028"; -} - -.fa-volume-up { - --fa: "\f028"; -} - -.fa-qrcode { - --fa: "\f029"; -} - -.fa-barcode { - --fa: "\f02a"; -} - -.fa-tag { - --fa: "\f02b"; -} - -.fa-tags { - --fa: "\f02c"; -} - -.fa-book { - --fa: "\f02d"; -} - -.fa-bookmark { - --fa: "\f02e"; -} - -.fa-print { - --fa: "\f02f"; -} - -.fa-camera { - --fa: "\f030"; -} - -.fa-camera-alt { - --fa: "\f030"; -} - -.fa-font { - --fa: "\f031"; -} - -.fa-bold { - --fa: "\f032"; -} - -.fa-italic { - --fa: "\f033"; -} - -.fa-text-height { - --fa: "\f034"; -} - -.fa-text-width { - --fa: "\f035"; -} - -.fa-align-left { - --fa: "\f036"; -} - -.fa-align-center { - --fa: "\f037"; -} - -.fa-align-right { - --fa: "\f038"; -} - -.fa-align-justify { - --fa: "\f039"; -} - -.fa-list { - --fa: "\f03a"; -} - -.fa-list-squares { - --fa: "\f03a"; -} - -.fa-outdent { - --fa: "\f03b"; -} - -.fa-dedent { - --fa: "\f03b"; -} - -.fa-indent { - --fa: "\f03c"; -} - -.fa-video { - --fa: "\f03d"; -} - -.fa-video-camera { - --fa: "\f03d"; -} - -.fa-image { - --fa: "\f03e"; -} - -.fa-location-pin { - --fa: "\f041"; -} - -.fa-map-marker { - --fa: "\f041"; -} - -.fa-circle-half-stroke { - --fa: "\f042"; -} - -.fa-adjust { - --fa: "\f042"; -} - -.fa-droplet { - --fa: "\f043"; -} - -.fa-tint { - --fa: "\f043"; -} - -.fa-pen-to-square { - --fa: "\f044"; -} - -.fa-edit { - --fa: "\f044"; -} - -.fa-arrows-up-down-left-right { - --fa: "\f047"; -} - -.fa-arrows { - --fa: "\f047"; -} - -.fa-backward-step { - --fa: "\f048"; -} - -.fa-step-backward { - --fa: "\f048"; -} - -.fa-backward-fast { - --fa: "\f049"; -} - -.fa-fast-backward { - --fa: "\f049"; -} - -.fa-backward { - --fa: "\f04a"; -} - -.fa-play { - --fa: "\f04b"; -} - -.fa-pause { - --fa: "\f04c"; -} - -.fa-stop { - --fa: "\f04d"; -} - -.fa-forward { - --fa: "\f04e"; -} - -.fa-forward-fast { - --fa: "\f050"; -} - -.fa-fast-forward { - --fa: "\f050"; -} - -.fa-forward-step { - --fa: "\f051"; -} - -.fa-step-forward { - --fa: "\f051"; -} - -.fa-eject { - --fa: "\f052"; -} - -.fa-chevron-left { - --fa: "\f053"; -} - -.fa-chevron-right { - --fa: "\f054"; -} - -.fa-circle-plus { - --fa: "\f055"; -} - -.fa-plus-circle { - --fa: "\f055"; -} - -.fa-circle-minus { - --fa: "\f056"; -} - -.fa-minus-circle { - --fa: "\f056"; -} - -.fa-circle-xmark { - --fa: "\f057"; -} - -.fa-times-circle { - --fa: "\f057"; -} - -.fa-xmark-circle { - --fa: "\f057"; -} - -.fa-circle-check { - --fa: "\f058"; -} - -.fa-check-circle { - --fa: "\f058"; -} - -.fa-circle-question { - --fa: "\f059"; -} - -.fa-question-circle { - --fa: "\f059"; -} - -.fa-circle-info { - --fa: "\f05a"; -} - -.fa-info-circle { - --fa: "\f05a"; -} - -.fa-crosshairs { - --fa: "\f05b"; -} - -.fa-ban { - --fa: "\f05e"; -} - -.fa-cancel { - --fa: "\f05e"; -} - -.fa-arrow-left { - --fa: "\f060"; -} - -.fa-arrow-right { - --fa: "\f061"; -} - -.fa-arrow-up { - --fa: "\f062"; -} - -.fa-arrow-down { - --fa: "\f063"; -} - -.fa-share { - --fa: "\f064"; -} - -.fa-mail-forward { - --fa: "\f064"; -} - -.fa-expand { - --fa: "\f065"; -} - -.fa-compress { - --fa: "\f066"; -} - -.fa-minus { - --fa: "\f068"; -} - -.fa-subtract { - --fa: "\f068"; -} - -.fa-circle-exclamation { - --fa: "\f06a"; -} - -.fa-exclamation-circle { - --fa: "\f06a"; -} - -.fa-gift { - --fa: "\f06b"; -} - -.fa-leaf { - --fa: "\f06c"; -} - -.fa-fire { - --fa: "\f06d"; -} - -.fa-eye { - --fa: "\f06e"; -} - -.fa-eye-slash { - --fa: "\f070"; -} - -.fa-triangle-exclamation { - --fa: "\f071"; -} - -.fa-exclamation-triangle { - --fa: "\f071"; -} - -.fa-warning { - --fa: "\f071"; -} - -.fa-plane { - --fa: "\f072"; -} - -.fa-calendar-days { - --fa: "\f073"; -} - -.fa-calendar-alt { - --fa: "\f073"; -} - -.fa-shuffle { - --fa: "\f074"; -} - -.fa-random { - --fa: "\f074"; -} - -.fa-comment { - --fa: "\f075"; -} - -.fa-magnet { - --fa: "\f076"; -} - -.fa-chevron-up { - --fa: "\f077"; -} - -.fa-chevron-down { - --fa: "\f078"; -} - -.fa-retweet { - --fa: "\f079"; -} - -.fa-cart-shopping { - --fa: "\f07a"; -} - -.fa-shopping-cart { - --fa: "\f07a"; -} - -.fa-folder { - --fa: "\f07b"; -} - -.fa-folder-blank { - --fa: "\f07b"; -} - -.fa-folder-open { - --fa: "\f07c"; -} - -.fa-arrows-up-down { - --fa: "\f07d"; -} - -.fa-arrows-v { - --fa: "\f07d"; -} - -.fa-arrows-left-right { - --fa: "\f07e"; -} - -.fa-arrows-h { - --fa: "\f07e"; -} - -.fa-chart-bar { - --fa: "\f080"; -} - -.fa-bar-chart { - --fa: "\f080"; -} - -.fa-camera-retro { - --fa: "\f083"; -} - -.fa-key { - --fa: "\f084"; -} - -.fa-gears { - --fa: "\f085"; -} - -.fa-cogs { - --fa: "\f085"; -} - -.fa-comments { - --fa: "\f086"; -} - -.fa-star-half { - --fa: "\f089"; -} - -.fa-arrow-right-from-bracket { - --fa: "\f08b"; -} - -.fa-sign-out { - --fa: "\f08b"; -} - -.fa-thumbtack { - --fa: "\f08d"; -} - -.fa-thumb-tack { - --fa: "\f08d"; -} - -.fa-arrow-up-right-from-square { - --fa: "\f08e"; -} - -.fa-external-link { - --fa: "\f08e"; -} - -.fa-arrow-right-to-bracket { - --fa: "\f090"; -} - -.fa-sign-in { - --fa: "\f090"; -} - -.fa-trophy { - --fa: "\f091"; -} - -.fa-upload { - --fa: "\f093"; -} - -.fa-lemon { - --fa: "\f094"; -} - -.fa-phone { - --fa: "\f095"; -} - -.fa-square-phone { - --fa: "\f098"; -} - -.fa-phone-square { - --fa: "\f098"; -} - -.fa-unlock { - --fa: "\f09c"; -} - -.fa-credit-card { - --fa: "\f09d"; -} - -.fa-credit-card-alt { - --fa: "\f09d"; -} - -.fa-rss { - --fa: "\f09e"; -} - -.fa-feed { - --fa: "\f09e"; -} - -.fa-hard-drive { - --fa: "\f0a0"; -} - -.fa-hdd { - --fa: "\f0a0"; -} - -.fa-bullhorn { - --fa: "\f0a1"; -} - -.fa-certificate { - --fa: "\f0a3"; -} - -.fa-hand-point-right { - --fa: "\f0a4"; -} - -.fa-hand-point-left { - --fa: "\f0a5"; -} - -.fa-hand-point-up { - --fa: "\f0a6"; -} - -.fa-hand-point-down { - --fa: "\f0a7"; -} - -.fa-circle-arrow-left { - --fa: "\f0a8"; -} - -.fa-arrow-circle-left { - --fa: "\f0a8"; -} - -.fa-circle-arrow-right { - --fa: "\f0a9"; -} - -.fa-arrow-circle-right { - --fa: "\f0a9"; -} - -.fa-circle-arrow-up { - --fa: "\f0aa"; -} - -.fa-arrow-circle-up { - --fa: "\f0aa"; -} - -.fa-circle-arrow-down { - --fa: "\f0ab"; -} - -.fa-arrow-circle-down { - --fa: "\f0ab"; -} - -.fa-globe { - --fa: "\f0ac"; -} - -.fa-wrench { - --fa: "\f0ad"; -} - -.fa-list-check { - --fa: "\f0ae"; -} - -.fa-tasks { - --fa: "\f0ae"; -} - -.fa-filter { - --fa: "\f0b0"; -} - -.fa-briefcase { - --fa: "\f0b1"; -} - -.fa-up-down-left-right { - --fa: "\f0b2"; -} - -.fa-arrows-alt { - --fa: "\f0b2"; -} - -.fa-users { - --fa: "\f0c0"; -} - -.fa-link { - --fa: "\f0c1"; -} - -.fa-chain { - --fa: "\f0c1"; -} - -.fa-cloud { - --fa: "\f0c2"; -} - -.fa-flask { - --fa: "\f0c3"; -} - -.fa-scissors { - --fa: "\f0c4"; -} - -.fa-cut { - --fa: "\f0c4"; -} - -.fa-copy { - --fa: "\f0c5"; -} - -.fa-paperclip { - --fa: "\f0c6"; -} - -.fa-floppy-disk { - --fa: "\f0c7"; -} - -.fa-save { - --fa: "\f0c7"; -} - -.fa-square { - --fa: "\f0c8"; -} - -.fa-bars { - --fa: "\f0c9"; -} - -.fa-navicon { - --fa: "\f0c9"; -} - -.fa-list-ul { - --fa: "\f0ca"; -} - -.fa-list-dots { - --fa: "\f0ca"; -} - -.fa-list-ol { - --fa: "\f0cb"; -} - -.fa-list-1-2 { - --fa: "\f0cb"; -} - -.fa-list-numeric { - --fa: "\f0cb"; -} - -.fa-strikethrough { - --fa: "\f0cc"; -} - -.fa-underline { - --fa: "\f0cd"; -} - -.fa-table { - --fa: "\f0ce"; -} - -.fa-wand-magic { - --fa: "\f0d0"; -} - -.fa-magic { - --fa: "\f0d0"; -} - -.fa-truck { - --fa: "\f0d1"; -} - -.fa-money-bill { - --fa: "\f0d6"; -} - -.fa-caret-down { - --fa: "\f0d7"; -} - -.fa-caret-up { - --fa: "\f0d8"; -} - -.fa-caret-left { - --fa: "\f0d9"; -} - -.fa-caret-right { - --fa: "\f0da"; -} - -.fa-table-columns { - --fa: "\f0db"; -} - -.fa-columns { - --fa: "\f0db"; -} - -.fa-sort { - --fa: "\f0dc"; -} - -.fa-unsorted { - --fa: "\f0dc"; -} - -.fa-sort-down { - --fa: "\f0dd"; -} - -.fa-sort-desc { - --fa: "\f0dd"; -} - -.fa-sort-up { - --fa: "\f0de"; -} - -.fa-sort-asc { - --fa: "\f0de"; -} - -.fa-envelope { - --fa: "\f0e0"; -} - -.fa-arrow-rotate-left { - --fa: "\f0e2"; -} - -.fa-arrow-left-rotate { - --fa: "\f0e2"; -} - -.fa-arrow-rotate-back { - --fa: "\f0e2"; -} - -.fa-arrow-rotate-backward { - --fa: "\f0e2"; -} - -.fa-undo { - --fa: "\f0e2"; -} - -.fa-gavel { - --fa: "\f0e3"; -} - -.fa-legal { - --fa: "\f0e3"; -} - -.fa-bolt { - --fa: "\f0e7"; -} - -.fa-zap { - --fa: "\f0e7"; -} - -.fa-sitemap { - --fa: "\f0e8"; -} - -.fa-umbrella { - --fa: "\f0e9"; -} - -.fa-paste { - --fa: "\f0ea"; -} - -.fa-file-clipboard { - --fa: "\f0ea"; -} - -.fa-lightbulb { - --fa: "\f0eb"; -} - -.fa-arrow-right-arrow-left { - --fa: "\f0ec"; -} - -.fa-exchange { - --fa: "\f0ec"; -} - -.fa-cloud-arrow-down { - --fa: "\f0ed"; -} - -.fa-cloud-download { - --fa: "\f0ed"; -} - -.fa-cloud-download-alt { - --fa: "\f0ed"; -} - -.fa-cloud-arrow-up { - --fa: "\f0ee"; -} - -.fa-cloud-upload { - --fa: "\f0ee"; -} - -.fa-cloud-upload-alt { - --fa: "\f0ee"; -} - -.fa-user-doctor { - --fa: "\f0f0"; -} - -.fa-user-md { - --fa: "\f0f0"; -} - -.fa-stethoscope { - --fa: "\f0f1"; -} - -.fa-suitcase { - --fa: "\f0f2"; -} - -.fa-bell { - --fa: "\f0f3"; -} - -.fa-mug-saucer { - --fa: "\f0f4"; -} - -.fa-coffee { - --fa: "\f0f4"; -} - -.fa-hospital { - --fa: "\f0f8"; -} - -.fa-hospital-alt { - --fa: "\f0f8"; -} - -.fa-hospital-wide { - --fa: "\f0f8"; -} - -.fa-truck-medical { - --fa: "\f0f9"; -} - -.fa-ambulance { - --fa: "\f0f9"; -} - -.fa-suitcase-medical { - --fa: "\f0fa"; -} - -.fa-medkit { - --fa: "\f0fa"; -} - -.fa-jet-fighter { - --fa: "\f0fb"; -} - -.fa-fighter-jet { - --fa: "\f0fb"; -} - -.fa-beer-mug-empty { - --fa: "\f0fc"; -} - -.fa-beer { - --fa: "\f0fc"; -} - -.fa-square-h { - --fa: "\f0fd"; -} - -.fa-h-square { - --fa: "\f0fd"; -} - -.fa-square-plus { - --fa: "\f0fe"; -} - -.fa-plus-square { - --fa: "\f0fe"; -} - -.fa-angles-left { - --fa: "\f100"; -} - -.fa-angle-double-left { - --fa: "\f100"; -} - -.fa-angles-right { - --fa: "\f101"; -} - -.fa-angle-double-right { - --fa: "\f101"; -} - -.fa-angles-up { - --fa: "\f102"; -} - -.fa-angle-double-up { - --fa: "\f102"; -} - -.fa-angles-down { - --fa: "\f103"; -} - -.fa-angle-double-down { - --fa: "\f103"; -} - -.fa-angle-left { - --fa: "\f104"; -} - -.fa-angle-right { - --fa: "\f105"; -} - -.fa-angle-up { - --fa: "\f106"; -} - -.fa-angle-down { - --fa: "\f107"; -} - -.fa-laptop { - --fa: "\f109"; -} - -.fa-tablet-button { - --fa: "\f10a"; -} - -.fa-mobile-button { - --fa: "\f10b"; -} - -.fa-quote-left { - --fa: "\f10d"; -} - -.fa-quote-left-alt { - --fa: "\f10d"; -} - -.fa-quote-right { - --fa: "\f10e"; -} - -.fa-quote-right-alt { - --fa: "\f10e"; -} - -.fa-spinner { - --fa: "\f110"; -} - -.fa-circle { - --fa: "\f111"; -} - -.fa-face-smile { - --fa: "\f118"; -} - -.fa-smile { - --fa: "\f118"; -} - -.fa-face-frown { - --fa: "\f119"; -} - -.fa-frown { - --fa: "\f119"; -} - -.fa-face-meh { - --fa: "\f11a"; -} - -.fa-meh { - --fa: "\f11a"; -} - -.fa-gamepad { - --fa: "\f11b"; -} - -.fa-keyboard { - --fa: "\f11c"; -} - -.fa-flag-checkered { - --fa: "\f11e"; -} - -.fa-terminal { - --fa: "\f120"; -} - -.fa-code { - --fa: "\f121"; -} - -.fa-reply-all { - --fa: "\f122"; -} - -.fa-mail-reply-all { - --fa: "\f122"; -} - -.fa-location-arrow { - --fa: "\f124"; -} - -.fa-crop { - --fa: "\f125"; -} - -.fa-code-branch { - --fa: "\f126"; -} - -.fa-link-slash { - --fa: "\f127"; -} - -.fa-chain-broken { - --fa: "\f127"; -} - -.fa-chain-slash { - --fa: "\f127"; -} - -.fa-unlink { - --fa: "\f127"; -} - -.fa-info { - --fa: "\f129"; -} - -.fa-superscript { - --fa: "\f12b"; -} - -.fa-subscript { - --fa: "\f12c"; -} - -.fa-eraser { - --fa: "\f12d"; -} - -.fa-puzzle-piece { - --fa: "\f12e"; -} - -.fa-microphone { - --fa: "\f130"; -} - -.fa-microphone-slash { - --fa: "\f131"; -} - -.fa-shield { - --fa: "\f132"; -} - -.fa-shield-blank { - --fa: "\f132"; -} - -.fa-calendar { - --fa: "\f133"; -} - -.fa-fire-extinguisher { - --fa: "\f134"; -} - -.fa-rocket { - --fa: "\f135"; -} - -.fa-circle-chevron-left { - --fa: "\f137"; -} - -.fa-chevron-circle-left { - --fa: "\f137"; -} - -.fa-circle-chevron-right { - --fa: "\f138"; -} - -.fa-chevron-circle-right { - --fa: "\f138"; -} - -.fa-circle-chevron-up { - --fa: "\f139"; -} - -.fa-chevron-circle-up { - --fa: "\f139"; -} - -.fa-circle-chevron-down { - --fa: "\f13a"; -} - -.fa-chevron-circle-down { - --fa: "\f13a"; -} - -.fa-anchor { - --fa: "\f13d"; -} - -.fa-unlock-keyhole { - --fa: "\f13e"; -} - -.fa-unlock-alt { - --fa: "\f13e"; -} - -.fa-bullseye { - --fa: "\f140"; -} - -.fa-ellipsis { - --fa: "\f141"; -} - -.fa-ellipsis-h { - --fa: "\f141"; -} - -.fa-ellipsis-vertical { - --fa: "\f142"; -} - -.fa-ellipsis-v { - --fa: "\f142"; -} - -.fa-square-rss { - --fa: "\f143"; -} - -.fa-rss-square { - --fa: "\f143"; -} - -.fa-circle-play { - --fa: "\f144"; -} - -.fa-play-circle { - --fa: "\f144"; -} - -.fa-ticket { - --fa: "\f145"; -} - -.fa-square-minus { - --fa: "\f146"; -} - -.fa-minus-square { - --fa: "\f146"; -} - -.fa-arrow-turn-up { - --fa: "\f148"; -} - -.fa-level-up { - --fa: "\f148"; -} - -.fa-arrow-turn-down { - --fa: "\f149"; -} - -.fa-level-down { - --fa: "\f149"; -} - -.fa-square-check { - --fa: "\f14a"; -} - -.fa-check-square { - --fa: "\f14a"; -} - -.fa-square-pen { - --fa: "\f14b"; -} - -.fa-pen-square { - --fa: "\f14b"; -} - -.fa-pencil-square { - --fa: "\f14b"; -} - -.fa-square-arrow-up-right { - --fa: "\f14c"; -} - -.fa-external-link-square { - --fa: "\f14c"; -} - -.fa-share-from-square { - --fa: "\f14d"; -} - -.fa-share-square { - --fa: "\f14d"; -} - -.fa-compass { - --fa: "\f14e"; -} - -.fa-square-caret-down { - --fa: "\f150"; -} - -.fa-caret-square-down { - --fa: "\f150"; -} - -.fa-square-caret-up { - --fa: "\f151"; -} - -.fa-caret-square-up { - --fa: "\f151"; -} - -.fa-square-caret-right { - --fa: "\f152"; -} - -.fa-caret-square-right { - --fa: "\f152"; -} - -.fa-euro-sign { - --fa: "\f153"; -} - -.fa-eur { - --fa: "\f153"; -} - -.fa-euro { - --fa: "\f153"; -} - -.fa-sterling-sign { - --fa: "\f154"; -} - -.fa-gbp { - --fa: "\f154"; -} - -.fa-pound-sign { - --fa: "\f154"; -} - -.fa-rupee-sign { - --fa: "\f156"; -} - -.fa-rupee { - --fa: "\f156"; -} - -.fa-yen-sign { - --fa: "\f157"; -} - -.fa-cny { - --fa: "\f157"; -} - -.fa-jpy { - --fa: "\f157"; -} - -.fa-rmb { - --fa: "\f157"; -} - -.fa-yen { - --fa: "\f157"; -} - -.fa-ruble-sign { - --fa: "\f158"; -} - -.fa-rouble { - --fa: "\f158"; -} - -.fa-rub { - --fa: "\f158"; -} - -.fa-ruble { - --fa: "\f158"; -} - -.fa-won-sign { - --fa: "\f159"; -} - -.fa-krw { - --fa: "\f159"; -} - -.fa-won { - --fa: "\f159"; -} - -.fa-file { - --fa: "\f15b"; -} - -.fa-file-lines { - --fa: "\f15c"; -} - -.fa-file-alt { - --fa: "\f15c"; -} - -.fa-file-text { - --fa: "\f15c"; -} - -.fa-arrow-down-a-z { - --fa: "\f15d"; -} - -.fa-sort-alpha-asc { - --fa: "\f15d"; -} - -.fa-sort-alpha-down { - --fa: "\f15d"; -} - -.fa-arrow-up-a-z { - --fa: "\f15e"; -} - -.fa-sort-alpha-up { - --fa: "\f15e"; -} - -.fa-arrow-down-wide-short { - --fa: "\f160"; -} - -.fa-sort-amount-asc { - --fa: "\f160"; -} - -.fa-sort-amount-down { - --fa: "\f160"; -} - -.fa-arrow-up-wide-short { - --fa: "\f161"; -} - -.fa-sort-amount-up { - --fa: "\f161"; -} - -.fa-arrow-down-1-9 { - --fa: "\f162"; -} - -.fa-sort-numeric-asc { - --fa: "\f162"; -} - -.fa-sort-numeric-down { - --fa: "\f162"; -} - -.fa-arrow-up-1-9 { - --fa: "\f163"; -} - -.fa-sort-numeric-up { - --fa: "\f163"; -} - -.fa-thumbs-up { - --fa: "\f164"; -} - -.fa-thumbs-down { - --fa: "\f165"; -} - -.fa-arrow-down-long { - --fa: "\f175"; -} - -.fa-long-arrow-down { - --fa: "\f175"; -} - -.fa-arrow-up-long { - --fa: "\f176"; -} - -.fa-long-arrow-up { - --fa: "\f176"; -} - -.fa-arrow-left-long { - --fa: "\f177"; -} - -.fa-long-arrow-left { - --fa: "\f177"; -} - -.fa-arrow-right-long { - --fa: "\f178"; -} - -.fa-long-arrow-right { - --fa: "\f178"; -} - -.fa-person-dress { - --fa: "\f182"; -} - -.fa-female { - --fa: "\f182"; -} - -.fa-person { - --fa: "\f183"; -} - -.fa-male { - --fa: "\f183"; -} - -.fa-sun { - --fa: "\f185"; -} - -.fa-moon { - --fa: "\f186"; -} - -.fa-box-archive { - --fa: "\f187"; -} - -.fa-archive { - --fa: "\f187"; -} - -.fa-bug { - --fa: "\f188"; -} - -.fa-square-caret-left { - --fa: "\f191"; -} - -.fa-caret-square-left { - --fa: "\f191"; -} - -.fa-circle-dot { - --fa: "\f192"; -} - -.fa-dot-circle { - --fa: "\f192"; -} - -.fa-wheelchair { - --fa: "\f193"; -} - -.fa-lira-sign { - --fa: "\f195"; -} - -.fa-shuttle-space { - --fa: "\f197"; -} - -.fa-space-shuttle { - --fa: "\f197"; -} - -.fa-square-envelope { - --fa: "\f199"; -} - -.fa-envelope-square { - --fa: "\f199"; -} - -.fa-building-columns { - --fa: "\f19c"; -} - -.fa-bank { - --fa: "\f19c"; -} - -.fa-institution { - --fa: "\f19c"; -} - -.fa-museum { - --fa: "\f19c"; -} - -.fa-university { - --fa: "\f19c"; -} - -.fa-graduation-cap { - --fa: "\f19d"; -} - -.fa-mortar-board { - --fa: "\f19d"; -} - -.fa-language { - --fa: "\f1ab"; -} - -.fa-fax { - --fa: "\f1ac"; -} - -.fa-building { - --fa: "\f1ad"; -} - -.fa-child { - --fa: "\f1ae"; -} - -.fa-paw { - --fa: "\f1b0"; -} - -.fa-cube { - --fa: "\f1b2"; -} - -.fa-cubes { - --fa: "\f1b3"; -} - -.fa-recycle { - --fa: "\f1b8"; -} - -.fa-car { - --fa: "\f1b9"; -} - -.fa-automobile { - --fa: "\f1b9"; -} - -.fa-taxi { - --fa: "\f1ba"; -} - -.fa-cab { - --fa: "\f1ba"; -} - -.fa-tree { - --fa: "\f1bb"; -} - -.fa-database { - --fa: "\f1c0"; -} - -.fa-file-pdf { - --fa: "\f1c1"; -} - -.fa-file-word { - --fa: "\f1c2"; -} - -.fa-file-excel { - --fa: "\f1c3"; -} - -.fa-file-powerpoint { - --fa: "\f1c4"; -} - -.fa-file-image { - --fa: "\f1c5"; -} - -.fa-file-zipper { - --fa: "\f1c6"; -} - -.fa-file-archive { - --fa: "\f1c6"; -} - -.fa-file-audio { - --fa: "\f1c7"; -} - -.fa-file-video { - --fa: "\f1c8"; -} - -.fa-file-code { - --fa: "\f1c9"; -} - -.fa-life-ring { - --fa: "\f1cd"; -} - -.fa-circle-notch { - --fa: "\f1ce"; -} - -.fa-paper-plane { - --fa: "\f1d8"; -} - -.fa-clock-rotate-left { - --fa: "\f1da"; -} - -.fa-history { - --fa: "\f1da"; -} - -.fa-heading { - --fa: "\f1dc"; -} - -.fa-header { - --fa: "\f1dc"; -} - -.fa-paragraph { - --fa: "\f1dd"; -} - -.fa-sliders { - --fa: "\f1de"; -} - -.fa-sliders-h { - --fa: "\f1de"; -} - -.fa-share-nodes { - --fa: "\f1e0"; -} - -.fa-share-alt { - --fa: "\f1e0"; -} - -.fa-square-share-nodes { - --fa: "\f1e1"; -} - -.fa-share-alt-square { - --fa: "\f1e1"; -} - -.fa-bomb { - --fa: "\f1e2"; -} - -.fa-futbol { - --fa: "\f1e3"; -} - -.fa-futbol-ball { - --fa: "\f1e3"; -} - -.fa-soccer-ball { - --fa: "\f1e3"; -} - -.fa-tty { - --fa: "\f1e4"; -} - -.fa-teletype { - --fa: "\f1e4"; -} - -.fa-binoculars { - --fa: "\f1e5"; -} - -.fa-plug { - --fa: "\f1e6"; -} - -.fa-newspaper { - --fa: "\f1ea"; -} - -.fa-wifi { - --fa: "\f1eb"; -} - -.fa-wifi-3 { - --fa: "\f1eb"; -} - -.fa-wifi-strong { - --fa: "\f1eb"; -} - -.fa-calculator { - --fa: "\f1ec"; -} - -.fa-bell-slash { - --fa: "\f1f6"; -} - -.fa-trash { - --fa: "\f1f8"; -} - -.fa-copyright { - --fa: "\f1f9"; -} - -.fa-eye-dropper { - --fa: "\f1fb"; -} - -.fa-eye-dropper-empty { - --fa: "\f1fb"; -} - -.fa-eyedropper { - --fa: "\f1fb"; -} - -.fa-paintbrush { - --fa: "\f1fc"; -} - -.fa-paint-brush { - --fa: "\f1fc"; -} - -.fa-cake-candles { - --fa: "\f1fd"; -} - -.fa-birthday-cake { - --fa: "\f1fd"; -} - -.fa-cake { - --fa: "\f1fd"; -} - -.fa-chart-area { - --fa: "\f1fe"; -} - -.fa-area-chart { - --fa: "\f1fe"; -} - -.fa-chart-pie { - --fa: "\f200"; -} - -.fa-pie-chart { - --fa: "\f200"; -} - -.fa-chart-line { - --fa: "\f201"; -} - -.fa-line-chart { - --fa: "\f201"; -} - -.fa-toggle-off { - --fa: "\f204"; -} - -.fa-toggle-on { - --fa: "\f205"; -} - -.fa-bicycle { - --fa: "\f206"; -} - -.fa-bus { - --fa: "\f207"; -} - -.fa-closed-captioning { - --fa: "\f20a"; -} - -.fa-shekel-sign { - --fa: "\f20b"; -} - -.fa-ils { - --fa: "\f20b"; -} - -.fa-shekel { - --fa: "\f20b"; -} - -.fa-sheqel { - --fa: "\f20b"; -} - -.fa-sheqel-sign { - --fa: "\f20b"; -} - -.fa-cart-plus { - --fa: "\f217"; -} - -.fa-cart-arrow-down { - --fa: "\f218"; -} - -.fa-diamond { - --fa: "\f219"; -} - -.fa-ship { - --fa: "\f21a"; -} - -.fa-user-secret { - --fa: "\f21b"; -} - -.fa-motorcycle { - --fa: "\f21c"; -} - -.fa-street-view { - --fa: "\f21d"; -} - -.fa-heart-pulse { - --fa: "\f21e"; -} - -.fa-heartbeat { - --fa: "\f21e"; -} - -.fa-venus { - --fa: "\f221"; -} - -.fa-mars { - --fa: "\f222"; -} - -.fa-mercury { - --fa: "\f223"; -} - -.fa-mars-and-venus { - --fa: "\f224"; -} - -.fa-transgender { - --fa: "\f225"; -} - -.fa-transgender-alt { - --fa: "\f225"; -} - -.fa-venus-double { - --fa: "\f226"; -} - -.fa-mars-double { - --fa: "\f227"; -} - -.fa-venus-mars { - --fa: "\f228"; -} - -.fa-mars-stroke { - --fa: "\f229"; -} - -.fa-mars-stroke-up { - --fa: "\f22a"; -} - -.fa-mars-stroke-v { - --fa: "\f22a"; -} - -.fa-mars-stroke-right { - --fa: "\f22b"; -} - -.fa-mars-stroke-h { - --fa: "\f22b"; -} - -.fa-neuter { - --fa: "\f22c"; -} - -.fa-genderless { - --fa: "\f22d"; -} - -.fa-server { - --fa: "\f233"; -} - -.fa-user-plus { - --fa: "\f234"; -} - -.fa-user-xmark { - --fa: "\f235"; -} - -.fa-user-times { - --fa: "\f235"; -} - -.fa-bed { - --fa: "\f236"; -} - -.fa-train { - --fa: "\f238"; -} - -.fa-train-subway { - --fa: "\f239"; -} - -.fa-subway { - --fa: "\f239"; -} - -.fa-battery-full { - --fa: "\f240"; -} - -.fa-battery { - --fa: "\f240"; -} - -.fa-battery-5 { - --fa: "\f240"; -} - -.fa-battery-three-quarters { - --fa: "\f241"; -} - -.fa-battery-4 { - --fa: "\f241"; -} - -.fa-battery-half { - --fa: "\f242"; -} - -.fa-battery-3 { - --fa: "\f242"; -} - -.fa-battery-quarter { - --fa: "\f243"; -} - -.fa-battery-2 { - --fa: "\f243"; -} - -.fa-battery-empty { - --fa: "\f244"; -} - -.fa-battery-0 { - --fa: "\f244"; -} - -.fa-arrow-pointer { - --fa: "\f245"; -} - -.fa-mouse-pointer { - --fa: "\f245"; -} - -.fa-i-cursor { - --fa: "\f246"; -} - -.fa-object-group { - --fa: "\f247"; -} - -.fa-object-ungroup { - --fa: "\f248"; -} - -.fa-note-sticky { - --fa: "\f249"; -} - -.fa-sticky-note { - --fa: "\f249"; -} - -.fa-clone { - --fa: "\f24d"; -} - -.fa-scale-balanced { - --fa: "\f24e"; -} - -.fa-balance-scale { - --fa: "\f24e"; -} - -.fa-hourglass-start { - --fa: "\f251"; -} - -.fa-hourglass-1 { - --fa: "\f251"; -} - -.fa-hourglass-half { - --fa: "\f252"; -} - -.fa-hourglass-2 { - --fa: "\f252"; -} - -.fa-hourglass-end { - --fa: "\f253"; -} - -.fa-hourglass-3 { - --fa: "\f253"; -} - -.fa-hourglass { - --fa: "\f254"; -} - -.fa-hourglass-empty { - --fa: "\f254"; -} - -.fa-hand-back-fist { - --fa: "\f255"; -} - -.fa-hand-rock { - --fa: "\f255"; -} - -.fa-hand { - --fa: "\f256"; -} - -.fa-hand-paper { - --fa: "\f256"; -} - -.fa-hand-scissors { - --fa: "\f257"; -} - -.fa-hand-lizard { - --fa: "\f258"; -} - -.fa-hand-spock { - --fa: "\f259"; -} - -.fa-hand-pointer { - --fa: "\f25a"; -} - -.fa-hand-peace { - --fa: "\f25b"; -} - -.fa-trademark { - --fa: "\f25c"; -} - -.fa-registered { - --fa: "\f25d"; -} - -.fa-tv { - --fa: "\f26c"; -} - -.fa-television { - --fa: "\f26c"; -} - -.fa-tv-alt { - --fa: "\f26c"; -} - -.fa-calendar-plus { - --fa: "\f271"; -} - -.fa-calendar-minus { - --fa: "\f272"; -} - -.fa-calendar-xmark { - --fa: "\f273"; -} - -.fa-calendar-times { - --fa: "\f273"; -} - -.fa-calendar-check { - --fa: "\f274"; -} - -.fa-industry { - --fa: "\f275"; -} - -.fa-map-pin { - --fa: "\f276"; -} - -.fa-signs-post { - --fa: "\f277"; -} - -.fa-map-signs { - --fa: "\f277"; -} - -.fa-map { - --fa: "\f279"; -} - -.fa-message { - --fa: "\f27a"; -} - -.fa-comment-alt { - --fa: "\f27a"; -} - -.fa-circle-pause { - --fa: "\f28b"; -} - -.fa-pause-circle { - --fa: "\f28b"; -} - -.fa-circle-stop { - --fa: "\f28d"; -} - -.fa-stop-circle { - --fa: "\f28d"; -} - -.fa-bag-shopping { - --fa: "\f290"; -} - -.fa-shopping-bag { - --fa: "\f290"; -} - -.fa-basket-shopping { - --fa: "\f291"; -} - -.fa-shopping-basket { - --fa: "\f291"; -} - -.fa-universal-access { - --fa: "\f29a"; -} - -.fa-person-walking-with-cane { - --fa: "\f29d"; -} - -.fa-blind { - --fa: "\f29d"; -} - -.fa-audio-description { - --fa: "\f29e"; -} - -.fa-phone-volume { - --fa: "\f2a0"; -} - -.fa-volume-control-phone { - --fa: "\f2a0"; -} - -.fa-braille { - --fa: "\f2a1"; -} - -.fa-ear-listen { - --fa: "\f2a2"; -} - -.fa-assistive-listening-systems { - --fa: "\f2a2"; -} - -.fa-hands-asl-interpreting { - --fa: "\f2a3"; -} - -.fa-american-sign-language-interpreting { - --fa: "\f2a3"; -} - -.fa-asl-interpreting { - --fa: "\f2a3"; -} - -.fa-hands-american-sign-language-interpreting { - --fa: "\f2a3"; -} - -.fa-ear-deaf { - --fa: "\f2a4"; -} - -.fa-deaf { - --fa: "\f2a4"; -} - -.fa-deafness { - --fa: "\f2a4"; -} - -.fa-hard-of-hearing { - --fa: "\f2a4"; -} - -.fa-hands { - --fa: "\f2a7"; -} - -.fa-sign-language { - --fa: "\f2a7"; -} - -.fa-signing { - --fa: "\f2a7"; -} - -.fa-eye-low-vision { - --fa: "\f2a8"; -} - -.fa-low-vision { - --fa: "\f2a8"; -} - -.fa-font-awesome { - --fa: "\f2b4"; -} - -.fa-font-awesome-flag { - --fa: "\f2b4"; -} - -.fa-font-awesome-logo-full { - --fa: "\f2b4"; -} - -.fa-handshake { - --fa: "\f2b5"; -} - -.fa-handshake-alt { - --fa: "\f2b5"; -} - -.fa-handshake-simple { - --fa: "\f2b5"; -} - -.fa-envelope-open { - --fa: "\f2b6"; -} - -.fa-address-book { - --fa: "\f2b9"; -} - -.fa-contact-book { - --fa: "\f2b9"; -} - -.fa-address-card { - --fa: "\f2bb"; -} - -.fa-contact-card { - --fa: "\f2bb"; -} - -.fa-vcard { - --fa: "\f2bb"; -} - -.fa-circle-user { - --fa: "\f2bd"; -} - -.fa-user-circle { - --fa: "\f2bd"; -} - -.fa-id-badge { - --fa: "\f2c1"; -} - -.fa-id-card { - --fa: "\f2c2"; -} - -.fa-drivers-license { - --fa: "\f2c2"; -} - -.fa-temperature-full { - --fa: "\f2c7"; -} - -.fa-temperature-4 { - --fa: "\f2c7"; -} - -.fa-thermometer-4 { - --fa: "\f2c7"; -} - -.fa-thermometer-full { - --fa: "\f2c7"; -} - -.fa-temperature-three-quarters { - --fa: "\f2c8"; -} - -.fa-temperature-3 { - --fa: "\f2c8"; -} - -.fa-thermometer-3 { - --fa: "\f2c8"; -} - -.fa-thermometer-three-quarters { - --fa: "\f2c8"; -} - -.fa-temperature-half { - --fa: "\f2c9"; -} - -.fa-temperature-2 { - --fa: "\f2c9"; -} - -.fa-thermometer-2 { - --fa: "\f2c9"; -} - -.fa-thermometer-half { - --fa: "\f2c9"; -} - -.fa-temperature-quarter { - --fa: "\f2ca"; -} - -.fa-temperature-1 { - --fa: "\f2ca"; -} - -.fa-thermometer-1 { - --fa: "\f2ca"; -} - -.fa-thermometer-quarter { - --fa: "\f2ca"; -} - -.fa-temperature-empty { - --fa: "\f2cb"; -} - -.fa-temperature-0 { - --fa: "\f2cb"; -} - -.fa-thermometer-0 { - --fa: "\f2cb"; -} - -.fa-thermometer-empty { - --fa: "\f2cb"; -} - -.fa-shower { - --fa: "\f2cc"; -} - -.fa-bath { - --fa: "\f2cd"; -} - -.fa-bathtub { - --fa: "\f2cd"; -} - -.fa-podcast { - --fa: "\f2ce"; -} - -.fa-window-maximize { - --fa: "\f2d0"; -} - -.fa-window-minimize { - --fa: "\f2d1"; -} - -.fa-window-restore { - --fa: "\f2d2"; -} - -.fa-square-xmark { - --fa: "\f2d3"; -} - -.fa-times-square { - --fa: "\f2d3"; -} - -.fa-xmark-square { - --fa: "\f2d3"; -} - -.fa-microchip { - --fa: "\f2db"; -} - -.fa-snowflake { - --fa: "\f2dc"; -} - -.fa-spoon { - --fa: "\f2e5"; -} - -.fa-utensil-spoon { - --fa: "\f2e5"; -} - -.fa-utensils { - --fa: "\f2e7"; -} - -.fa-cutlery { - --fa: "\f2e7"; -} - -.fa-rotate-left { - --fa: "\f2ea"; -} - -.fa-rotate-back { - --fa: "\f2ea"; -} - -.fa-rotate-backward { - --fa: "\f2ea"; -} - -.fa-undo-alt { - --fa: "\f2ea"; -} - -.fa-trash-can { - --fa: "\f2ed"; -} - -.fa-trash-alt { - --fa: "\f2ed"; -} - -.fa-rotate { - --fa: "\f2f1"; -} - -.fa-sync-alt { - --fa: "\f2f1"; -} - -.fa-stopwatch { - --fa: "\f2f2"; -} - -.fa-right-from-bracket { - --fa: "\f2f5"; -} - -.fa-sign-out-alt { - --fa: "\f2f5"; -} - -.fa-right-to-bracket { - --fa: "\f2f6"; -} - -.fa-sign-in-alt { - --fa: "\f2f6"; -} - -.fa-rotate-right { - --fa: "\f2f9"; -} - -.fa-redo-alt { - --fa: "\f2f9"; -} - -.fa-rotate-forward { - --fa: "\f2f9"; -} - -.fa-poo { - --fa: "\f2fe"; -} - -.fa-images { - --fa: "\f302"; -} - -.fa-pencil { - --fa: "\f303"; -} - -.fa-pencil-alt { - --fa: "\f303"; -} - -.fa-pen { - --fa: "\f304"; -} - -.fa-pen-clip { - --fa: "\f305"; -} - -.fa-pen-alt { - --fa: "\f305"; -} - -.fa-octagon { - --fa: "\f306"; -} - -.fa-down-long { - --fa: "\f309"; -} - -.fa-long-arrow-alt-down { - --fa: "\f309"; -} - -.fa-left-long { - --fa: "\f30a"; -} - -.fa-long-arrow-alt-left { - --fa: "\f30a"; -} - -.fa-right-long { - --fa: "\f30b"; -} - -.fa-long-arrow-alt-right { - --fa: "\f30b"; -} - -.fa-up-long { - --fa: "\f30c"; -} - -.fa-long-arrow-alt-up { - --fa: "\f30c"; -} - -.fa-hexagon { - --fa: "\f312"; -} - -.fa-file-pen { - --fa: "\f31c"; -} - -.fa-file-edit { - --fa: "\f31c"; -} - -.fa-maximize { - --fa: "\f31e"; -} - -.fa-expand-arrows-alt { - --fa: "\f31e"; -} - -.fa-clipboard { - --fa: "\f328"; -} - -.fa-left-right { - --fa: "\f337"; -} - -.fa-arrows-alt-h { - --fa: "\f337"; -} - -.fa-up-down { - --fa: "\f338"; -} - -.fa-arrows-alt-v { - --fa: "\f338"; -} - -.fa-alarm-clock { - --fa: "\f34e"; -} - -.fa-circle-down { - --fa: "\f358"; -} - -.fa-arrow-alt-circle-down { - --fa: "\f358"; -} - -.fa-circle-left { - --fa: "\f359"; -} - -.fa-arrow-alt-circle-left { - --fa: "\f359"; -} - -.fa-circle-right { - --fa: "\f35a"; -} - -.fa-arrow-alt-circle-right { - --fa: "\f35a"; -} - -.fa-circle-up { - --fa: "\f35b"; -} - -.fa-arrow-alt-circle-up { - --fa: "\f35b"; -} - -.fa-up-right-from-square { - --fa: "\f35d"; -} - -.fa-external-link-alt { - --fa: "\f35d"; -} - -.fa-square-up-right { - --fa: "\f360"; -} - -.fa-external-link-square-alt { - --fa: "\f360"; -} - -.fa-right-left { - --fa: "\f362"; -} - -.fa-exchange-alt { - --fa: "\f362"; -} - -.fa-repeat { - --fa: "\f363"; -} - -.fa-code-commit { - --fa: "\f386"; -} - -.fa-code-merge { - --fa: "\f387"; -} - -.fa-desktop { - --fa: "\f390"; -} - -.fa-desktop-alt { - --fa: "\f390"; -} - -.fa-gem { - --fa: "\f3a5"; -} - -.fa-turn-down { - --fa: "\f3be"; -} - -.fa-level-down-alt { - --fa: "\f3be"; -} - -.fa-turn-up { - --fa: "\f3bf"; -} - -.fa-level-up-alt { - --fa: "\f3bf"; -} - -.fa-lock-open { - --fa: "\f3c1"; -} - -.fa-location-dot { - --fa: "\f3c5"; -} - -.fa-map-marker-alt { - --fa: "\f3c5"; -} - -.fa-microphone-lines { - --fa: "\f3c9"; -} - -.fa-microphone-alt { - --fa: "\f3c9"; -} - -.fa-mobile-screen-button { - --fa: "\f3cd"; -} - -.fa-mobile-alt { - --fa: "\f3cd"; -} - -.fa-mobile { - --fa: "\f3ce"; -} - -.fa-mobile-android { - --fa: "\f3ce"; -} - -.fa-mobile-phone { - --fa: "\f3ce"; -} - -.fa-mobile-screen { - --fa: "\f3cf"; -} - -.fa-mobile-android-alt { - --fa: "\f3cf"; -} - -.fa-money-bill-1 { - --fa: "\f3d1"; -} - -.fa-money-bill-alt { - --fa: "\f3d1"; -} - -.fa-phone-slash { - --fa: "\f3dd"; -} - -.fa-image-portrait { - --fa: "\f3e0"; -} - -.fa-portrait { - --fa: "\f3e0"; -} - -.fa-reply { - --fa: "\f3e5"; -} - -.fa-mail-reply { - --fa: "\f3e5"; -} - -.fa-shield-halved { - --fa: "\f3ed"; -} - -.fa-shield-alt { - --fa: "\f3ed"; -} - -.fa-tablet-screen-button { - --fa: "\f3fa"; -} - -.fa-tablet-alt { - --fa: "\f3fa"; -} - -.fa-tablet { - --fa: "\f3fb"; -} - -.fa-tablet-android { - --fa: "\f3fb"; -} - -.fa-ticket-simple { - --fa: "\f3ff"; -} - -.fa-ticket-alt { - --fa: "\f3ff"; -} - -.fa-rectangle-xmark { - --fa: "\f410"; -} - -.fa-rectangle-times { - --fa: "\f410"; -} - -.fa-times-rectangle { - --fa: "\f410"; -} - -.fa-window-close { - --fa: "\f410"; -} - -.fa-down-left-and-up-right-to-center { - --fa: "\f422"; -} - -.fa-compress-alt { - --fa: "\f422"; -} - -.fa-up-right-and-down-left-from-center { - --fa: "\f424"; -} - -.fa-expand-alt { - --fa: "\f424"; -} - -.fa-baseball-bat-ball { - --fa: "\f432"; -} - -.fa-baseball { - --fa: "\f433"; -} - -.fa-baseball-ball { - --fa: "\f433"; -} - -.fa-basketball { - --fa: "\f434"; -} - -.fa-basketball-ball { - --fa: "\f434"; -} - -.fa-bowling-ball { - --fa: "\f436"; -} - -.fa-chess { - --fa: "\f439"; -} - -.fa-chess-bishop { - --fa: "\f43a"; -} - -.fa-chess-board { - --fa: "\f43c"; -} - -.fa-chess-king { - --fa: "\f43f"; -} - -.fa-chess-knight { - --fa: "\f441"; -} - -.fa-chess-pawn { - --fa: "\f443"; -} - -.fa-chess-queen { - --fa: "\f445"; -} - -.fa-chess-rook { - --fa: "\f447"; -} - -.fa-dumbbell { - --fa: "\f44b"; -} - -.fa-football { - --fa: "\f44e"; -} - -.fa-football-ball { - --fa: "\f44e"; -} - -.fa-golf-ball-tee { - --fa: "\f450"; -} - -.fa-golf-ball { - --fa: "\f450"; -} - -.fa-hockey-puck { - --fa: "\f453"; -} - -.fa-broom-ball { - --fa: "\f458"; -} - -.fa-quidditch { - --fa: "\f458"; -} - -.fa-quidditch-broom-ball { - --fa: "\f458"; -} - -.fa-square-full { - --fa: "\f45c"; -} - -.fa-table-tennis-paddle-ball { - --fa: "\f45d"; -} - -.fa-ping-pong-paddle-ball { - --fa: "\f45d"; -} - -.fa-table-tennis { - --fa: "\f45d"; -} - -.fa-volleyball { - --fa: "\f45f"; -} - -.fa-volleyball-ball { - --fa: "\f45f"; -} - -.fa-hand-dots { - --fa: "\f461"; -} - -.fa-allergies { - --fa: "\f461"; -} - -.fa-bandage { - --fa: "\f462"; -} - -.fa-band-aid { - --fa: "\f462"; -} - -.fa-box { - --fa: "\f466"; -} - -.fa-boxes-stacked { - --fa: "\f468"; -} - -.fa-boxes { - --fa: "\f468"; -} - -.fa-boxes-alt { - --fa: "\f468"; -} - -.fa-briefcase-medical { - --fa: "\f469"; -} - -.fa-fire-flame-simple { - --fa: "\f46a"; -} - -.fa-burn { - --fa: "\f46a"; -} - -.fa-capsules { - --fa: "\f46b"; -} - -.fa-clipboard-check { - --fa: "\f46c"; -} - -.fa-clipboard-list { - --fa: "\f46d"; -} - -.fa-person-dots-from-line { - --fa: "\f470"; -} - -.fa-diagnoses { - --fa: "\f470"; -} - -.fa-dna { - --fa: "\f471"; -} - -.fa-dolly { - --fa: "\f472"; -} - -.fa-dolly-box { - --fa: "\f472"; -} - -.fa-cart-flatbed { - --fa: "\f474"; -} - -.fa-dolly-flatbed { - --fa: "\f474"; -} - -.fa-file-medical { - --fa: "\f477"; -} - -.fa-file-waveform { - --fa: "\f478"; -} - -.fa-file-medical-alt { - --fa: "\f478"; -} - -.fa-kit-medical { - --fa: "\f479"; -} - -.fa-first-aid { - --fa: "\f479"; -} - -.fa-circle-h { - --fa: "\f47e"; -} - -.fa-hospital-symbol { - --fa: "\f47e"; -} - -.fa-id-card-clip { - --fa: "\f47f"; -} - -.fa-id-card-alt { - --fa: "\f47f"; -} - -.fa-notes-medical { - --fa: "\f481"; -} - -.fa-pallet { - --fa: "\f482"; -} - -.fa-pills { - --fa: "\f484"; -} - -.fa-prescription-bottle { - --fa: "\f485"; -} - -.fa-prescription-bottle-medical { - --fa: "\f486"; -} - -.fa-prescription-bottle-alt { - --fa: "\f486"; -} - -.fa-bed-pulse { - --fa: "\f487"; -} - -.fa-procedures { - --fa: "\f487"; -} - -.fa-truck-fast { - --fa: "\f48b"; -} - -.fa-shipping-fast { - --fa: "\f48b"; -} - -.fa-smoking { - --fa: "\f48d"; -} - -.fa-syringe { - --fa: "\f48e"; -} - -.fa-tablets { - --fa: "\f490"; -} - -.fa-thermometer { - --fa: "\f491"; -} - -.fa-vial { - --fa: "\f492"; -} - -.fa-vials { - --fa: "\f493"; -} - -.fa-warehouse { - --fa: "\f494"; -} - -.fa-weight-scale { - --fa: "\f496"; -} - -.fa-weight { - --fa: "\f496"; -} - -.fa-x-ray { - --fa: "\f497"; -} - -.fa-box-open { - --fa: "\f49e"; -} - -.fa-comment-dots { - --fa: "\f4ad"; -} - -.fa-commenting { - --fa: "\f4ad"; -} - -.fa-comment-slash { - --fa: "\f4b3"; -} - -.fa-couch { - --fa: "\f4b8"; -} - -.fa-circle-dollar-to-slot { - --fa: "\f4b9"; -} - -.fa-donate { - --fa: "\f4b9"; -} - -.fa-dove { - --fa: "\f4ba"; -} - -.fa-hand-holding { - --fa: "\f4bd"; -} - -.fa-hand-holding-heart { - --fa: "\f4be"; -} - -.fa-hand-holding-dollar { - --fa: "\f4c0"; -} - -.fa-hand-holding-usd { - --fa: "\f4c0"; -} - -.fa-hand-holding-droplet { - --fa: "\f4c1"; -} - -.fa-hand-holding-water { - --fa: "\f4c1"; -} - -.fa-hands-holding { - --fa: "\f4c2"; -} - -.fa-handshake-angle { - --fa: "\f4c4"; -} - -.fa-hands-helping { - --fa: "\f4c4"; -} - -.fa-parachute-box { - --fa: "\f4cd"; -} - -.fa-people-carry-box { - --fa: "\f4ce"; -} - -.fa-people-carry { - --fa: "\f4ce"; -} - -.fa-piggy-bank { - --fa: "\f4d3"; -} - -.fa-ribbon { - --fa: "\f4d6"; -} - -.fa-route { - --fa: "\f4d7"; -} - -.fa-seedling { - --fa: "\f4d8"; -} - -.fa-sprout { - --fa: "\f4d8"; -} - -.fa-sign-hanging { - --fa: "\f4d9"; -} - -.fa-sign { - --fa: "\f4d9"; -} - -.fa-face-smile-wink { - --fa: "\f4da"; -} - -.fa-smile-wink { - --fa: "\f4da"; -} - -.fa-tape { - --fa: "\f4db"; -} - -.fa-truck-ramp-box { - --fa: "\f4de"; -} - -.fa-truck-loading { - --fa: "\f4de"; -} - -.fa-truck-moving { - --fa: "\f4df"; -} - -.fa-video-slash { - --fa: "\f4e2"; -} - -.fa-wine-glass { - --fa: "\f4e3"; -} - -.fa-user-astronaut { - --fa: "\f4fb"; -} - -.fa-user-check { - --fa: "\f4fc"; -} - -.fa-user-clock { - --fa: "\f4fd"; -} - -.fa-user-gear { - --fa: "\f4fe"; -} - -.fa-user-cog { - --fa: "\f4fe"; -} - -.fa-user-pen { - --fa: "\f4ff"; -} - -.fa-user-edit { - --fa: "\f4ff"; -} - -.fa-user-group { - --fa: "\f500"; -} - -.fa-user-friends { - --fa: "\f500"; -} - -.fa-user-graduate { - --fa: "\f501"; -} - -.fa-user-lock { - --fa: "\f502"; -} - -.fa-user-minus { - --fa: "\f503"; -} - -.fa-user-ninja { - --fa: "\f504"; -} - -.fa-user-shield { - --fa: "\f505"; -} - -.fa-user-slash { - --fa: "\f506"; -} - -.fa-user-alt-slash { - --fa: "\f506"; -} - -.fa-user-large-slash { - --fa: "\f506"; -} - -.fa-user-tag { - --fa: "\f507"; -} - -.fa-user-tie { - --fa: "\f508"; -} - -.fa-users-gear { - --fa: "\f509"; -} - -.fa-users-cog { - --fa: "\f509"; -} - -.fa-scale-unbalanced { - --fa: "\f515"; -} - -.fa-balance-scale-left { - --fa: "\f515"; -} - -.fa-scale-unbalanced-flip { - --fa: "\f516"; -} - -.fa-balance-scale-right { - --fa: "\f516"; -} - -.fa-blender { - --fa: "\f517"; -} - -.fa-book-open { - --fa: "\f518"; -} - -.fa-tower-broadcast { - --fa: "\f519"; -} - -.fa-broadcast-tower { - --fa: "\f519"; -} - -.fa-broom { - --fa: "\f51a"; -} - -.fa-chalkboard { - --fa: "\f51b"; -} - -.fa-blackboard { - --fa: "\f51b"; -} - -.fa-chalkboard-user { - --fa: "\f51c"; -} - -.fa-chalkboard-teacher { - --fa: "\f51c"; -} - -.fa-church { - --fa: "\f51d"; -} - -.fa-coins { - --fa: "\f51e"; -} - -.fa-compact-disc { - --fa: "\f51f"; -} - -.fa-crow { - --fa: "\f520"; -} - -.fa-crown { - --fa: "\f521"; -} - -.fa-dice { - --fa: "\f522"; -} - -.fa-dice-five { - --fa: "\f523"; -} - -.fa-dice-four { - --fa: "\f524"; -} - -.fa-dice-one { - --fa: "\f525"; -} - -.fa-dice-six { - --fa: "\f526"; -} - -.fa-dice-three { - --fa: "\f527"; -} - -.fa-dice-two { - --fa: "\f528"; -} - -.fa-divide { - --fa: "\f529"; -} - -.fa-door-closed { - --fa: "\f52a"; -} - -.fa-door-open { - --fa: "\f52b"; -} - -.fa-feather { - --fa: "\f52d"; -} - -.fa-frog { - --fa: "\f52e"; -} - -.fa-gas-pump { - --fa: "\f52f"; -} - -.fa-glasses { - --fa: "\f530"; -} - -.fa-greater-than-equal { - --fa: "\f532"; -} - -.fa-helicopter { - --fa: "\f533"; -} - -.fa-infinity { - --fa: "\f534"; -} - -.fa-kiwi-bird { - --fa: "\f535"; -} - -.fa-less-than-equal { - --fa: "\f537"; -} - -.fa-memory { - --fa: "\f538"; -} - -.fa-microphone-lines-slash { - --fa: "\f539"; -} - -.fa-microphone-alt-slash { - --fa: "\f539"; -} - -.fa-money-bill-wave { - --fa: "\f53a"; -} - -.fa-money-bill-1-wave { - --fa: "\f53b"; -} - -.fa-money-bill-wave-alt { - --fa: "\f53b"; -} - -.fa-money-check { - --fa: "\f53c"; -} - -.fa-money-check-dollar { - --fa: "\f53d"; -} - -.fa-money-check-alt { - --fa: "\f53d"; -} - -.fa-not-equal { - --fa: "\f53e"; -} - -.fa-palette { - --fa: "\f53f"; -} - -.fa-square-parking { - --fa: "\f540"; -} - -.fa-parking { - --fa: "\f540"; -} - -.fa-diagram-project { - --fa: "\f542"; -} - -.fa-project-diagram { - --fa: "\f542"; -} - -.fa-receipt { - --fa: "\f543"; -} - -.fa-robot { - --fa: "\f544"; -} - -.fa-ruler { - --fa: "\f545"; -} - -.fa-ruler-combined { - --fa: "\f546"; -} - -.fa-ruler-horizontal { - --fa: "\f547"; -} - -.fa-ruler-vertical { - --fa: "\f548"; -} - -.fa-school { - --fa: "\f549"; -} - -.fa-screwdriver { - --fa: "\f54a"; -} - -.fa-shoe-prints { - --fa: "\f54b"; -} - -.fa-skull { - --fa: "\f54c"; -} - -.fa-ban-smoking { - --fa: "\f54d"; -} - -.fa-smoking-ban { - --fa: "\f54d"; -} - -.fa-store { - --fa: "\f54e"; -} - -.fa-shop { - --fa: "\f54f"; -} - -.fa-store-alt { - --fa: "\f54f"; -} - -.fa-bars-staggered { - --fa: "\f550"; -} - -.fa-reorder { - --fa: "\f550"; -} - -.fa-stream { - --fa: "\f550"; -} - -.fa-stroopwafel { - --fa: "\f551"; -} - -.fa-toolbox { - --fa: "\f552"; -} - -.fa-shirt { - --fa: "\f553"; -} - -.fa-t-shirt { - --fa: "\f553"; -} - -.fa-tshirt { - --fa: "\f553"; -} - -.fa-person-walking { - --fa: "\f554"; -} - -.fa-walking { - --fa: "\f554"; -} - -.fa-wallet { - --fa: "\f555"; -} - -.fa-face-angry { - --fa: "\f556"; -} - -.fa-angry { - --fa: "\f556"; -} - -.fa-archway { - --fa: "\f557"; -} - -.fa-book-atlas { - --fa: "\f558"; -} - -.fa-atlas { - --fa: "\f558"; -} - -.fa-award { - --fa: "\f559"; -} - -.fa-delete-left { - --fa: "\f55a"; -} - -.fa-backspace { - --fa: "\f55a"; -} - -.fa-bezier-curve { - --fa: "\f55b"; -} - -.fa-bong { - --fa: "\f55c"; -} - -.fa-brush { - --fa: "\f55d"; -} - -.fa-bus-simple { - --fa: "\f55e"; -} - -.fa-bus-alt { - --fa: "\f55e"; -} - -.fa-cannabis { - --fa: "\f55f"; -} - -.fa-check-double { - --fa: "\f560"; -} - -.fa-martini-glass-citrus { - --fa: "\f561"; -} - -.fa-cocktail { - --fa: "\f561"; -} - -.fa-bell-concierge { - --fa: "\f562"; -} - -.fa-concierge-bell { - --fa: "\f562"; -} - -.fa-cookie { - --fa: "\f563"; -} - -.fa-cookie-bite { - --fa: "\f564"; -} - -.fa-crop-simple { - --fa: "\f565"; -} - -.fa-crop-alt { - --fa: "\f565"; -} - -.fa-tachograph-digital { - --fa: "\f566"; -} - -.fa-digital-tachograph { - --fa: "\f566"; -} - -.fa-face-dizzy { - --fa: "\f567"; -} - -.fa-dizzy { - --fa: "\f567"; -} - -.fa-compass-drafting { - --fa: "\f568"; -} - -.fa-drafting-compass { - --fa: "\f568"; -} - -.fa-drum { - --fa: "\f569"; -} - -.fa-drum-steelpan { - --fa: "\f56a"; -} - -.fa-feather-pointed { - --fa: "\f56b"; -} - -.fa-feather-alt { - --fa: "\f56b"; -} - -.fa-file-contract { - --fa: "\f56c"; -} - -.fa-file-arrow-down { - --fa: "\f56d"; -} - -.fa-file-download { - --fa: "\f56d"; -} - -.fa-file-export { - --fa: "\f56e"; -} - -.fa-arrow-right-from-file { - --fa: "\f56e"; -} - -.fa-file-import { - --fa: "\f56f"; -} - -.fa-arrow-right-to-file { - --fa: "\f56f"; -} - -.fa-file-invoice { - --fa: "\f570"; -} - -.fa-file-invoice-dollar { - --fa: "\f571"; -} - -.fa-file-prescription { - --fa: "\f572"; -} - -.fa-file-signature { - --fa: "\f573"; -} - -.fa-file-arrow-up { - --fa: "\f574"; -} - -.fa-file-upload { - --fa: "\f574"; -} - -.fa-fill { - --fa: "\f575"; -} - -.fa-fill-drip { - --fa: "\f576"; -} - -.fa-fingerprint { - --fa: "\f577"; -} - -.fa-fish { - --fa: "\f578"; -} - -.fa-face-flushed { - --fa: "\f579"; -} - -.fa-flushed { - --fa: "\f579"; -} - -.fa-face-frown-open { - --fa: "\f57a"; -} - -.fa-frown-open { - --fa: "\f57a"; -} - -.fa-martini-glass { - --fa: "\f57b"; -} - -.fa-glass-martini-alt { - --fa: "\f57b"; -} - -.fa-earth-africa { - --fa: "\f57c"; -} - -.fa-globe-africa { - --fa: "\f57c"; -} - -.fa-earth-americas { - --fa: "\f57d"; -} - -.fa-earth { - --fa: "\f57d"; -} - -.fa-earth-america { - --fa: "\f57d"; -} - -.fa-globe-americas { - --fa: "\f57d"; -} - -.fa-earth-asia { - --fa: "\f57e"; -} - -.fa-globe-asia { - --fa: "\f57e"; -} - -.fa-face-grimace { - --fa: "\f57f"; -} - -.fa-grimace { - --fa: "\f57f"; -} - -.fa-face-grin { - --fa: "\f580"; -} - -.fa-grin { - --fa: "\f580"; -} - -.fa-face-grin-wide { - --fa: "\f581"; -} - -.fa-grin-alt { - --fa: "\f581"; -} - -.fa-face-grin-beam { - --fa: "\f582"; -} - -.fa-grin-beam { - --fa: "\f582"; -} - -.fa-face-grin-beam-sweat { - --fa: "\f583"; -} - -.fa-grin-beam-sweat { - --fa: "\f583"; -} - -.fa-face-grin-hearts { - --fa: "\f584"; -} - -.fa-grin-hearts { - --fa: "\f584"; -} - -.fa-face-grin-squint { - --fa: "\f585"; -} - -.fa-grin-squint { - --fa: "\f585"; -} - -.fa-face-grin-squint-tears { - --fa: "\f586"; -} - -.fa-grin-squint-tears { - --fa: "\f586"; -} - -.fa-face-grin-stars { - --fa: "\f587"; -} - -.fa-grin-stars { - --fa: "\f587"; -} - -.fa-face-grin-tears { - --fa: "\f588"; -} - -.fa-grin-tears { - --fa: "\f588"; -} - -.fa-face-grin-tongue { - --fa: "\f589"; -} - -.fa-grin-tongue { - --fa: "\f589"; -} - -.fa-face-grin-tongue-squint { - --fa: "\f58a"; -} - -.fa-grin-tongue-squint { - --fa: "\f58a"; -} - -.fa-face-grin-tongue-wink { - --fa: "\f58b"; -} - -.fa-grin-tongue-wink { - --fa: "\f58b"; -} - -.fa-face-grin-wink { - --fa: "\f58c"; -} - -.fa-grin-wink { - --fa: "\f58c"; -} - -.fa-grip { - --fa: "\f58d"; -} - -.fa-grid-horizontal { - --fa: "\f58d"; -} - -.fa-grip-horizontal { - --fa: "\f58d"; -} - -.fa-grip-vertical { - --fa: "\f58e"; -} - -.fa-grid-vertical { - --fa: "\f58e"; -} - -.fa-headset { - --fa: "\f590"; -} - -.fa-highlighter { - --fa: "\f591"; -} - -.fa-hot-tub-person { - --fa: "\f593"; -} - -.fa-hot-tub { - --fa: "\f593"; -} - -.fa-hotel { - --fa: "\f594"; -} - -.fa-joint { - --fa: "\f595"; -} - -.fa-face-kiss { - --fa: "\f596"; -} - -.fa-kiss { - --fa: "\f596"; -} - -.fa-face-kiss-beam { - --fa: "\f597"; -} - -.fa-kiss-beam { - --fa: "\f597"; -} - -.fa-face-kiss-wink-heart { - --fa: "\f598"; -} - -.fa-kiss-wink-heart { - --fa: "\f598"; -} - -.fa-face-laugh { - --fa: "\f599"; -} - -.fa-laugh { - --fa: "\f599"; -} - -.fa-face-laugh-beam { - --fa: "\f59a"; -} - -.fa-laugh-beam { - --fa: "\f59a"; -} - -.fa-face-laugh-squint { - --fa: "\f59b"; -} - -.fa-laugh-squint { - --fa: "\f59b"; -} - -.fa-face-laugh-wink { - --fa: "\f59c"; -} - -.fa-laugh-wink { - --fa: "\f59c"; -} - -.fa-cart-flatbed-suitcase { - --fa: "\f59d"; -} - -.fa-luggage-cart { - --fa: "\f59d"; -} - -.fa-map-location { - --fa: "\f59f"; -} - -.fa-map-marked { - --fa: "\f59f"; -} - -.fa-map-location-dot { - --fa: "\f5a0"; -} - -.fa-map-marked-alt { - --fa: "\f5a0"; -} - -.fa-marker { - --fa: "\f5a1"; -} - -.fa-medal { - --fa: "\f5a2"; -} - -.fa-face-meh-blank { - --fa: "\f5a4"; -} - -.fa-meh-blank { - --fa: "\f5a4"; -} - -.fa-face-rolling-eyes { - --fa: "\f5a5"; -} - -.fa-meh-rolling-eyes { - --fa: "\f5a5"; -} - -.fa-monument { - --fa: "\f5a6"; -} - -.fa-mortar-pestle { - --fa: "\f5a7"; -} - -.fa-paint-roller { - --fa: "\f5aa"; -} - -.fa-passport { - --fa: "\f5ab"; -} - -.fa-pen-fancy { - --fa: "\f5ac"; -} - -.fa-pen-nib { - --fa: "\f5ad"; -} - -.fa-pen-ruler { - --fa: "\f5ae"; -} - -.fa-pencil-ruler { - --fa: "\f5ae"; -} - -.fa-plane-arrival { - --fa: "\f5af"; -} - -.fa-plane-departure { - --fa: "\f5b0"; -} - -.fa-prescription { - --fa: "\f5b1"; -} - -.fa-face-sad-cry { - --fa: "\f5b3"; -} - -.fa-sad-cry { - --fa: "\f5b3"; -} - -.fa-face-sad-tear { - --fa: "\f5b4"; -} - -.fa-sad-tear { - --fa: "\f5b4"; -} - -.fa-van-shuttle { - --fa: "\f5b6"; -} - -.fa-shuttle-van { - --fa: "\f5b6"; -} - -.fa-signature { - --fa: "\f5b7"; -} - -.fa-face-smile-beam { - --fa: "\f5b8"; -} - -.fa-smile-beam { - --fa: "\f5b8"; -} - -.fa-solar-panel { - --fa: "\f5ba"; -} - -.fa-spa { - --fa: "\f5bb"; -} - -.fa-splotch { - --fa: "\f5bc"; -} - -.fa-spray-can { - --fa: "\f5bd"; -} - -.fa-stamp { - --fa: "\f5bf"; -} - -.fa-star-half-stroke { - --fa: "\f5c0"; -} - -.fa-star-half-alt { - --fa: "\f5c0"; -} - -.fa-suitcase-rolling { - --fa: "\f5c1"; -} - -.fa-face-surprise { - --fa: "\f5c2"; -} - -.fa-surprise { - --fa: "\f5c2"; -} - -.fa-swatchbook { - --fa: "\f5c3"; -} - -.fa-person-swimming { - --fa: "\f5c4"; -} - -.fa-swimmer { - --fa: "\f5c4"; -} - -.fa-water-ladder { - --fa: "\f5c5"; -} - -.fa-ladder-water { - --fa: "\f5c5"; -} - -.fa-swimming-pool { - --fa: "\f5c5"; -} - -.fa-droplet-slash { - --fa: "\f5c7"; -} - -.fa-tint-slash { - --fa: "\f5c7"; -} - -.fa-face-tired { - --fa: "\f5c8"; -} - -.fa-tired { - --fa: "\f5c8"; -} - -.fa-tooth { - --fa: "\f5c9"; -} - -.fa-umbrella-beach { - --fa: "\f5ca"; -} - -.fa-weight-hanging { - --fa: "\f5cd"; -} - -.fa-wine-glass-empty { - --fa: "\f5ce"; -} - -.fa-wine-glass-alt { - --fa: "\f5ce"; -} - -.fa-spray-can-sparkles { - --fa: "\f5d0"; -} - -.fa-air-freshener { - --fa: "\f5d0"; -} - -.fa-apple-whole { - --fa: "\f5d1"; -} - -.fa-apple-alt { - --fa: "\f5d1"; -} - -.fa-atom { - --fa: "\f5d2"; -} - -.fa-bone { - --fa: "\f5d7"; -} - -.fa-book-open-reader { - --fa: "\f5da"; -} - -.fa-book-reader { - --fa: "\f5da"; -} - -.fa-brain { - --fa: "\f5dc"; -} - -.fa-car-rear { - --fa: "\f5de"; -} - -.fa-car-alt { - --fa: "\f5de"; -} - -.fa-car-battery { - --fa: "\f5df"; -} - -.fa-battery-car { - --fa: "\f5df"; -} - -.fa-car-burst { - --fa: "\f5e1"; -} - -.fa-car-crash { - --fa: "\f5e1"; -} - -.fa-car-side { - --fa: "\f5e4"; -} - -.fa-charging-station { - --fa: "\f5e7"; -} - -.fa-diamond-turn-right { - --fa: "\f5eb"; -} - -.fa-directions { - --fa: "\f5eb"; -} - -.fa-draw-polygon { - --fa: "\f5ee"; -} - -.fa-vector-polygon { - --fa: "\f5ee"; -} - -.fa-laptop-code { - --fa: "\f5fc"; -} - -.fa-layer-group { - --fa: "\f5fd"; -} - -.fa-location-crosshairs { - --fa: "\f601"; -} - -.fa-location { - --fa: "\f601"; -} - -.fa-lungs { - --fa: "\f604"; -} - -.fa-microscope { - --fa: "\f610"; -} - -.fa-oil-can { - --fa: "\f613"; -} - -.fa-poop { - --fa: "\f619"; -} - -.fa-shapes { - --fa: "\f61f"; -} - -.fa-triangle-circle-square { - --fa: "\f61f"; -} - -.fa-star-of-life { - --fa: "\f621"; -} - -.fa-gauge { - --fa: "\f624"; -} - -.fa-dashboard { - --fa: "\f624"; -} - -.fa-gauge-med { - --fa: "\f624"; -} - -.fa-tachometer-alt-average { - --fa: "\f624"; -} - -.fa-gauge-high { - --fa: "\f625"; -} - -.fa-tachometer-alt { - --fa: "\f625"; -} - -.fa-tachometer-alt-fast { - --fa: "\f625"; -} - -.fa-gauge-simple { - --fa: "\f629"; -} - -.fa-gauge-simple-med { - --fa: "\f629"; -} - -.fa-tachometer-average { - --fa: "\f629"; -} - -.fa-gauge-simple-high { - --fa: "\f62a"; -} - -.fa-tachometer { - --fa: "\f62a"; -} - -.fa-tachometer-fast { - --fa: "\f62a"; -} - -.fa-teeth { - --fa: "\f62e"; -} - -.fa-teeth-open { - --fa: "\f62f"; -} - -.fa-masks-theater { - --fa: "\f630"; -} - -.fa-theater-masks { - --fa: "\f630"; -} - -.fa-traffic-light { - --fa: "\f637"; -} - -.fa-truck-monster { - --fa: "\f63b"; -} - -.fa-truck-pickup { - --fa: "\f63c"; -} - -.fa-rectangle-ad { - --fa: "\f641"; -} - -.fa-ad { - --fa: "\f641"; -} - -.fa-ankh { - --fa: "\f644"; -} - -.fa-book-bible { - --fa: "\f647"; -} - -.fa-bible { - --fa: "\f647"; -} - -.fa-business-time { - --fa: "\f64a"; -} - -.fa-briefcase-clock { - --fa: "\f64a"; -} - -.fa-city { - --fa: "\f64f"; -} - -.fa-comment-dollar { - --fa: "\f651"; -} - -.fa-comments-dollar { - --fa: "\f653"; -} - -.fa-cross { - --fa: "\f654"; -} - -.fa-dharmachakra { - --fa: "\f655"; -} - -.fa-envelope-open-text { - --fa: "\f658"; -} - -.fa-folder-minus { - --fa: "\f65d"; -} - -.fa-folder-plus { - --fa: "\f65e"; -} - -.fa-filter-circle-dollar { - --fa: "\f662"; -} - -.fa-funnel-dollar { - --fa: "\f662"; -} - -.fa-gopuram { - --fa: "\f664"; -} - -.fa-hamsa { - --fa: "\f665"; -} - -.fa-bahai { - --fa: "\f666"; -} - -.fa-haykal { - --fa: "\f666"; -} - -.fa-jedi { - --fa: "\f669"; -} - -.fa-book-journal-whills { - --fa: "\f66a"; -} - -.fa-journal-whills { - --fa: "\f66a"; -} - -.fa-kaaba { - --fa: "\f66b"; -} - -.fa-khanda { - --fa: "\f66d"; -} - -.fa-landmark { - --fa: "\f66f"; -} - -.fa-envelopes-bulk { - --fa: "\f674"; -} - -.fa-mail-bulk { - --fa: "\f674"; -} - -.fa-menorah { - --fa: "\f676"; -} - -.fa-mosque { - --fa: "\f678"; -} - -.fa-om { - --fa: "\f679"; -} - -.fa-spaghetti-monster-flying { - --fa: "\f67b"; -} - -.fa-pastafarianism { - --fa: "\f67b"; -} - -.fa-peace { - --fa: "\f67c"; -} - -.fa-place-of-worship { - --fa: "\f67f"; -} - -.fa-square-poll-vertical { - --fa: "\f681"; -} - -.fa-poll { - --fa: "\f681"; -} - -.fa-square-poll-horizontal { - --fa: "\f682"; -} - -.fa-poll-h { - --fa: "\f682"; -} - -.fa-person-praying { - --fa: "\f683"; -} - -.fa-pray { - --fa: "\f683"; -} - -.fa-hands-praying { - --fa: "\f684"; -} - -.fa-praying-hands { - --fa: "\f684"; -} - -.fa-book-quran { - --fa: "\f687"; -} - -.fa-quran { - --fa: "\f687"; -} - -.fa-magnifying-glass-dollar { - --fa: "\f688"; -} - -.fa-search-dollar { - --fa: "\f688"; -} - -.fa-magnifying-glass-location { - --fa: "\f689"; -} - -.fa-search-location { - --fa: "\f689"; -} - -.fa-socks { - --fa: "\f696"; -} - -.fa-square-root-variable { - --fa: "\f698"; -} - -.fa-square-root-alt { - --fa: "\f698"; -} - -.fa-star-and-crescent { - --fa: "\f699"; -} - -.fa-star-of-david { - --fa: "\f69a"; -} - -.fa-synagogue { - --fa: "\f69b"; -} - -.fa-scroll-torah { - --fa: "\f6a0"; -} - -.fa-torah { - --fa: "\f6a0"; -} - -.fa-torii-gate { - --fa: "\f6a1"; -} - -.fa-vihara { - --fa: "\f6a7"; -} - -.fa-volume-xmark { - --fa: "\f6a9"; -} - -.fa-volume-mute { - --fa: "\f6a9"; -} - -.fa-volume-times { - --fa: "\f6a9"; -} - -.fa-yin-yang { - --fa: "\f6ad"; -} - -.fa-blender-phone { - --fa: "\f6b6"; -} - -.fa-book-skull { - --fa: "\f6b7"; -} - -.fa-book-dead { - --fa: "\f6b7"; -} - -.fa-campground { - --fa: "\f6bb"; -} - -.fa-cat { - --fa: "\f6be"; -} - -.fa-chair { - --fa: "\f6c0"; -} - -.fa-cloud-moon { - --fa: "\f6c3"; -} - -.fa-cloud-sun { - --fa: "\f6c4"; -} - -.fa-cow { - --fa: "\f6c8"; -} - -.fa-dice-d20 { - --fa: "\f6cf"; -} - -.fa-dice-d6 { - --fa: "\f6d1"; -} - -.fa-dog { - --fa: "\f6d3"; -} - -.fa-dragon { - --fa: "\f6d5"; -} - -.fa-drumstick-bite { - --fa: "\f6d7"; -} - -.fa-dungeon { - --fa: "\f6d9"; -} - -.fa-file-csv { - --fa: "\f6dd"; -} - -.fa-hand-fist { - --fa: "\f6de"; -} - -.fa-fist-raised { - --fa: "\f6de"; -} - -.fa-ghost { - --fa: "\f6e2"; -} - -.fa-hammer { - --fa: "\f6e3"; -} - -.fa-hanukiah { - --fa: "\f6e6"; -} - -.fa-hat-wizard { - --fa: "\f6e8"; -} - -.fa-person-hiking { - --fa: "\f6ec"; -} - -.fa-hiking { - --fa: "\f6ec"; -} - -.fa-hippo { - --fa: "\f6ed"; -} - -.fa-horse { - --fa: "\f6f0"; -} - -.fa-house-chimney-crack { - --fa: "\f6f1"; -} - -.fa-house-damage { - --fa: "\f6f1"; -} - -.fa-hryvnia-sign { - --fa: "\f6f2"; -} - -.fa-hryvnia { - --fa: "\f6f2"; -} - -.fa-mask { - --fa: "\f6fa"; -} - -.fa-mountain { - --fa: "\f6fc"; -} - -.fa-network-wired { - --fa: "\f6ff"; -} - -.fa-otter { - --fa: "\f700"; -} - -.fa-ring { - --fa: "\f70b"; -} - -.fa-person-running { - --fa: "\f70c"; -} - -.fa-running { - --fa: "\f70c"; -} - -.fa-scroll { - --fa: "\f70e"; -} - -.fa-skull-crossbones { - --fa: "\f714"; -} - -.fa-slash { - --fa: "\f715"; -} - -.fa-spider { - --fa: "\f717"; -} - -.fa-toilet-paper { - --fa: "\f71e"; -} - -.fa-toilet-paper-alt { - --fa: "\f71e"; -} - -.fa-toilet-paper-blank { - --fa: "\f71e"; -} - -.fa-tractor { - --fa: "\f722"; -} - -.fa-user-injured { - --fa: "\f728"; -} - -.fa-vr-cardboard { - --fa: "\f729"; -} - -.fa-wand-sparkles { - --fa: "\f72b"; -} - -.fa-wind { - --fa: "\f72e"; -} - -.fa-wine-bottle { - --fa: "\f72f"; -} - -.fa-cloud-meatball { - --fa: "\f73b"; -} - -.fa-cloud-moon-rain { - --fa: "\f73c"; -} - -.fa-cloud-rain { - --fa: "\f73d"; -} - -.fa-cloud-showers-heavy { - --fa: "\f740"; -} - -.fa-cloud-sun-rain { - --fa: "\f743"; -} - -.fa-democrat { - --fa: "\f747"; -} - -.fa-flag-usa { - --fa: "\f74d"; -} - -.fa-hurricane { - --fa: "\f751"; -} - -.fa-landmark-dome { - --fa: "\f752"; -} - -.fa-landmark-alt { - --fa: "\f752"; -} - -.fa-meteor { - --fa: "\f753"; -} - -.fa-person-booth { - --fa: "\f756"; -} - -.fa-poo-storm { - --fa: "\f75a"; -} - -.fa-poo-bolt { - --fa: "\f75a"; -} - -.fa-rainbow { - --fa: "\f75b"; -} - -.fa-republican { - --fa: "\f75e"; -} - -.fa-smog { - --fa: "\f75f"; -} - -.fa-temperature-high { - --fa: "\f769"; -} - -.fa-temperature-low { - --fa: "\f76b"; -} - -.fa-cloud-bolt { - --fa: "\f76c"; -} - -.fa-thunderstorm { - --fa: "\f76c"; -} - -.fa-tornado { - --fa: "\f76f"; -} - -.fa-volcano { - --fa: "\f770"; -} - -.fa-check-to-slot { - --fa: "\f772"; -} - -.fa-vote-yea { - --fa: "\f772"; -} - -.fa-water { - --fa: "\f773"; -} - -.fa-baby { - --fa: "\f77c"; -} - -.fa-baby-carriage { - --fa: "\f77d"; -} - -.fa-carriage-baby { - --fa: "\f77d"; -} - -.fa-biohazard { - --fa: "\f780"; -} - -.fa-blog { - --fa: "\f781"; -} - -.fa-calendar-day { - --fa: "\f783"; -} - -.fa-calendar-week { - --fa: "\f784"; -} - -.fa-candy-cane { - --fa: "\f786"; -} - -.fa-carrot { - --fa: "\f787"; -} - -.fa-cash-register { - --fa: "\f788"; -} - -.fa-minimize { - --fa: "\f78c"; -} - -.fa-compress-arrows-alt { - --fa: "\f78c"; -} - -.fa-dumpster { - --fa: "\f793"; -} - -.fa-dumpster-fire { - --fa: "\f794"; -} - -.fa-ethernet { - --fa: "\f796"; -} - -.fa-gifts { - --fa: "\f79c"; -} - -.fa-champagne-glasses { - --fa: "\f79f"; -} - -.fa-glass-cheers { - --fa: "\f79f"; -} - -.fa-whiskey-glass { - --fa: "\f7a0"; -} - -.fa-glass-whiskey { - --fa: "\f7a0"; -} - -.fa-earth-europe { - --fa: "\f7a2"; -} - -.fa-globe-europe { - --fa: "\f7a2"; -} - -.fa-grip-lines { - --fa: "\f7a4"; -} - -.fa-grip-lines-vertical { - --fa: "\f7a5"; -} - -.fa-guitar { - --fa: "\f7a6"; -} - -.fa-heart-crack { - --fa: "\f7a9"; -} - -.fa-heart-broken { - --fa: "\f7a9"; -} - -.fa-holly-berry { - --fa: "\f7aa"; -} - -.fa-horse-head { - --fa: "\f7ab"; -} - -.fa-icicles { - --fa: "\f7ad"; -} - -.fa-igloo { - --fa: "\f7ae"; -} - -.fa-mitten { - --fa: "\f7b5"; -} - -.fa-mug-hot { - --fa: "\f7b6"; -} - -.fa-radiation { - --fa: "\f7b9"; -} - -.fa-circle-radiation { - --fa: "\f7ba"; -} - -.fa-radiation-alt { - --fa: "\f7ba"; -} - -.fa-restroom { - --fa: "\f7bd"; -} - -.fa-satellite { - --fa: "\f7bf"; -} - -.fa-satellite-dish { - --fa: "\f7c0"; -} - -.fa-sd-card { - --fa: "\f7c2"; -} - -.fa-sim-card { - --fa: "\f7c4"; -} - -.fa-person-skating { - --fa: "\f7c5"; -} - -.fa-skating { - --fa: "\f7c5"; -} - -.fa-person-skiing { - --fa: "\f7c9"; -} - -.fa-skiing { - --fa: "\f7c9"; -} - -.fa-person-skiing-nordic { - --fa: "\f7ca"; -} - -.fa-skiing-nordic { - --fa: "\f7ca"; -} - -.fa-sleigh { - --fa: "\f7cc"; -} - -.fa-comment-sms { - --fa: "\f7cd"; -} - -.fa-sms { - --fa: "\f7cd"; -} - -.fa-person-snowboarding { - --fa: "\f7ce"; -} - -.fa-snowboarding { - --fa: "\f7ce"; -} - -.fa-snowman { - --fa: "\f7d0"; -} - -.fa-snowplow { - --fa: "\f7d2"; -} - -.fa-tenge-sign { - --fa: "\f7d7"; -} - -.fa-tenge { - --fa: "\f7d7"; -} - -.fa-toilet { - --fa: "\f7d8"; -} - -.fa-screwdriver-wrench { - --fa: "\f7d9"; -} - -.fa-tools { - --fa: "\f7d9"; -} - -.fa-cable-car { - --fa: "\f7da"; -} - -.fa-tram { - --fa: "\f7da"; -} - -.fa-fire-flame-curved { - --fa: "\f7e4"; -} - -.fa-fire-alt { - --fa: "\f7e4"; -} - -.fa-bacon { - --fa: "\f7e5"; -} - -.fa-book-medical { - --fa: "\f7e6"; -} - -.fa-bread-slice { - --fa: "\f7ec"; -} - -.fa-cheese { - --fa: "\f7ef"; -} - -.fa-house-chimney-medical { - --fa: "\f7f2"; -} - -.fa-clinic-medical { - --fa: "\f7f2"; -} - -.fa-clipboard-user { - --fa: "\f7f3"; -} - -.fa-comment-medical { - --fa: "\f7f5"; -} - -.fa-crutch { - --fa: "\f7f7"; -} - -.fa-disease { - --fa: "\f7fa"; -} - -.fa-egg { - --fa: "\f7fb"; -} - -.fa-folder-tree { - --fa: "\f802"; -} - -.fa-burger { - --fa: "\f805"; -} - -.fa-hamburger { - --fa: "\f805"; -} - -.fa-hand-middle-finger { - --fa: "\f806"; -} - -.fa-helmet-safety { - --fa: "\f807"; -} - -.fa-hard-hat { - --fa: "\f807"; -} - -.fa-hat-hard { - --fa: "\f807"; -} - -.fa-hospital-user { - --fa: "\f80d"; -} - -.fa-hotdog { - --fa: "\f80f"; -} - -.fa-ice-cream { - --fa: "\f810"; -} - -.fa-laptop-medical { - --fa: "\f812"; -} - -.fa-pager { - --fa: "\f815"; -} - -.fa-pepper-hot { - --fa: "\f816"; -} - -.fa-pizza-slice { - --fa: "\f818"; -} - -.fa-sack-dollar { - --fa: "\f81d"; -} - -.fa-book-tanakh { - --fa: "\f827"; -} - -.fa-tanakh { - --fa: "\f827"; -} - -.fa-bars-progress { - --fa: "\f828"; -} - -.fa-tasks-alt { - --fa: "\f828"; -} - -.fa-trash-arrow-up { - --fa: "\f829"; -} - -.fa-trash-restore { - --fa: "\f829"; -} - -.fa-trash-can-arrow-up { - --fa: "\f82a"; -} - -.fa-trash-restore-alt { - --fa: "\f82a"; -} - -.fa-user-nurse { - --fa: "\f82f"; -} - -.fa-wave-square { - --fa: "\f83e"; -} - -.fa-person-biking { - --fa: "\f84a"; -} - -.fa-biking { - --fa: "\f84a"; -} - -.fa-border-all { - --fa: "\f84c"; -} - -.fa-border-none { - --fa: "\f850"; -} - -.fa-border-top-left { - --fa: "\f853"; -} - -.fa-border-style { - --fa: "\f853"; -} - -.fa-person-digging { - --fa: "\f85e"; -} - -.fa-digging { - --fa: "\f85e"; -} - -.fa-fan { - --fa: "\f863"; -} - -.fa-icons { - --fa: "\f86d"; -} - -.fa-heart-music-camera-bolt { - --fa: "\f86d"; -} - -.fa-phone-flip { - --fa: "\f879"; -} - -.fa-phone-alt { - --fa: "\f879"; -} - -.fa-square-phone-flip { - --fa: "\f87b"; -} - -.fa-phone-square-alt { - --fa: "\f87b"; -} - -.fa-photo-film { - --fa: "\f87c"; -} - -.fa-photo-video { - --fa: "\f87c"; -} - -.fa-text-slash { - --fa: "\f87d"; -} - -.fa-remove-format { - --fa: "\f87d"; -} - -.fa-arrow-down-z-a { - --fa: "\f881"; -} - -.fa-sort-alpha-desc { - --fa: "\f881"; -} - -.fa-sort-alpha-down-alt { - --fa: "\f881"; -} - -.fa-arrow-up-z-a { - --fa: "\f882"; -} - -.fa-sort-alpha-up-alt { - --fa: "\f882"; -} - -.fa-arrow-down-short-wide { - --fa: "\f884"; -} - -.fa-sort-amount-desc { - --fa: "\f884"; -} - -.fa-sort-amount-down-alt { - --fa: "\f884"; -} - -.fa-arrow-up-short-wide { - --fa: "\f885"; -} - -.fa-sort-amount-up-alt { - --fa: "\f885"; -} - -.fa-arrow-down-9-1 { - --fa: "\f886"; -} - -.fa-sort-numeric-desc { - --fa: "\f886"; -} - -.fa-sort-numeric-down-alt { - --fa: "\f886"; -} - -.fa-arrow-up-9-1 { - --fa: "\f887"; -} - -.fa-sort-numeric-up-alt { - --fa: "\f887"; -} - -.fa-spell-check { - --fa: "\f891"; -} - -.fa-voicemail { - --fa: "\f897"; -} - -.fa-hat-cowboy { - --fa: "\f8c0"; -} - -.fa-hat-cowboy-side { - --fa: "\f8c1"; -} - -.fa-computer-mouse { - --fa: "\f8cc"; -} - -.fa-mouse { - --fa: "\f8cc"; -} - -.fa-radio { - --fa: "\f8d7"; -} - -.fa-record-vinyl { - --fa: "\f8d9"; -} - -.fa-walkie-talkie { - --fa: "\f8ef"; -} - -.fa-caravan { - --fa: "\f8ff"; -} -:root, :host { - --fa-family-brands: "Font Awesome 7 Brands"; - --fa-font-brands: normal 400 1em/1 var(--fa-family-brands); -} - -@font-face { - font-family: "Font Awesome 7 Brands"; - font-style: normal; - font-weight: 400; - font-display: block; - src: url("../webfonts/fa-brands-400.woff2"); -} -.fab, -.fa-brands, -.fa-classic.fa-brands { - --fa-family: var(--fa-family-brands); - --fa-style: 400; -} - -.fa-firefox-browser { - --fa: "\e007"; -} - -.fa-ideal { - --fa: "\e013"; -} - -.fa-microblog { - --fa: "\e01a"; -} - -.fa-square-pied-piper { - --fa: "\e01e"; -} - -.fa-pied-piper-square { - --fa: "\e01e"; -} - -.fa-unity { - --fa: "\e049"; -} - -.fa-dailymotion { - --fa: "\e052"; -} - -.fa-square-instagram { - --fa: "\e055"; -} - -.fa-instagram-square { - --fa: "\e055"; -} - -.fa-mixer { - --fa: "\e056"; -} - -.fa-shopify { - --fa: "\e057"; -} - -.fa-deezer { - --fa: "\e077"; -} - -.fa-edge-legacy { - --fa: "\e078"; -} - -.fa-google-pay { - --fa: "\e079"; -} - -.fa-rust { - --fa: "\e07a"; -} - -.fa-tiktok { - --fa: "\e07b"; -} - -.fa-unsplash { - --fa: "\e07c"; -} - -.fa-cloudflare { - --fa: "\e07d"; -} - -.fa-guilded { - --fa: "\e07e"; -} - -.fa-hive { - --fa: "\e07f"; -} - -.fa-42-group { - --fa: "\e080"; -} - -.fa-innosoft { - --fa: "\e080"; -} - -.fa-instalod { - --fa: "\e081"; -} - -.fa-octopus-deploy { - --fa: "\e082"; -} - -.fa-perbyte { - --fa: "\e083"; -} - -.fa-uncharted { - --fa: "\e084"; -} - -.fa-watchman-monitoring { - --fa: "\e087"; -} - -.fa-wodu { - --fa: "\e088"; -} - -.fa-wirsindhandwerk { - --fa: "\e2d0"; -} - -.fa-wsh { - --fa: "\e2d0"; -} - -.fa-bots { - --fa: "\e340"; -} - -.fa-cmplid { - --fa: "\e360"; -} - -.fa-bilibili { - --fa: "\e3d9"; -} - -.fa-golang { - --fa: "\e40f"; -} - -.fa-pix { - --fa: "\e43a"; -} - -.fa-sitrox { - --fa: "\e44a"; -} - -.fa-hashnode { - --fa: "\e499"; -} - -.fa-meta { - --fa: "\e49b"; -} - -.fa-padlet { - --fa: "\e4a0"; -} - -.fa-nfc-directional { - --fa: "\e530"; -} - -.fa-nfc-symbol { - --fa: "\e531"; -} - -.fa-screenpal { - --fa: "\e570"; -} - -.fa-space-awesome { - --fa: "\e5ac"; -} - -.fa-square-font-awesome { - --fa: "\e5ad"; -} - -.fa-square-gitlab { - --fa: "\e5ae"; -} - -.fa-gitlab-square { - --fa: "\e5ae"; -} - -.fa-odysee { - --fa: "\e5c6"; -} - -.fa-stubber { - --fa: "\e5c7"; -} - -.fa-debian { - --fa: "\e60b"; -} - -.fa-shoelace { - --fa: "\e60c"; -} - -.fa-threads { - --fa: "\e618"; -} - -.fa-square-threads { - --fa: "\e619"; -} - -.fa-square-x-twitter { - --fa: "\e61a"; -} - -.fa-x-twitter { - --fa: "\e61b"; -} - -.fa-opensuse { - --fa: "\e62b"; -} - -.fa-letterboxd { - --fa: "\e62d"; -} - -.fa-square-letterboxd { - --fa: "\e62e"; -} - -.fa-mintbit { - --fa: "\e62f"; -} - -.fa-google-scholar { - --fa: "\e63b"; -} - -.fa-brave { - --fa: "\e63c"; -} - -.fa-brave-reverse { - --fa: "\e63d"; -} - -.fa-pixiv { - --fa: "\e640"; -} - -.fa-upwork { - --fa: "\e641"; -} - -.fa-webflow { - --fa: "\e65c"; -} - -.fa-signal-messenger { - --fa: "\e663"; -} - -.fa-bluesky { - --fa: "\e671"; -} - -.fa-jxl { - --fa: "\e67b"; -} - -.fa-square-upwork { - --fa: "\e67c"; -} - -.fa-web-awesome { - --fa: "\e682"; -} - -.fa-square-web-awesome { - --fa: "\e683"; -} - -.fa-square-web-awesome-stroke { - --fa: "\e684"; -} - -.fa-dart-lang { - --fa: "\e693"; -} - -.fa-flutter { - --fa: "\e694"; -} - -.fa-files-pinwheel { - --fa: "\e69f"; -} - -.fa-css { - --fa: "\e6a2"; -} - -.fa-square-bluesky { - --fa: "\e6a3"; -} - -.fa-openai { - --fa: "\e7cf"; -} - -.fa-square-linkedin { - --fa: "\e7d0"; -} - -.fa-cash-app { - --fa: "\e7d4"; -} - -.fa-disqus { - --fa: "\e7d5"; -} - -.fa-eleventy { - --fa: "\e7d6"; -} - -.fa-11ty { - --fa: "\e7d6"; -} - -.fa-kakao-talk { - --fa: "\e7d7"; -} - -.fa-linktree { - --fa: "\e7d8"; -} - -.fa-notion { - --fa: "\e7d9"; -} - -.fa-pandora { - --fa: "\e7da"; -} - -.fa-pixelfed { - --fa: "\e7db"; -} - -.fa-tidal { - --fa: "\e7dc"; -} - -.fa-vsco { - --fa: "\e7dd"; -} - -.fa-w3c { - --fa: "\e7de"; -} - -.fa-lumon { - --fa: "\e7e2"; -} - -.fa-lumon-drop { - --fa: "\e7e3"; -} - -.fa-square-figma { - --fa: "\e7e4"; -} - -.fa-tex { - --fa: "\e7ff"; -} - -.fa-duolingo { - --fa: "\e812"; -} - -.fa-square-twitter { - --fa: "\f081"; -} - -.fa-twitter-square { - --fa: "\f081"; -} - -.fa-square-facebook { - --fa: "\f082"; -} - -.fa-facebook-square { - --fa: "\f082"; -} - -.fa-linkedin { - --fa: "\f08c"; -} - -.fa-square-github { - --fa: "\f092"; -} - -.fa-github-square { - --fa: "\f092"; -} - -.fa-twitter { - --fa: "\f099"; -} - -.fa-facebook { - --fa: "\f09a"; -} - -.fa-github { - --fa: "\f09b"; -} - -.fa-pinterest { - --fa: "\f0d2"; -} - -.fa-square-pinterest { - --fa: "\f0d3"; -} - -.fa-pinterest-square { - --fa: "\f0d3"; -} - -.fa-square-google-plus { - --fa: "\f0d4"; -} - -.fa-google-plus-square { - --fa: "\f0d4"; -} - -.fa-google-plus-g { - --fa: "\f0d5"; -} - -.fa-linkedin-in { - --fa: "\f0e1"; -} - -.fa-github-alt { - --fa: "\f113"; -} - -.fa-maxcdn { - --fa: "\f136"; -} - -.fa-html5 { - --fa: "\f13b"; -} - -.fa-css3 { - --fa: "\f13c"; -} - -.fa-btc { - --fa: "\f15a"; -} - -.fa-youtube { - --fa: "\f167"; -} - -.fa-xing { - --fa: "\f168"; -} - -.fa-square-xing { - --fa: "\f169"; -} - -.fa-xing-square { - --fa: "\f169"; -} - -.fa-dropbox { - --fa: "\f16b"; -} - -.fa-stack-overflow { - --fa: "\f16c"; -} - -.fa-instagram { - --fa: "\f16d"; -} - -.fa-flickr { - --fa: "\f16e"; -} - -.fa-adn { - --fa: "\f170"; -} - -.fa-bitbucket { - --fa: "\f171"; -} - -.fa-tumblr { - --fa: "\f173"; -} - -.fa-square-tumblr { - --fa: "\f174"; -} - -.fa-tumblr-square { - --fa: "\f174"; -} - -.fa-apple { - --fa: "\f179"; -} - -.fa-windows { - --fa: "\f17a"; -} - -.fa-android { - --fa: "\f17b"; -} - -.fa-linux { - --fa: "\f17c"; -} - -.fa-dribbble { - --fa: "\f17d"; -} - -.fa-skype { - --fa: "\f17e"; -} - -.fa-foursquare { - --fa: "\f180"; -} - -.fa-trello { - --fa: "\f181"; -} - -.fa-gratipay { - --fa: "\f184"; -} - -.fa-vk { - --fa: "\f189"; -} - -.fa-weibo { - --fa: "\f18a"; -} - -.fa-renren { - --fa: "\f18b"; -} - -.fa-pagelines { - --fa: "\f18c"; -} - -.fa-stack-exchange { - --fa: "\f18d"; -} - -.fa-square-vimeo { - --fa: "\f194"; -} - -.fa-vimeo-square { - --fa: "\f194"; -} - -.fa-slack { - --fa: "\f198"; -} - -.fa-slack-hash { - --fa: "\f198"; -} - -.fa-wordpress { - --fa: "\f19a"; -} - -.fa-openid { - --fa: "\f19b"; -} - -.fa-yahoo { - --fa: "\f19e"; -} - -.fa-google { - --fa: "\f1a0"; -} - -.fa-reddit { - --fa: "\f1a1"; -} - -.fa-square-reddit { - --fa: "\f1a2"; -} - -.fa-reddit-square { - --fa: "\f1a2"; -} - -.fa-stumbleupon-circle { - --fa: "\f1a3"; -} - -.fa-stumbleupon { - --fa: "\f1a4"; -} - -.fa-delicious { - --fa: "\f1a5"; -} - -.fa-digg { - --fa: "\f1a6"; -} - -.fa-pied-piper-pp { - --fa: "\f1a7"; -} - -.fa-pied-piper-alt { - --fa: "\f1a8"; -} - -.fa-drupal { - --fa: "\f1a9"; -} - -.fa-joomla { - --fa: "\f1aa"; -} - -.fa-behance { - --fa: "\f1b4"; -} - -.fa-square-behance { - --fa: "\f1b5"; -} - -.fa-behance-square { - --fa: "\f1b5"; -} - -.fa-steam { - --fa: "\f1b6"; -} - -.fa-square-steam { - --fa: "\f1b7"; -} - -.fa-steam-square { - --fa: "\f1b7"; -} - -.fa-spotify { - --fa: "\f1bc"; -} - -.fa-deviantart { - --fa: "\f1bd"; -} - -.fa-soundcloud { - --fa: "\f1be"; -} - -.fa-vine { - --fa: "\f1ca"; -} - -.fa-codepen { - --fa: "\f1cb"; -} - -.fa-jsfiddle { - --fa: "\f1cc"; -} - -.fa-rebel { - --fa: "\f1d0"; -} - -.fa-empire { - --fa: "\f1d1"; -} - -.fa-square-git { - --fa: "\f1d2"; -} - -.fa-git-square { - --fa: "\f1d2"; -} - -.fa-git { - --fa: "\f1d3"; -} - -.fa-hacker-news { - --fa: "\f1d4"; -} - -.fa-tencent-weibo { - --fa: "\f1d5"; -} - -.fa-qq { - --fa: "\f1d6"; -} - -.fa-weixin { - --fa: "\f1d7"; -} - -.fa-slideshare { - --fa: "\f1e7"; -} - -.fa-twitch { - --fa: "\f1e8"; -} - -.fa-yelp { - --fa: "\f1e9"; -} - -.fa-paypal { - --fa: "\f1ed"; -} - -.fa-google-wallet { - --fa: "\f1ee"; -} - -.fa-cc-visa { - --fa: "\f1f0"; -} - -.fa-cc-mastercard { - --fa: "\f1f1"; -} - -.fa-cc-discover { - --fa: "\f1f2"; -} - -.fa-cc-amex { - --fa: "\f1f3"; -} - -.fa-cc-paypal { - --fa: "\f1f4"; -} - -.fa-cc-stripe { - --fa: "\f1f5"; -} - -.fa-lastfm { - --fa: "\f202"; -} - -.fa-square-lastfm { - --fa: "\f203"; -} - -.fa-lastfm-square { - --fa: "\f203"; -} - -.fa-ioxhost { - --fa: "\f208"; -} - -.fa-angellist { - --fa: "\f209"; -} - -.fa-buysellads { - --fa: "\f20d"; -} - -.fa-connectdevelop { - --fa: "\f20e"; -} - -.fa-dashcube { - --fa: "\f210"; -} - -.fa-forumbee { - --fa: "\f211"; -} - -.fa-leanpub { - --fa: "\f212"; -} - -.fa-sellsy { - --fa: "\f213"; -} - -.fa-shirtsinbulk { - --fa: "\f214"; -} - -.fa-simplybuilt { - --fa: "\f215"; -} - -.fa-skyatlas { - --fa: "\f216"; -} - -.fa-pinterest-p { - --fa: "\f231"; -} - -.fa-whatsapp { - --fa: "\f232"; -} - -.fa-viacoin { - --fa: "\f237"; -} - -.fa-medium { - --fa: "\f23a"; -} - -.fa-medium-m { - --fa: "\f23a"; -} - -.fa-y-combinator { - --fa: "\f23b"; -} - -.fa-optin-monster { - --fa: "\f23c"; -} - -.fa-opencart { - --fa: "\f23d"; -} - -.fa-expeditedssl { - --fa: "\f23e"; -} - -.fa-cc-jcb { - --fa: "\f24b"; -} - -.fa-cc-diners-club { - --fa: "\f24c"; -} - -.fa-creative-commons { - --fa: "\f25e"; -} - -.fa-gg { - --fa: "\f260"; -} - -.fa-gg-circle { - --fa: "\f261"; -} - -.fa-odnoklassniki { - --fa: "\f263"; -} - -.fa-square-odnoklassniki { - --fa: "\f264"; -} - -.fa-odnoklassniki-square { - --fa: "\f264"; -} - -.fa-get-pocket { - --fa: "\f265"; -} - -.fa-wikipedia-w { - --fa: "\f266"; -} - -.fa-safari { - --fa: "\f267"; -} - -.fa-chrome { - --fa: "\f268"; -} - -.fa-firefox { - --fa: "\f269"; -} - -.fa-opera { - --fa: "\f26a"; -} - -.fa-internet-explorer { - --fa: "\f26b"; -} - -.fa-contao { - --fa: "\f26d"; -} - -.fa-500px { - --fa: "\f26e"; -} - -.fa-amazon { - --fa: "\f270"; -} - -.fa-houzz { - --fa: "\f27c"; -} - -.fa-vimeo-v { - --fa: "\f27d"; -} - -.fa-black-tie { - --fa: "\f27e"; -} - -.fa-fonticons { - --fa: "\f280"; -} - -.fa-reddit-alien { - --fa: "\f281"; -} - -.fa-edge { - --fa: "\f282"; -} - -.fa-codiepie { - --fa: "\f284"; -} - -.fa-modx { - --fa: "\f285"; -} - -.fa-fort-awesome { - --fa: "\f286"; -} - -.fa-usb { - --fa: "\f287"; -} - -.fa-product-hunt { - --fa: "\f288"; -} - -.fa-mixcloud { - --fa: "\f289"; -} - -.fa-scribd { - --fa: "\f28a"; -} - -.fa-bluetooth { - --fa: "\f293"; -} - -.fa-bluetooth-b { - --fa: "\f294"; -} - -.fa-gitlab { - --fa: "\f296"; -} - -.fa-wpbeginner { - --fa: "\f297"; -} - -.fa-wpforms { - --fa: "\f298"; -} - -.fa-envira { - --fa: "\f299"; -} - -.fa-glide { - --fa: "\f2a5"; -} - -.fa-glide-g { - --fa: "\f2a6"; -} - -.fa-viadeo { - --fa: "\f2a9"; -} - -.fa-square-viadeo { - --fa: "\f2aa"; -} - -.fa-viadeo-square { - --fa: "\f2aa"; -} - -.fa-snapchat { - --fa: "\f2ab"; -} - -.fa-snapchat-ghost { - --fa: "\f2ab"; -} - -.fa-square-snapchat { - --fa: "\f2ad"; -} - -.fa-snapchat-square { - --fa: "\f2ad"; -} - -.fa-pied-piper { - --fa: "\f2ae"; -} - -.fa-first-order { - --fa: "\f2b0"; -} - -.fa-yoast { - --fa: "\f2b1"; -} - -.fa-themeisle { - --fa: "\f2b2"; -} - -.fa-google-plus { - --fa: "\f2b3"; -} - -.fa-font-awesome { - --fa: "\f2b4"; -} - -.fa-font-awesome-flag { - --fa: "\f2b4"; -} - -.fa-font-awesome-logo-full { - --fa: "\f2b4"; -} - -.fa-linode { - --fa: "\f2b8"; -} - -.fa-quora { - --fa: "\f2c4"; -} - -.fa-free-code-camp { - --fa: "\f2c5"; -} - -.fa-telegram { - --fa: "\f2c6"; -} - -.fa-telegram-plane { - --fa: "\f2c6"; -} - -.fa-bandcamp { - --fa: "\f2d5"; -} - -.fa-grav { - --fa: "\f2d6"; -} - -.fa-etsy { - --fa: "\f2d7"; -} - -.fa-imdb { - --fa: "\f2d8"; -} - -.fa-ravelry { - --fa: "\f2d9"; -} - -.fa-sellcast { - --fa: "\f2da"; -} - -.fa-superpowers { - --fa: "\f2dd"; -} - -.fa-wpexplorer { - --fa: "\f2de"; -} - -.fa-meetup { - --fa: "\f2e0"; -} - -.fa-square-font-awesome-stroke { - --fa: "\f35c"; -} - -.fa-font-awesome-alt { - --fa: "\f35c"; -} - -.fa-accessible-icon { - --fa: "\f368"; -} - -.fa-accusoft { - --fa: "\f369"; -} - -.fa-adversal { - --fa: "\f36a"; -} - -.fa-affiliatetheme { - --fa: "\f36b"; -} - -.fa-algolia { - --fa: "\f36c"; -} - -.fa-amilia { - --fa: "\f36d"; -} - -.fa-angrycreative { - --fa: "\f36e"; -} - -.fa-app-store { - --fa: "\f36f"; -} - -.fa-app-store-ios { - --fa: "\f370"; -} - -.fa-apper { - --fa: "\f371"; -} - -.fa-asymmetrik { - --fa: "\f372"; -} - -.fa-audible { - --fa: "\f373"; -} - -.fa-avianex { - --fa: "\f374"; -} - -.fa-aws { - --fa: "\f375"; -} - -.fa-bimobject { - --fa: "\f378"; -} - -.fa-bitcoin { - --fa: "\f379"; -} - -.fa-bity { - --fa: "\f37a"; -} - -.fa-blackberry { - --fa: "\f37b"; -} - -.fa-blogger { - --fa: "\f37c"; -} - -.fa-blogger-b { - --fa: "\f37d"; -} - -.fa-buromobelexperte { - --fa: "\f37f"; -} - -.fa-centercode { - --fa: "\f380"; -} - -.fa-cloudscale { - --fa: "\f383"; -} - -.fa-cloudsmith { - --fa: "\f384"; -} - -.fa-cloudversify { - --fa: "\f385"; -} - -.fa-cpanel { - --fa: "\f388"; -} - -.fa-css3-alt { - --fa: "\f38b"; -} - -.fa-cuttlefish { - --fa: "\f38c"; -} - -.fa-d-and-d { - --fa: "\f38d"; -} - -.fa-deploydog { - --fa: "\f38e"; -} - -.fa-deskpro { - --fa: "\f38f"; -} - -.fa-digital-ocean { - --fa: "\f391"; -} - -.fa-discord { - --fa: "\f392"; -} - -.fa-discourse { - --fa: "\f393"; -} - -.fa-dochub { - --fa: "\f394"; -} - -.fa-docker { - --fa: "\f395"; -} - -.fa-draft2digital { - --fa: "\f396"; -} - -.fa-square-dribbble { - --fa: "\f397"; -} - -.fa-dribbble-square { - --fa: "\f397"; -} - -.fa-dyalog { - --fa: "\f399"; -} - -.fa-earlybirds { - --fa: "\f39a"; -} - -.fa-erlang { - --fa: "\f39d"; -} - -.fa-facebook-f { - --fa: "\f39e"; -} - -.fa-facebook-messenger { - --fa: "\f39f"; -} - -.fa-firstdraft { - --fa: "\f3a1"; -} - -.fa-fonticons-fi { - --fa: "\f3a2"; -} - -.fa-fort-awesome-alt { - --fa: "\f3a3"; -} - -.fa-freebsd { - --fa: "\f3a4"; -} - -.fa-gitkraken { - --fa: "\f3a6"; -} - -.fa-gofore { - --fa: "\f3a7"; -} - -.fa-goodreads { - --fa: "\f3a8"; -} - -.fa-goodreads-g { - --fa: "\f3a9"; -} - -.fa-google-drive { - --fa: "\f3aa"; -} - -.fa-google-play { - --fa: "\f3ab"; -} - -.fa-gripfire { - --fa: "\f3ac"; -} - -.fa-grunt { - --fa: "\f3ad"; -} - -.fa-gulp { - --fa: "\f3ae"; -} - -.fa-square-hacker-news { - --fa: "\f3af"; -} - -.fa-hacker-news-square { - --fa: "\f3af"; -} - -.fa-hire-a-helper { - --fa: "\f3b0"; -} - -.fa-hotjar { - --fa: "\f3b1"; -} - -.fa-hubspot { - --fa: "\f3b2"; -} - -.fa-itunes { - --fa: "\f3b4"; -} - -.fa-itunes-note { - --fa: "\f3b5"; -} - -.fa-jenkins { - --fa: "\f3b6"; -} - -.fa-joget { - --fa: "\f3b7"; -} - -.fa-js { - --fa: "\f3b8"; -} - -.fa-square-js { - --fa: "\f3b9"; -} - -.fa-js-square { - --fa: "\f3b9"; -} - -.fa-keycdn { - --fa: "\f3ba"; -} - -.fa-kickstarter { - --fa: "\f3bb"; -} - -.fa-square-kickstarter { - --fa: "\f3bb"; -} - -.fa-kickstarter-k { - --fa: "\f3bc"; -} - -.fa-laravel { - --fa: "\f3bd"; -} - -.fa-line { - --fa: "\f3c0"; -} - -.fa-lyft { - --fa: "\f3c3"; -} - -.fa-magento { - --fa: "\f3c4"; -} - -.fa-medapps { - --fa: "\f3c6"; -} - -.fa-medrt { - --fa: "\f3c8"; -} - -.fa-microsoft { - --fa: "\f3ca"; -} - -.fa-mix { - --fa: "\f3cb"; -} - -.fa-mizuni { - --fa: "\f3cc"; -} - -.fa-monero { - --fa: "\f3d0"; -} - -.fa-napster { - --fa: "\f3d2"; -} - -.fa-node-js { - --fa: "\f3d3"; -} - -.fa-npm { - --fa: "\f3d4"; -} - -.fa-ns8 { - --fa: "\f3d5"; -} - -.fa-nutritionix { - --fa: "\f3d6"; -} - -.fa-page4 { - --fa: "\f3d7"; -} - -.fa-palfed { - --fa: "\f3d8"; -} - -.fa-patreon { - --fa: "\f3d9"; -} - -.fa-periscope { - --fa: "\f3da"; -} - -.fa-phabricator { - --fa: "\f3db"; -} - -.fa-phoenix-framework { - --fa: "\f3dc"; -} - -.fa-playstation { - --fa: "\f3df"; -} - -.fa-pushed { - --fa: "\f3e1"; -} - -.fa-python { - --fa: "\f3e2"; -} - -.fa-red-river { - --fa: "\f3e3"; -} - -.fa-wpressr { - --fa: "\f3e4"; -} - -.fa-rendact { - --fa: "\f3e4"; -} - -.fa-replyd { - --fa: "\f3e6"; -} - -.fa-resolving { - --fa: "\f3e7"; -} - -.fa-rocketchat { - --fa: "\f3e8"; -} - -.fa-rockrms { - --fa: "\f3e9"; -} - -.fa-schlix { - --fa: "\f3ea"; -} - -.fa-searchengin { - --fa: "\f3eb"; -} - -.fa-servicestack { - --fa: "\f3ec"; -} - -.fa-sistrix { - --fa: "\f3ee"; -} - -.fa-speakap { - --fa: "\f3f3"; -} - -.fa-staylinked { - --fa: "\f3f5"; -} - -.fa-steam-symbol { - --fa: "\f3f6"; -} - -.fa-sticker-mule { - --fa: "\f3f7"; -} - -.fa-studiovinari { - --fa: "\f3f8"; -} - -.fa-supple { - --fa: "\f3f9"; -} - -.fa-uber { - --fa: "\f402"; -} - -.fa-uikit { - --fa: "\f403"; -} - -.fa-uniregistry { - --fa: "\f404"; -} - -.fa-untappd { - --fa: "\f405"; -} - -.fa-ussunnah { - --fa: "\f407"; -} - -.fa-vaadin { - --fa: "\f408"; -} - -.fa-viber { - --fa: "\f409"; -} - -.fa-vimeo { - --fa: "\f40a"; -} - -.fa-vnv { - --fa: "\f40b"; -} - -.fa-square-whatsapp { - --fa: "\f40c"; -} - -.fa-whatsapp-square { - --fa: "\f40c"; -} - -.fa-whmcs { - --fa: "\f40d"; -} - -.fa-wordpress-simple { - --fa: "\f411"; -} - -.fa-xbox { - --fa: "\f412"; -} - -.fa-yandex { - --fa: "\f413"; -} - -.fa-yandex-international { - --fa: "\f414"; -} - -.fa-apple-pay { - --fa: "\f415"; -} - -.fa-cc-apple-pay { - --fa: "\f416"; -} - -.fa-fly { - --fa: "\f417"; -} - -.fa-node { - --fa: "\f419"; -} - -.fa-osi { - --fa: "\f41a"; -} - -.fa-react { - --fa: "\f41b"; -} - -.fa-autoprefixer { - --fa: "\f41c"; -} - -.fa-less { - --fa: "\f41d"; -} - -.fa-sass { - --fa: "\f41e"; -} - -.fa-vuejs { - --fa: "\f41f"; -} - -.fa-angular { - --fa: "\f420"; -} - -.fa-aviato { - --fa: "\f421"; -} - -.fa-ember { - --fa: "\f423"; -} - -.fa-gitter { - --fa: "\f426"; -} - -.fa-hooli { - --fa: "\f427"; -} - -.fa-strava { - --fa: "\f428"; -} - -.fa-stripe { - --fa: "\f429"; -} - -.fa-stripe-s { - --fa: "\f42a"; -} - -.fa-typo3 { - --fa: "\f42b"; -} - -.fa-amazon-pay { - --fa: "\f42c"; -} - -.fa-cc-amazon-pay { - --fa: "\f42d"; -} - -.fa-ethereum { - --fa: "\f42e"; -} - -.fa-korvue { - --fa: "\f42f"; -} - -.fa-elementor { - --fa: "\f430"; -} - -.fa-square-youtube { - --fa: "\f431"; -} - -.fa-youtube-square { - --fa: "\f431"; -} - -.fa-flipboard { - --fa: "\f44d"; -} - -.fa-hips { - --fa: "\f452"; -} - -.fa-php { - --fa: "\f457"; -} - -.fa-quinscape { - --fa: "\f459"; -} - -.fa-readme { - --fa: "\f4d5"; -} - -.fa-java { - --fa: "\f4e4"; -} - -.fa-pied-piper-hat { - --fa: "\f4e5"; -} - -.fa-creative-commons-by { - --fa: "\f4e7"; -} - -.fa-creative-commons-nc { - --fa: "\f4e8"; -} - -.fa-creative-commons-nc-eu { - --fa: "\f4e9"; -} - -.fa-creative-commons-nc-jp { - --fa: "\f4ea"; -} - -.fa-creative-commons-nd { - --fa: "\f4eb"; -} - -.fa-creative-commons-pd { - --fa: "\f4ec"; -} - -.fa-creative-commons-pd-alt { - --fa: "\f4ed"; -} - -.fa-creative-commons-remix { - --fa: "\f4ee"; -} - -.fa-creative-commons-sa { - --fa: "\f4ef"; -} - -.fa-creative-commons-sampling { - --fa: "\f4f0"; -} - -.fa-creative-commons-sampling-plus { - --fa: "\f4f1"; -} - -.fa-creative-commons-share { - --fa: "\f4f2"; -} - -.fa-creative-commons-zero { - --fa: "\f4f3"; -} - -.fa-ebay { - --fa: "\f4f4"; -} - -.fa-keybase { - --fa: "\f4f5"; -} - -.fa-mastodon { - --fa: "\f4f6"; -} - -.fa-r-project { - --fa: "\f4f7"; -} - -.fa-researchgate { - --fa: "\f4f8"; -} - -.fa-teamspeak { - --fa: "\f4f9"; -} - -.fa-first-order-alt { - --fa: "\f50a"; -} - -.fa-fulcrum { - --fa: "\f50b"; -} - -.fa-galactic-republic { - --fa: "\f50c"; -} - -.fa-galactic-senate { - --fa: "\f50d"; -} - -.fa-jedi-order { - --fa: "\f50e"; -} - -.fa-mandalorian { - --fa: "\f50f"; -} - -.fa-old-republic { - --fa: "\f510"; -} - -.fa-phoenix-squadron { - --fa: "\f511"; -} - -.fa-sith { - --fa: "\f512"; -} - -.fa-trade-federation { - --fa: "\f513"; -} - -.fa-wolf-pack-battalion { - --fa: "\f514"; -} - -.fa-hornbill { - --fa: "\f592"; -} - -.fa-mailchimp { - --fa: "\f59e"; -} - -.fa-megaport { - --fa: "\f5a3"; -} - -.fa-nimblr { - --fa: "\f5a8"; -} - -.fa-rev { - --fa: "\f5b2"; -} - -.fa-shopware { - --fa: "\f5b5"; -} - -.fa-squarespace { - --fa: "\f5be"; -} - -.fa-themeco { - --fa: "\f5c6"; -} - -.fa-weebly { - --fa: "\f5cc"; -} - -.fa-wix { - --fa: "\f5cf"; -} - -.fa-ello { - --fa: "\f5f1"; -} - -.fa-hackerrank { - --fa: "\f5f7"; -} - -.fa-kaggle { - --fa: "\f5fa"; -} - -.fa-markdown { - --fa: "\f60f"; -} - -.fa-neos { - --fa: "\f612"; -} - -.fa-zhihu { - --fa: "\f63f"; -} - -.fa-alipay { - --fa: "\f642"; -} - -.fa-the-red-yeti { - --fa: "\f69d"; -} - -.fa-critical-role { - --fa: "\f6c9"; -} - -.fa-d-and-d-beyond { - --fa: "\f6ca"; -} - -.fa-dev { - --fa: "\f6cc"; -} - -.fa-fantasy-flight-games { - --fa: "\f6dc"; -} - -.fa-wizards-of-the-coast { - --fa: "\f730"; -} - -.fa-think-peaks { - --fa: "\f731"; -} - -.fa-reacteurope { - --fa: "\f75d"; -} - -.fa-artstation { - --fa: "\f77a"; -} - -.fa-atlassian { - --fa: "\f77b"; -} - -.fa-canadian-maple-leaf { - --fa: "\f785"; -} - -.fa-centos { - --fa: "\f789"; -} - -.fa-confluence { - --fa: "\f78d"; -} - -.fa-dhl { - --fa: "\f790"; -} - -.fa-diaspora { - --fa: "\f791"; -} - -.fa-fedex { - --fa: "\f797"; -} - -.fa-fedora { - --fa: "\f798"; -} - -.fa-figma { - --fa: "\f799"; -} - -.fa-intercom { - --fa: "\f7af"; -} - -.fa-invision { - --fa: "\f7b0"; -} - -.fa-jira { - --fa: "\f7b1"; -} - -.fa-mendeley { - --fa: "\f7b3"; -} - -.fa-raspberry-pi { - --fa: "\f7bb"; -} - -.fa-redhat { - --fa: "\f7bc"; -} - -.fa-sketch { - --fa: "\f7c6"; -} - -.fa-sourcetree { - --fa: "\f7d3"; -} - -.fa-suse { - --fa: "\f7d6"; -} - -.fa-ubuntu { - --fa: "\f7df"; -} - -.fa-ups { - --fa: "\f7e0"; -} - -.fa-usps { - --fa: "\f7e1"; -} - -.fa-yarn { - --fa: "\f7e3"; -} - -.fa-airbnb { - --fa: "\f834"; -} - -.fa-battle-net { - --fa: "\f835"; -} - -.fa-bootstrap { - --fa: "\f836"; -} - -.fa-buffer { - --fa: "\f837"; -} - -.fa-chromecast { - --fa: "\f838"; -} - -.fa-evernote { - --fa: "\f839"; -} - -.fa-itch-io { - --fa: "\f83a"; -} - -.fa-salesforce { - --fa: "\f83b"; -} - -.fa-speaker-deck { - --fa: "\f83c"; -} - -.fa-symfony { - --fa: "\f83d"; -} - -.fa-waze { - --fa: "\f83f"; -} - -.fa-yammer { - --fa: "\f840"; -} - -.fa-git-alt { - --fa: "\f841"; -} - -.fa-stackpath { - --fa: "\f842"; -} - -.fa-cotton-bureau { - --fa: "\f89e"; -} - -.fa-buy-n-large { - --fa: "\f8a6"; -} - -.fa-mdb { - --fa: "\f8ca"; -} - -.fa-orcid { - --fa: "\f8d2"; -} - -.fa-swift { - --fa: "\f8e1"; -} - -.fa-umbraco { - --fa: "\f8e8"; -}:root, :host { - --fa-family-classic: "Font Awesome 7 Free"; - --fa-font-regular: normal 400 1em/1 var(--fa-family-classic); - /* deprecated: this older custom property will be removed next major release */ - --fa-style-family-classic: var(--fa-family-classic); -} - -@font-face { - font-family: "Font Awesome 7 Free"; - font-style: normal; - font-weight: 400; - font-display: block; - src: url("../webfonts/fa-regular-400.woff2"); -} -.far { - --fa-family: var(--fa-family-classic); - --fa-style: 400; -} - -.fa-classic { - --fa-family: var(--fa-family-classic); -} - -.fa-regular { - --fa-style: 400; -}:root, :host { - --fa-family-classic: "Font Awesome 7 Free"; - --fa-font-solid: normal 900 1em/1 var(--fa-family-classic); - /* deprecated: this older custom property will be removed next major release */ - --fa-style-family-classic: var(--fa-family-classic); -} - -@font-face { - font-family: "Font Awesome 7 Free"; - font-style: normal; - font-weight: 900; - font-display: block; - src: url("../webfonts/fa-solid-900.woff2"); -} -.fas { - --fa-family: var(--fa-family-classic); - --fa-style: 900; -} - -.fa-classic { - --fa-family: var(--fa-family-classic); -} - -.fa-solid { - --fa-style: 900; -}@font-face { - font-family: "Font Awesome 5 Brands"; - font-display: block; - font-weight: 400; - src: url("../webfonts/fa-brands-400.woff2") format("woff2"); -} -@font-face { - font-family: "Font Awesome 5 Free"; - font-display: block; - font-weight: 900; - src: url("../webfonts/fa-solid-900.woff2") format("woff2"); -} -@font-face { - font-family: "Font Awesome 5 Free"; - font-display: block; - font-weight: 400; - src: url("../webfonts/fa-regular-400.woff2") format("woff2"); -}@font-face { - font-family: "FontAwesome"; - font-display: block; - src: url("../webfonts/fa-solid-900.woff2") format("woff2"); -} -@font-face { - font-family: "FontAwesome"; - font-display: block; - src: url("../webfonts/fa-brands-400.woff2") format("woff2"); -} -@font-face { - font-family: "FontAwesome"; - font-display: block; - src: url("../webfonts/fa-regular-400.woff2") format("woff2"); - unicode-range: U+F003, U+F006, U+F014, U+F016-F017, U+F01A-F01B, U+F01D, U+F022, U+F03E, U+F044, U+F046, U+F05C-F05D, U+F06E, U+F070, U+F087-F088, U+F08A, U+F094, U+F096-F097, U+F09D, U+F0A0, U+F0A2, U+F0A4-F0A7, U+F0C5, U+F0C7, U+F0E5-F0E6, U+F0EB, U+F0F6-F0F8, U+F10C, U+F114-F115, U+F118-F11A, U+F11C-F11D, U+F133, U+F147, U+F14E, U+F150-F152, U+F185-F186, U+F18E, U+F190-F192, U+F196, U+F1C1-F1C9, U+F1D9, U+F1DB, U+F1E3, U+F1EA, U+F1F7, U+F1F9, U+F20A, U+F247-F248, U+F24A, U+F24D, U+F255-F25B, U+F25D, U+F271-F274, U+F278, U+F27B, U+F28C, U+F28E, U+F29C, U+F2B5, U+F2B7, U+F2BA, U+F2BC, U+F2BE, U+F2C0-F2C1, U+F2C3, U+F2D0, U+F2D2, U+F2D4, U+F2DC; -} -@font-face { - font-family: "FontAwesome"; - font-display: block; - src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"); - unicode-range: U+F041, U+F047, U+F065-F066, U+F07D-F07E, U+F080, U+F08B, U+F08E, U+F090, U+F09A, U+F0AC, U+F0AE, U+F0B2, U+F0D0, U+F0D6, U+F0E4, U+F0EC, U+F10A-F10B, U+F123, U+F13E, U+F148-F149, U+F14C, U+F156, U+F15E, U+F160-F161, U+F163, U+F175-F178, U+F195, U+F1F8, U+F219, U+F27A; -} \ No newline at end of file diff --git a/src/media/vendor/fa7free/css/brands.css b/src/media/vendor/fa7free/css/brands.css deleted file mode 100644 index 389e32b..0000000 --- a/src/media/vendor/fa7free/css/brands.css +++ /dev/null @@ -1,2219 +0,0 @@ -/*! - * Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2025 Fonticons, Inc. - */ -:root, :host { - --fa-family-brands: "Font Awesome 7 Brands"; - --fa-font-brands: normal 400 1em/1 var(--fa-family-brands); -} - -@font-face { - font-family: "Font Awesome 7 Brands"; - font-style: normal; - font-weight: 400; - font-display: block; - src: url("../webfonts/fa-brands-400.woff2"); -} -.fab, -.fa-brands, -.fa-classic.fa-brands { - --fa-family: var(--fa-family-brands); - --fa-style: 400; -} - -.fa-firefox-browser { - --fa: "\e007"; -} - -.fa-ideal { - --fa: "\e013"; -} - -.fa-microblog { - --fa: "\e01a"; -} - -.fa-square-pied-piper { - --fa: "\e01e"; -} - -.fa-pied-piper-square { - --fa: "\e01e"; -} - -.fa-unity { - --fa: "\e049"; -} - -.fa-dailymotion { - --fa: "\e052"; -} - -.fa-square-instagram { - --fa: "\e055"; -} - -.fa-instagram-square { - --fa: "\e055"; -} - -.fa-mixer { - --fa: "\e056"; -} - -.fa-shopify { - --fa: "\e057"; -} - -.fa-deezer { - --fa: "\e077"; -} - -.fa-edge-legacy { - --fa: "\e078"; -} - -.fa-google-pay { - --fa: "\e079"; -} - -.fa-rust { - --fa: "\e07a"; -} - -.fa-tiktok { - --fa: "\e07b"; -} - -.fa-unsplash { - --fa: "\e07c"; -} - -.fa-cloudflare { - --fa: "\e07d"; -} - -.fa-guilded { - --fa: "\e07e"; -} - -.fa-hive { - --fa: "\e07f"; -} - -.fa-42-group { - --fa: "\e080"; -} - -.fa-innosoft { - --fa: "\e080"; -} - -.fa-instalod { - --fa: "\e081"; -} - -.fa-octopus-deploy { - --fa: "\e082"; -} - -.fa-perbyte { - --fa: "\e083"; -} - -.fa-uncharted { - --fa: "\e084"; -} - -.fa-watchman-monitoring { - --fa: "\e087"; -} - -.fa-wodu { - --fa: "\e088"; -} - -.fa-wirsindhandwerk { - --fa: "\e2d0"; -} - -.fa-wsh { - --fa: "\e2d0"; -} - -.fa-bots { - --fa: "\e340"; -} - -.fa-cmplid { - --fa: "\e360"; -} - -.fa-bilibili { - --fa: "\e3d9"; -} - -.fa-golang { - --fa: "\e40f"; -} - -.fa-pix { - --fa: "\e43a"; -} - -.fa-sitrox { - --fa: "\e44a"; -} - -.fa-hashnode { - --fa: "\e499"; -} - -.fa-meta { - --fa: "\e49b"; -} - -.fa-padlet { - --fa: "\e4a0"; -} - -.fa-nfc-directional { - --fa: "\e530"; -} - -.fa-nfc-symbol { - --fa: "\e531"; -} - -.fa-screenpal { - --fa: "\e570"; -} - -.fa-space-awesome { - --fa: "\e5ac"; -} - -.fa-square-font-awesome { - --fa: "\e5ad"; -} - -.fa-square-gitlab { - --fa: "\e5ae"; -} - -.fa-gitlab-square { - --fa: "\e5ae"; -} - -.fa-odysee { - --fa: "\e5c6"; -} - -.fa-stubber { - --fa: "\e5c7"; -} - -.fa-debian { - --fa: "\e60b"; -} - -.fa-shoelace { - --fa: "\e60c"; -} - -.fa-threads { - --fa: "\e618"; -} - -.fa-square-threads { - --fa: "\e619"; -} - -.fa-square-x-twitter { - --fa: "\e61a"; -} - -.fa-x-twitter { - --fa: "\e61b"; -} - -.fa-opensuse { - --fa: "\e62b"; -} - -.fa-letterboxd { - --fa: "\e62d"; -} - -.fa-square-letterboxd { - --fa: "\e62e"; -} - -.fa-mintbit { - --fa: "\e62f"; -} - -.fa-google-scholar { - --fa: "\e63b"; -} - -.fa-brave { - --fa: "\e63c"; -} - -.fa-brave-reverse { - --fa: "\e63d"; -} - -.fa-pixiv { - --fa: "\e640"; -} - -.fa-upwork { - --fa: "\e641"; -} - -.fa-webflow { - --fa: "\e65c"; -} - -.fa-signal-messenger { - --fa: "\e663"; -} - -.fa-bluesky { - --fa: "\e671"; -} - -.fa-jxl { - --fa: "\e67b"; -} - -.fa-square-upwork { - --fa: "\e67c"; -} - -.fa-web-awesome { - --fa: "\e682"; -} - -.fa-square-web-awesome { - --fa: "\e683"; -} - -.fa-square-web-awesome-stroke { - --fa: "\e684"; -} - -.fa-dart-lang { - --fa: "\e693"; -} - -.fa-flutter { - --fa: "\e694"; -} - -.fa-files-pinwheel { - --fa: "\e69f"; -} - -.fa-css { - --fa: "\e6a2"; -} - -.fa-square-bluesky { - --fa: "\e6a3"; -} - -.fa-openai { - --fa: "\e7cf"; -} - -.fa-square-linkedin { - --fa: "\e7d0"; -} - -.fa-cash-app { - --fa: "\e7d4"; -} - -.fa-disqus { - --fa: "\e7d5"; -} - -.fa-eleventy { - --fa: "\e7d6"; -} - -.fa-11ty { - --fa: "\e7d6"; -} - -.fa-kakao-talk { - --fa: "\e7d7"; -} - -.fa-linktree { - --fa: "\e7d8"; -} - -.fa-notion { - --fa: "\e7d9"; -} - -.fa-pandora { - --fa: "\e7da"; -} - -.fa-pixelfed { - --fa: "\e7db"; -} - -.fa-tidal { - --fa: "\e7dc"; -} - -.fa-vsco { - --fa: "\e7dd"; -} - -.fa-w3c { - --fa: "\e7de"; -} - -.fa-lumon { - --fa: "\e7e2"; -} - -.fa-lumon-drop { - --fa: "\e7e3"; -} - -.fa-square-figma { - --fa: "\e7e4"; -} - -.fa-tex { - --fa: "\e7ff"; -} - -.fa-duolingo { - --fa: "\e812"; -} - -.fa-square-twitter { - --fa: "\f081"; -} - -.fa-twitter-square { - --fa: "\f081"; -} - -.fa-square-facebook { - --fa: "\f082"; -} - -.fa-facebook-square { - --fa: "\f082"; -} - -.fa-linkedin { - --fa: "\f08c"; -} - -.fa-square-github { - --fa: "\f092"; -} - -.fa-github-square { - --fa: "\f092"; -} - -.fa-twitter { - --fa: "\f099"; -} - -.fa-facebook { - --fa: "\f09a"; -} - -.fa-github { - --fa: "\f09b"; -} - -.fa-pinterest { - --fa: "\f0d2"; -} - -.fa-square-pinterest { - --fa: "\f0d3"; -} - -.fa-pinterest-square { - --fa: "\f0d3"; -} - -.fa-square-google-plus { - --fa: "\f0d4"; -} - -.fa-google-plus-square { - --fa: "\f0d4"; -} - -.fa-google-plus-g { - --fa: "\f0d5"; -} - -.fa-linkedin-in { - --fa: "\f0e1"; -} - -.fa-github-alt { - --fa: "\f113"; -} - -.fa-maxcdn { - --fa: "\f136"; -} - -.fa-html5 { - --fa: "\f13b"; -} - -.fa-css3 { - --fa: "\f13c"; -} - -.fa-btc { - --fa: "\f15a"; -} - -.fa-youtube { - --fa: "\f167"; -} - -.fa-xing { - --fa: "\f168"; -} - -.fa-square-xing { - --fa: "\f169"; -} - -.fa-xing-square { - --fa: "\f169"; -} - -.fa-dropbox { - --fa: "\f16b"; -} - -.fa-stack-overflow { - --fa: "\f16c"; -} - -.fa-instagram { - --fa: "\f16d"; -} - -.fa-flickr { - --fa: "\f16e"; -} - -.fa-adn { - --fa: "\f170"; -} - -.fa-bitbucket { - --fa: "\f171"; -} - -.fa-tumblr { - --fa: "\f173"; -} - -.fa-square-tumblr { - --fa: "\f174"; -} - -.fa-tumblr-square { - --fa: "\f174"; -} - -.fa-apple { - --fa: "\f179"; -} - -.fa-windows { - --fa: "\f17a"; -} - -.fa-android { - --fa: "\f17b"; -} - -.fa-linux { - --fa: "\f17c"; -} - -.fa-dribbble { - --fa: "\f17d"; -} - -.fa-skype { - --fa: "\f17e"; -} - -.fa-foursquare { - --fa: "\f180"; -} - -.fa-trello { - --fa: "\f181"; -} - -.fa-gratipay { - --fa: "\f184"; -} - -.fa-vk { - --fa: "\f189"; -} - -.fa-weibo { - --fa: "\f18a"; -} - -.fa-renren { - --fa: "\f18b"; -} - -.fa-pagelines { - --fa: "\f18c"; -} - -.fa-stack-exchange { - --fa: "\f18d"; -} - -.fa-square-vimeo { - --fa: "\f194"; -} - -.fa-vimeo-square { - --fa: "\f194"; -} - -.fa-slack { - --fa: "\f198"; -} - -.fa-slack-hash { - --fa: "\f198"; -} - -.fa-wordpress { - --fa: "\f19a"; -} - -.fa-openid { - --fa: "\f19b"; -} - -.fa-yahoo { - --fa: "\f19e"; -} - -.fa-google { - --fa: "\f1a0"; -} - -.fa-reddit { - --fa: "\f1a1"; -} - -.fa-square-reddit { - --fa: "\f1a2"; -} - -.fa-reddit-square { - --fa: "\f1a2"; -} - -.fa-stumbleupon-circle { - --fa: "\f1a3"; -} - -.fa-stumbleupon { - --fa: "\f1a4"; -} - -.fa-delicious { - --fa: "\f1a5"; -} - -.fa-digg { - --fa: "\f1a6"; -} - -.fa-pied-piper-pp { - --fa: "\f1a7"; -} - -.fa-pied-piper-alt { - --fa: "\f1a8"; -} - -.fa-drupal { - --fa: "\f1a9"; -} - -.fa-joomla { - --fa: "\f1aa"; -} - -.fa-behance { - --fa: "\f1b4"; -} - -.fa-square-behance { - --fa: "\f1b5"; -} - -.fa-behance-square { - --fa: "\f1b5"; -} - -.fa-steam { - --fa: "\f1b6"; -} - -.fa-square-steam { - --fa: "\f1b7"; -} - -.fa-steam-square { - --fa: "\f1b7"; -} - -.fa-spotify { - --fa: "\f1bc"; -} - -.fa-deviantart { - --fa: "\f1bd"; -} - -.fa-soundcloud { - --fa: "\f1be"; -} - -.fa-vine { - --fa: "\f1ca"; -} - -.fa-codepen { - --fa: "\f1cb"; -} - -.fa-jsfiddle { - --fa: "\f1cc"; -} - -.fa-rebel { - --fa: "\f1d0"; -} - -.fa-empire { - --fa: "\f1d1"; -} - -.fa-square-git { - --fa: "\f1d2"; -} - -.fa-git-square { - --fa: "\f1d2"; -} - -.fa-git { - --fa: "\f1d3"; -} - -.fa-hacker-news { - --fa: "\f1d4"; -} - -.fa-tencent-weibo { - --fa: "\f1d5"; -} - -.fa-qq { - --fa: "\f1d6"; -} - -.fa-weixin { - --fa: "\f1d7"; -} - -.fa-slideshare { - --fa: "\f1e7"; -} - -.fa-twitch { - --fa: "\f1e8"; -} - -.fa-yelp { - --fa: "\f1e9"; -} - -.fa-paypal { - --fa: "\f1ed"; -} - -.fa-google-wallet { - --fa: "\f1ee"; -} - -.fa-cc-visa { - --fa: "\f1f0"; -} - -.fa-cc-mastercard { - --fa: "\f1f1"; -} - -.fa-cc-discover { - --fa: "\f1f2"; -} - -.fa-cc-amex { - --fa: "\f1f3"; -} - -.fa-cc-paypal { - --fa: "\f1f4"; -} - -.fa-cc-stripe { - --fa: "\f1f5"; -} - -.fa-lastfm { - --fa: "\f202"; -} - -.fa-square-lastfm { - --fa: "\f203"; -} - -.fa-lastfm-square { - --fa: "\f203"; -} - -.fa-ioxhost { - --fa: "\f208"; -} - -.fa-angellist { - --fa: "\f209"; -} - -.fa-buysellads { - --fa: "\f20d"; -} - -.fa-connectdevelop { - --fa: "\f20e"; -} - -.fa-dashcube { - --fa: "\f210"; -} - -.fa-forumbee { - --fa: "\f211"; -} - -.fa-leanpub { - --fa: "\f212"; -} - -.fa-sellsy { - --fa: "\f213"; -} - -.fa-shirtsinbulk { - --fa: "\f214"; -} - -.fa-simplybuilt { - --fa: "\f215"; -} - -.fa-skyatlas { - --fa: "\f216"; -} - -.fa-pinterest-p { - --fa: "\f231"; -} - -.fa-whatsapp { - --fa: "\f232"; -} - -.fa-viacoin { - --fa: "\f237"; -} - -.fa-medium { - --fa: "\f23a"; -} - -.fa-medium-m { - --fa: "\f23a"; -} - -.fa-y-combinator { - --fa: "\f23b"; -} - -.fa-optin-monster { - --fa: "\f23c"; -} - -.fa-opencart { - --fa: "\f23d"; -} - -.fa-expeditedssl { - --fa: "\f23e"; -} - -.fa-cc-jcb { - --fa: "\f24b"; -} - -.fa-cc-diners-club { - --fa: "\f24c"; -} - -.fa-creative-commons { - --fa: "\f25e"; -} - -.fa-gg { - --fa: "\f260"; -} - -.fa-gg-circle { - --fa: "\f261"; -} - -.fa-odnoklassniki { - --fa: "\f263"; -} - -.fa-square-odnoklassniki { - --fa: "\f264"; -} - -.fa-odnoklassniki-square { - --fa: "\f264"; -} - -.fa-get-pocket { - --fa: "\f265"; -} - -.fa-wikipedia-w { - --fa: "\f266"; -} - -.fa-safari { - --fa: "\f267"; -} - -.fa-chrome { - --fa: "\f268"; -} - -.fa-firefox { - --fa: "\f269"; -} - -.fa-opera { - --fa: "\f26a"; -} - -.fa-internet-explorer { - --fa: "\f26b"; -} - -.fa-contao { - --fa: "\f26d"; -} - -.fa-500px { - --fa: "\f26e"; -} - -.fa-amazon { - --fa: "\f270"; -} - -.fa-houzz { - --fa: "\f27c"; -} - -.fa-vimeo-v { - --fa: "\f27d"; -} - -.fa-black-tie { - --fa: "\f27e"; -} - -.fa-fonticons { - --fa: "\f280"; -} - -.fa-reddit-alien { - --fa: "\f281"; -} - -.fa-edge { - --fa: "\f282"; -} - -.fa-codiepie { - --fa: "\f284"; -} - -.fa-modx { - --fa: "\f285"; -} - -.fa-fort-awesome { - --fa: "\f286"; -} - -.fa-usb { - --fa: "\f287"; -} - -.fa-product-hunt { - --fa: "\f288"; -} - -.fa-mixcloud { - --fa: "\f289"; -} - -.fa-scribd { - --fa: "\f28a"; -} - -.fa-bluetooth { - --fa: "\f293"; -} - -.fa-bluetooth-b { - --fa: "\f294"; -} - -.fa-gitlab { - --fa: "\f296"; -} - -.fa-wpbeginner { - --fa: "\f297"; -} - -.fa-wpforms { - --fa: "\f298"; -} - -.fa-envira { - --fa: "\f299"; -} - -.fa-glide { - --fa: "\f2a5"; -} - -.fa-glide-g { - --fa: "\f2a6"; -} - -.fa-viadeo { - --fa: "\f2a9"; -} - -.fa-square-viadeo { - --fa: "\f2aa"; -} - -.fa-viadeo-square { - --fa: "\f2aa"; -} - -.fa-snapchat { - --fa: "\f2ab"; -} - -.fa-snapchat-ghost { - --fa: "\f2ab"; -} - -.fa-square-snapchat { - --fa: "\f2ad"; -} - -.fa-snapchat-square { - --fa: "\f2ad"; -} - -.fa-pied-piper { - --fa: "\f2ae"; -} - -.fa-first-order { - --fa: "\f2b0"; -} - -.fa-yoast { - --fa: "\f2b1"; -} - -.fa-themeisle { - --fa: "\f2b2"; -} - -.fa-google-plus { - --fa: "\f2b3"; -} - -.fa-font-awesome { - --fa: "\f2b4"; -} - -.fa-font-awesome-flag { - --fa: "\f2b4"; -} - -.fa-font-awesome-logo-full { - --fa: "\f2b4"; -} - -.fa-linode { - --fa: "\f2b8"; -} - -.fa-quora { - --fa: "\f2c4"; -} - -.fa-free-code-camp { - --fa: "\f2c5"; -} - -.fa-telegram { - --fa: "\f2c6"; -} - -.fa-telegram-plane { - --fa: "\f2c6"; -} - -.fa-bandcamp { - --fa: "\f2d5"; -} - -.fa-grav { - --fa: "\f2d6"; -} - -.fa-etsy { - --fa: "\f2d7"; -} - -.fa-imdb { - --fa: "\f2d8"; -} - -.fa-ravelry { - --fa: "\f2d9"; -} - -.fa-sellcast { - --fa: "\f2da"; -} - -.fa-superpowers { - --fa: "\f2dd"; -} - -.fa-wpexplorer { - --fa: "\f2de"; -} - -.fa-meetup { - --fa: "\f2e0"; -} - -.fa-square-font-awesome-stroke { - --fa: "\f35c"; -} - -.fa-font-awesome-alt { - --fa: "\f35c"; -} - -.fa-accessible-icon { - --fa: "\f368"; -} - -.fa-accusoft { - --fa: "\f369"; -} - -.fa-adversal { - --fa: "\f36a"; -} - -.fa-affiliatetheme { - --fa: "\f36b"; -} - -.fa-algolia { - --fa: "\f36c"; -} - -.fa-amilia { - --fa: "\f36d"; -} - -.fa-angrycreative { - --fa: "\f36e"; -} - -.fa-app-store { - --fa: "\f36f"; -} - -.fa-app-store-ios { - --fa: "\f370"; -} - -.fa-apper { - --fa: "\f371"; -} - -.fa-asymmetrik { - --fa: "\f372"; -} - -.fa-audible { - --fa: "\f373"; -} - -.fa-avianex { - --fa: "\f374"; -} - -.fa-aws { - --fa: "\f375"; -} - -.fa-bimobject { - --fa: "\f378"; -} - -.fa-bitcoin { - --fa: "\f379"; -} - -.fa-bity { - --fa: "\f37a"; -} - -.fa-blackberry { - --fa: "\f37b"; -} - -.fa-blogger { - --fa: "\f37c"; -} - -.fa-blogger-b { - --fa: "\f37d"; -} - -.fa-buromobelexperte { - --fa: "\f37f"; -} - -.fa-centercode { - --fa: "\f380"; -} - -.fa-cloudscale { - --fa: "\f383"; -} - -.fa-cloudsmith { - --fa: "\f384"; -} - -.fa-cloudversify { - --fa: "\f385"; -} - -.fa-cpanel { - --fa: "\f388"; -} - -.fa-css3-alt { - --fa: "\f38b"; -} - -.fa-cuttlefish { - --fa: "\f38c"; -} - -.fa-d-and-d { - --fa: "\f38d"; -} - -.fa-deploydog { - --fa: "\f38e"; -} - -.fa-deskpro { - --fa: "\f38f"; -} - -.fa-digital-ocean { - --fa: "\f391"; -} - -.fa-discord { - --fa: "\f392"; -} - -.fa-discourse { - --fa: "\f393"; -} - -.fa-dochub { - --fa: "\f394"; -} - -.fa-docker { - --fa: "\f395"; -} - -.fa-draft2digital { - --fa: "\f396"; -} - -.fa-square-dribbble { - --fa: "\f397"; -} - -.fa-dribbble-square { - --fa: "\f397"; -} - -.fa-dyalog { - --fa: "\f399"; -} - -.fa-earlybirds { - --fa: "\f39a"; -} - -.fa-erlang { - --fa: "\f39d"; -} - -.fa-facebook-f { - --fa: "\f39e"; -} - -.fa-facebook-messenger { - --fa: "\f39f"; -} - -.fa-firstdraft { - --fa: "\f3a1"; -} - -.fa-fonticons-fi { - --fa: "\f3a2"; -} - -.fa-fort-awesome-alt { - --fa: "\f3a3"; -} - -.fa-freebsd { - --fa: "\f3a4"; -} - -.fa-gitkraken { - --fa: "\f3a6"; -} - -.fa-gofore { - --fa: "\f3a7"; -} - -.fa-goodreads { - --fa: "\f3a8"; -} - -.fa-goodreads-g { - --fa: "\f3a9"; -} - -.fa-google-drive { - --fa: "\f3aa"; -} - -.fa-google-play { - --fa: "\f3ab"; -} - -.fa-gripfire { - --fa: "\f3ac"; -} - -.fa-grunt { - --fa: "\f3ad"; -} - -.fa-gulp { - --fa: "\f3ae"; -} - -.fa-square-hacker-news { - --fa: "\f3af"; -} - -.fa-hacker-news-square { - --fa: "\f3af"; -} - -.fa-hire-a-helper { - --fa: "\f3b0"; -} - -.fa-hotjar { - --fa: "\f3b1"; -} - -.fa-hubspot { - --fa: "\f3b2"; -} - -.fa-itunes { - --fa: "\f3b4"; -} - -.fa-itunes-note { - --fa: "\f3b5"; -} - -.fa-jenkins { - --fa: "\f3b6"; -} - -.fa-joget { - --fa: "\f3b7"; -} - -.fa-js { - --fa: "\f3b8"; -} - -.fa-square-js { - --fa: "\f3b9"; -} - -.fa-js-square { - --fa: "\f3b9"; -} - -.fa-keycdn { - --fa: "\f3ba"; -} - -.fa-kickstarter { - --fa: "\f3bb"; -} - -.fa-square-kickstarter { - --fa: "\f3bb"; -} - -.fa-kickstarter-k { - --fa: "\f3bc"; -} - -.fa-laravel { - --fa: "\f3bd"; -} - -.fa-line { - --fa: "\f3c0"; -} - -.fa-lyft { - --fa: "\f3c3"; -} - -.fa-magento { - --fa: "\f3c4"; -} - -.fa-medapps { - --fa: "\f3c6"; -} - -.fa-medrt { - --fa: "\f3c8"; -} - -.fa-microsoft { - --fa: "\f3ca"; -} - -.fa-mix { - --fa: "\f3cb"; -} - -.fa-mizuni { - --fa: "\f3cc"; -} - -.fa-monero { - --fa: "\f3d0"; -} - -.fa-napster { - --fa: "\f3d2"; -} - -.fa-node-js { - --fa: "\f3d3"; -} - -.fa-npm { - --fa: "\f3d4"; -} - -.fa-ns8 { - --fa: "\f3d5"; -} - -.fa-nutritionix { - --fa: "\f3d6"; -} - -.fa-page4 { - --fa: "\f3d7"; -} - -.fa-palfed { - --fa: "\f3d8"; -} - -.fa-patreon { - --fa: "\f3d9"; -} - -.fa-periscope { - --fa: "\f3da"; -} - -.fa-phabricator { - --fa: "\f3db"; -} - -.fa-phoenix-framework { - --fa: "\f3dc"; -} - -.fa-playstation { - --fa: "\f3df"; -} - -.fa-pushed { - --fa: "\f3e1"; -} - -.fa-python { - --fa: "\f3e2"; -} - -.fa-red-river { - --fa: "\f3e3"; -} - -.fa-wpressr { - --fa: "\f3e4"; -} - -.fa-rendact { - --fa: "\f3e4"; -} - -.fa-replyd { - --fa: "\f3e6"; -} - -.fa-resolving { - --fa: "\f3e7"; -} - -.fa-rocketchat { - --fa: "\f3e8"; -} - -.fa-rockrms { - --fa: "\f3e9"; -} - -.fa-schlix { - --fa: "\f3ea"; -} - -.fa-searchengin { - --fa: "\f3eb"; -} - -.fa-servicestack { - --fa: "\f3ec"; -} - -.fa-sistrix { - --fa: "\f3ee"; -} - -.fa-speakap { - --fa: "\f3f3"; -} - -.fa-staylinked { - --fa: "\f3f5"; -} - -.fa-steam-symbol { - --fa: "\f3f6"; -} - -.fa-sticker-mule { - --fa: "\f3f7"; -} - -.fa-studiovinari { - --fa: "\f3f8"; -} - -.fa-supple { - --fa: "\f3f9"; -} - -.fa-uber { - --fa: "\f402"; -} - -.fa-uikit { - --fa: "\f403"; -} - -.fa-uniregistry { - --fa: "\f404"; -} - -.fa-untappd { - --fa: "\f405"; -} - -.fa-ussunnah { - --fa: "\f407"; -} - -.fa-vaadin { - --fa: "\f408"; -} - -.fa-viber { - --fa: "\f409"; -} - -.fa-vimeo { - --fa: "\f40a"; -} - -.fa-vnv { - --fa: "\f40b"; -} - -.fa-square-whatsapp { - --fa: "\f40c"; -} - -.fa-whatsapp-square { - --fa: "\f40c"; -} - -.fa-whmcs { - --fa: "\f40d"; -} - -.fa-wordpress-simple { - --fa: "\f411"; -} - -.fa-xbox { - --fa: "\f412"; -} - -.fa-yandex { - --fa: "\f413"; -} - -.fa-yandex-international { - --fa: "\f414"; -} - -.fa-apple-pay { - --fa: "\f415"; -} - -.fa-cc-apple-pay { - --fa: "\f416"; -} - -.fa-fly { - --fa: "\f417"; -} - -.fa-node { - --fa: "\f419"; -} - -.fa-osi { - --fa: "\f41a"; -} - -.fa-react { - --fa: "\f41b"; -} - -.fa-autoprefixer { - --fa: "\f41c"; -} - -.fa-less { - --fa: "\f41d"; -} - -.fa-sass { - --fa: "\f41e"; -} - -.fa-vuejs { - --fa: "\f41f"; -} - -.fa-angular { - --fa: "\f420"; -} - -.fa-aviato { - --fa: "\f421"; -} - -.fa-ember { - --fa: "\f423"; -} - -.fa-gitter { - --fa: "\f426"; -} - -.fa-hooli { - --fa: "\f427"; -} - -.fa-strava { - --fa: "\f428"; -} - -.fa-stripe { - --fa: "\f429"; -} - -.fa-stripe-s { - --fa: "\f42a"; -} - -.fa-typo3 { - --fa: "\f42b"; -} - -.fa-amazon-pay { - --fa: "\f42c"; -} - -.fa-cc-amazon-pay { - --fa: "\f42d"; -} - -.fa-ethereum { - --fa: "\f42e"; -} - -.fa-korvue { - --fa: "\f42f"; -} - -.fa-elementor { - --fa: "\f430"; -} - -.fa-square-youtube { - --fa: "\f431"; -} - -.fa-youtube-square { - --fa: "\f431"; -} - -.fa-flipboard { - --fa: "\f44d"; -} - -.fa-hips { - --fa: "\f452"; -} - -.fa-php { - --fa: "\f457"; -} - -.fa-quinscape { - --fa: "\f459"; -} - -.fa-readme { - --fa: "\f4d5"; -} - -.fa-java { - --fa: "\f4e4"; -} - -.fa-pied-piper-hat { - --fa: "\f4e5"; -} - -.fa-creative-commons-by { - --fa: "\f4e7"; -} - -.fa-creative-commons-nc { - --fa: "\f4e8"; -} - -.fa-creative-commons-nc-eu { - --fa: "\f4e9"; -} - -.fa-creative-commons-nc-jp { - --fa: "\f4ea"; -} - -.fa-creative-commons-nd { - --fa: "\f4eb"; -} - -.fa-creative-commons-pd { - --fa: "\f4ec"; -} - -.fa-creative-commons-pd-alt { - --fa: "\f4ed"; -} - -.fa-creative-commons-remix { - --fa: "\f4ee"; -} - -.fa-creative-commons-sa { - --fa: "\f4ef"; -} - -.fa-creative-commons-sampling { - --fa: "\f4f0"; -} - -.fa-creative-commons-sampling-plus { - --fa: "\f4f1"; -} - -.fa-creative-commons-share { - --fa: "\f4f2"; -} - -.fa-creative-commons-zero { - --fa: "\f4f3"; -} - -.fa-ebay { - --fa: "\f4f4"; -} - -.fa-keybase { - --fa: "\f4f5"; -} - -.fa-mastodon { - --fa: "\f4f6"; -} - -.fa-r-project { - --fa: "\f4f7"; -} - -.fa-researchgate { - --fa: "\f4f8"; -} - -.fa-teamspeak { - --fa: "\f4f9"; -} - -.fa-first-order-alt { - --fa: "\f50a"; -} - -.fa-fulcrum { - --fa: "\f50b"; -} - -.fa-galactic-republic { - --fa: "\f50c"; -} - -.fa-galactic-senate { - --fa: "\f50d"; -} - -.fa-jedi-order { - --fa: "\f50e"; -} - -.fa-mandalorian { - --fa: "\f50f"; -} - -.fa-old-republic { - --fa: "\f510"; -} - -.fa-phoenix-squadron { - --fa: "\f511"; -} - -.fa-sith { - --fa: "\f512"; -} - -.fa-trade-federation { - --fa: "\f513"; -} - -.fa-wolf-pack-battalion { - --fa: "\f514"; -} - -.fa-hornbill { - --fa: "\f592"; -} - -.fa-mailchimp { - --fa: "\f59e"; -} - -.fa-megaport { - --fa: "\f5a3"; -} - -.fa-nimblr { - --fa: "\f5a8"; -} - -.fa-rev { - --fa: "\f5b2"; -} - -.fa-shopware { - --fa: "\f5b5"; -} - -.fa-squarespace { - --fa: "\f5be"; -} - -.fa-themeco { - --fa: "\f5c6"; -} - -.fa-weebly { - --fa: "\f5cc"; -} - -.fa-wix { - --fa: "\f5cf"; -} - -.fa-ello { - --fa: "\f5f1"; -} - -.fa-hackerrank { - --fa: "\f5f7"; -} - -.fa-kaggle { - --fa: "\f5fa"; -} - -.fa-markdown { - --fa: "\f60f"; -} - -.fa-neos { - --fa: "\f612"; -} - -.fa-zhihu { - --fa: "\f63f"; -} - -.fa-alipay { - --fa: "\f642"; -} - -.fa-the-red-yeti { - --fa: "\f69d"; -} - -.fa-critical-role { - --fa: "\f6c9"; -} - -.fa-d-and-d-beyond { - --fa: "\f6ca"; -} - -.fa-dev { - --fa: "\f6cc"; -} - -.fa-fantasy-flight-games { - --fa: "\f6dc"; -} - -.fa-wizards-of-the-coast { - --fa: "\f730"; -} - -.fa-think-peaks { - --fa: "\f731"; -} - -.fa-reacteurope { - --fa: "\f75d"; -} - -.fa-artstation { - --fa: "\f77a"; -} - -.fa-atlassian { - --fa: "\f77b"; -} - -.fa-canadian-maple-leaf { - --fa: "\f785"; -} - -.fa-centos { - --fa: "\f789"; -} - -.fa-confluence { - --fa: "\f78d"; -} - -.fa-dhl { - --fa: "\f790"; -} - -.fa-diaspora { - --fa: "\f791"; -} - -.fa-fedex { - --fa: "\f797"; -} - -.fa-fedora { - --fa: "\f798"; -} - -.fa-figma { - --fa: "\f799"; -} - -.fa-intercom { - --fa: "\f7af"; -} - -.fa-invision { - --fa: "\f7b0"; -} - -.fa-jira { - --fa: "\f7b1"; -} - -.fa-mendeley { - --fa: "\f7b3"; -} - -.fa-raspberry-pi { - --fa: "\f7bb"; -} - -.fa-redhat { - --fa: "\f7bc"; -} - -.fa-sketch { - --fa: "\f7c6"; -} - -.fa-sourcetree { - --fa: "\f7d3"; -} - -.fa-suse { - --fa: "\f7d6"; -} - -.fa-ubuntu { - --fa: "\f7df"; -} - -.fa-ups { - --fa: "\f7e0"; -} - -.fa-usps { - --fa: "\f7e1"; -} - -.fa-yarn { - --fa: "\f7e3"; -} - -.fa-airbnb { - --fa: "\f834"; -} - -.fa-battle-net { - --fa: "\f835"; -} - -.fa-bootstrap { - --fa: "\f836"; -} - -.fa-buffer { - --fa: "\f837"; -} - -.fa-chromecast { - --fa: "\f838"; -} - -.fa-evernote { - --fa: "\f839"; -} - -.fa-itch-io { - --fa: "\f83a"; -} - -.fa-salesforce { - --fa: "\f83b"; -} - -.fa-speaker-deck { - --fa: "\f83c"; -} - -.fa-symfony { - --fa: "\f83d"; -} - -.fa-waze { - --fa: "\f83f"; -} - -.fa-yammer { - --fa: "\f840"; -} - -.fa-git-alt { - --fa: "\f841"; -} - -.fa-stackpath { - --fa: "\f842"; -} - -.fa-cotton-bureau { - --fa: "\f89e"; -} - -.fa-buy-n-large { - --fa: "\f8a6"; -} - -.fa-mdb { - --fa: "\f8ca"; -} - -.fa-orcid { - --fa: "\f8d2"; -} - -.fa-swift { - --fa: "\f8e1"; -} - -.fa-umbraco { - --fa: "\f8e8"; -} \ No newline at end of file diff --git a/src/media/vendor/fa7free/css/fontawesome.css b/src/media/vendor/fa7free/css/fontawesome.css deleted file mode 100644 index 9e736b5..0000000 --- a/src/media/vendor/fa7free/css/fontawesome.css +++ /dev/null @@ -1,8361 +0,0 @@ -/*! - * Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2025 Fonticons, Inc. - */ -.fa-solid, -.fa-regular, -.fa-brands, -.fa-classic, -.fas, -.far, -.fab, -.fa { - --_fa-family: var(--fa-family, var(--fa-style-family, "Font Awesome 7 Free")); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - display: var(--fa-display, inline-block); - font-family: var(--_fa-family); - font-feature-settings: normal; - font-style: normal; - font-synthesis: none; - font-variant: normal; - font-weight: var(--fa-style, 900); - line-height: 1; - text-align: center; - text-rendering: auto; - width: var(--fa-width, 1.25em); -} - -:is(.fas, -.far, -.fab, -.fa-solid, -.fa-regular, -.fa-brands, -.fa-classic, -.fa)::before { - content: var(--fa)/""; -} - -@supports not (content: ""/"") { - :is(.fas, - .far, - .fab, - .fa-solid, - .fa-regular, - .fa-brands, - .fa-classic, - .fa)::before { - content: var(--fa); - } -} -.fa-1x { - font-size: 1em; -} - -.fa-2x { - font-size: 2em; -} - -.fa-3x { - font-size: 3em; -} - -.fa-4x { - font-size: 4em; -} - -.fa-5x { - font-size: 5em; -} - -.fa-6x { - font-size: 6em; -} - -.fa-7x { - font-size: 7em; -} - -.fa-8x { - font-size: 8em; -} - -.fa-9x { - font-size: 9em; -} - -.fa-10x { - font-size: 10em; -} - -.fa-2xs { - font-size: calc(10 / 16 * 1em); /* converts a 10px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 10 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 10 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-xs { - font-size: calc(12 / 16 * 1em); /* converts a 12px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 12 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 12 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-sm { - font-size: calc(14 / 16 * 1em); /* converts a 14px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 14 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 14 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-lg { - font-size: calc(20 / 16 * 1em); /* converts a 20px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 20 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 20 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-xl { - font-size: calc(24 / 16 * 1em); /* converts a 24px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 24 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 24 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-2xl { - font-size: calc(32 / 16 * 1em); /* converts a 32px size into an em-based value that's relative to the scale's 16px base */ - line-height: calc(1 / 32 * 1em); /* sets the line-height of the icon back to that of it's parent */ - vertical-align: calc((6 / 32 - 0.375) * 1em); /* vertically centers the icon taking into account the surrounding text's descender */ -} - -.fa-width-auto { - --fa-width: auto; -} - -.fa-fw, -.fa-width-fixed { - --fa-width: 1.25em; -} - -.fa-ul { - list-style-type: none; - margin-inline-start: var(--fa-li-margin, 2.5em); - padding-inline-start: 0; -} -.fa-ul > li { - position: relative; -} - -.fa-li { - inset-inline-start: calc(-1 * var(--fa-li-width, 2em)); - position: absolute; - text-align: center; - width: var(--fa-li-width, 2em); - line-height: inherit; -} - -/* Heads Up: Bordered Icons will not be supported in the future! - - This feature will be deprecated in the next major release of Font Awesome (v8)! - - You may continue to use it in this version *v7), but it will not be supported in Font Awesome v8. -*/ -/* Notes: -* --@{v.$css-prefix}-border-width = 1/16 by default (to render as ~1px based on a 16px default font-size) -* --@{v.$css-prefix}-border-padding = - ** 3/16 for vertical padding (to give ~2px of vertical whitespace around an icon considering it's vertical alignment) - ** 4/16 for horizontal padding (to give ~4px of horizontal whitespace around an icon) -*/ -.fa-border { - border-color: var(--fa-border-color, #eee); - border-radius: var(--fa-border-radius, 0.1em); - border-style: var(--fa-border-style, solid); - border-width: var(--fa-border-width, 0.0625em); - box-sizing: var(--fa-border-box-sizing, content-box); - padding: var(--fa-border-padding, 0.1875em 0.25em); -} - -.fa-pull-left, -.fa-pull-start { - float: inline-start; - margin-inline-end: var(--fa-pull-margin, 0.3em); -} - -.fa-pull-right, -.fa-pull-end { - float: inline-end; - margin-inline-start: var(--fa-pull-margin, 0.3em); -} - -.fa-beat { - animation-name: fa-beat; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, ease-in-out); -} - -.fa-bounce { - animation-name: fa-bounce; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); -} - -.fa-fade { - animation-name: fa-fade; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); -} - -.fa-beat-fade { - animation-name: fa-beat-fade; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); -} - -.fa-flip { - animation-name: fa-flip; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, ease-in-out); -} - -.fa-shake { - animation-name: fa-shake; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, linear); -} - -.fa-spin { - animation-name: fa-spin; - animation-delay: var(--fa-animation-delay, 0s); - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 2s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, linear); -} - -.fa-spin-reverse { - --fa-animation-direction: reverse; -} - -.fa-pulse, -.fa-spin-pulse { - animation-name: fa-spin; - animation-direction: var(--fa-animation-direction, normal); - animation-duration: var(--fa-animation-duration, 1s); - animation-iteration-count: var(--fa-animation-iteration-count, infinite); - animation-timing-function: var(--fa-animation-timing, steps(8)); -} - -@media (prefers-reduced-motion: reduce) { - .fa-beat, - .fa-bounce, - .fa-fade, - .fa-beat-fade, - .fa-flip, - .fa-pulse, - .fa-shake, - .fa-spin, - .fa-spin-pulse { - animation: none !important; - transition: none !important; - } -} -@keyframes fa-beat { - 0%, 90% { - transform: scale(1); - } - 45% { - transform: scale(var(--fa-beat-scale, 1.25)); - } -} -@keyframes fa-bounce { - 0% { - transform: scale(1, 1) translateY(0); - } - 10% { - transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); - } - 30% { - transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); - } - 50% { - transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); - } - 57% { - transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); - } - 64% { - transform: scale(1, 1) translateY(0); - } - 100% { - transform: scale(1, 1) translateY(0); - } -} -@keyframes fa-fade { - 50% { - opacity: var(--fa-fade-opacity, 0.4); - } -} -@keyframes fa-beat-fade { - 0%, 100% { - opacity: var(--fa-beat-fade-opacity, 0.4); - transform: scale(1); - } - 50% { - opacity: 1; - transform: scale(var(--fa-beat-fade-scale, 1.125)); - } -} -@keyframes fa-flip { - 50% { - transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); - } -} -@keyframes fa-shake { - 0% { - transform: rotate(-15deg); - } - 4% { - transform: rotate(15deg); - } - 8%, 24% { - transform: rotate(-18deg); - } - 12%, 28% { - transform: rotate(18deg); - } - 16% { - transform: rotate(-22deg); - } - 20% { - transform: rotate(22deg); - } - 32% { - transform: rotate(-12deg); - } - 36% { - transform: rotate(12deg); - } - 40%, 100% { - transform: rotate(0deg); - } -} -@keyframes fa-spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} -.fa-rotate-90 { - transform: rotate(90deg); -} - -.fa-rotate-180 { - transform: rotate(180deg); -} - -.fa-rotate-270 { - transform: rotate(270deg); -} - -.fa-flip-horizontal { - transform: scale(-1, 1); -} - -.fa-flip-vertical { - transform: scale(1, -1); -} - -.fa-flip-both, -.fa-flip-horizontal.fa-flip-vertical { - transform: scale(-1, -1); -} - -.fa-rotate-by { - transform: rotate(var(--fa-rotate-angle, 0)); -} - -.fa-stack { - display: inline-block; - height: 2em; - line-height: 2em; - position: relative; - vertical-align: middle; - width: 2.5em; -} - -.fa-stack-1x, -.fa-stack-2x { - --fa-width: 100%; - inset: 0; - position: absolute; - text-align: center; - width: var(--fa-width); - z-index: var(--fa-stack-z-index, auto); -} - -.fa-stack-1x { - line-height: inherit; -} - -.fa-stack-2x { - font-size: 2em; -} - -.fa-inverse { - color: var(--fa-inverse, #fff); -} - -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.fa-0 { - --fa: "\30 "; -} - -.fa-1 { - --fa: "\31 "; -} - -.fa-2 { - --fa: "\32 "; -} - -.fa-3 { - --fa: "\33 "; -} - -.fa-4 { - --fa: "\34 "; -} - -.fa-5 { - --fa: "\35 "; -} - -.fa-6 { - --fa: "\36 "; -} - -.fa-7 { - --fa: "\37 "; -} - -.fa-8 { - --fa: "\38 "; -} - -.fa-9 { - --fa: "\39 "; -} - -.fa-exclamation { - --fa: "\!"; -} - -.fa-hashtag { - --fa: "\#"; -} - -.fa-dollar-sign { - --fa: "\$"; -} - -.fa-dollar { - --fa: "\$"; -} - -.fa-usd { - --fa: "\$"; -} - -.fa-percent { - --fa: "\%"; -} - -.fa-percentage { - --fa: "\%"; -} - -.fa-asterisk { - --fa: "\*"; -} - -.fa-plus { - --fa: "\+"; -} - -.fa-add { - --fa: "\+"; -} - -.fa-less-than { - --fa: "\<"; -} - -.fa-equals { - --fa: "\="; -} - -.fa-greater-than { - --fa: "\>"; -} - -.fa-question { - --fa: "\?"; -} - -.fa-at { - --fa: "\@"; -} - -.fa-a { - --fa: "A"; -} - -.fa-b { - --fa: "B"; -} - -.fa-c { - --fa: "C"; -} - -.fa-d { - --fa: "D"; -} - -.fa-e { - --fa: "E"; -} - -.fa-f { - --fa: "F"; -} - -.fa-g { - --fa: "G"; -} - -.fa-h { - --fa: "H"; -} - -.fa-i { - --fa: "I"; -} - -.fa-j { - --fa: "J"; -} - -.fa-k { - --fa: "K"; -} - -.fa-l { - --fa: "L"; -} - -.fa-m { - --fa: "M"; -} - -.fa-n { - --fa: "N"; -} - -.fa-o { - --fa: "O"; -} - -.fa-p { - --fa: "P"; -} - -.fa-q { - --fa: "Q"; -} - -.fa-r { - --fa: "R"; -} - -.fa-s { - --fa: "S"; -} - -.fa-t { - --fa: "T"; -} - -.fa-u { - --fa: "U"; -} - -.fa-v { - --fa: "V"; -} - -.fa-w { - --fa: "W"; -} - -.fa-x { - --fa: "X"; -} - -.fa-y { - --fa: "Y"; -} - -.fa-z { - --fa: "Z"; -} - -.fa-faucet { - --fa: "\e005"; -} - -.fa-faucet-drip { - --fa: "\e006"; -} - -.fa-house-chimney-window { - --fa: "\e00d"; -} - -.fa-house-signal { - --fa: "\e012"; -} - -.fa-temperature-arrow-down { - --fa: "\e03f"; -} - -.fa-temperature-down { - --fa: "\e03f"; -} - -.fa-temperature-arrow-up { - --fa: "\e040"; -} - -.fa-temperature-up { - --fa: "\e040"; -} - -.fa-trailer { - --fa: "\e041"; -} - -.fa-bacteria { - --fa: "\e059"; -} - -.fa-bacterium { - --fa: "\e05a"; -} - -.fa-box-tissue { - --fa: "\e05b"; -} - -.fa-hand-holding-medical { - --fa: "\e05c"; -} - -.fa-hand-sparkles { - --fa: "\e05d"; -} - -.fa-hands-bubbles { - --fa: "\e05e"; -} - -.fa-hands-wash { - --fa: "\e05e"; -} - -.fa-handshake-slash { - --fa: "\e060"; -} - -.fa-handshake-alt-slash { - --fa: "\e060"; -} - -.fa-handshake-simple-slash { - --fa: "\e060"; -} - -.fa-head-side-cough { - --fa: "\e061"; -} - -.fa-head-side-cough-slash { - --fa: "\e062"; -} - -.fa-head-side-mask { - --fa: "\e063"; -} - -.fa-head-side-virus { - --fa: "\e064"; -} - -.fa-house-chimney-user { - --fa: "\e065"; -} - -.fa-house-laptop { - --fa: "\e066"; -} - -.fa-laptop-house { - --fa: "\e066"; -} - -.fa-lungs-virus { - --fa: "\e067"; -} - -.fa-people-arrows { - --fa: "\e068"; -} - -.fa-people-arrows-left-right { - --fa: "\e068"; -} - -.fa-plane-slash { - --fa: "\e069"; -} - -.fa-pump-medical { - --fa: "\e06a"; -} - -.fa-pump-soap { - --fa: "\e06b"; -} - -.fa-shield-virus { - --fa: "\e06c"; -} - -.fa-sink { - --fa: "\e06d"; -} - -.fa-soap { - --fa: "\e06e"; -} - -.fa-stopwatch-20 { - --fa: "\e06f"; -} - -.fa-shop-slash { - --fa: "\e070"; -} - -.fa-store-alt-slash { - --fa: "\e070"; -} - -.fa-store-slash { - --fa: "\e071"; -} - -.fa-toilet-paper-slash { - --fa: "\e072"; -} - -.fa-users-slash { - --fa: "\e073"; -} - -.fa-virus { - --fa: "\e074"; -} - -.fa-virus-slash { - --fa: "\e075"; -} - -.fa-viruses { - --fa: "\e076"; -} - -.fa-vest { - --fa: "\e085"; -} - -.fa-vest-patches { - --fa: "\e086"; -} - -.fa-arrow-trend-down { - --fa: "\e097"; -} - -.fa-arrow-trend-up { - --fa: "\e098"; -} - -.fa-arrow-up-from-bracket { - --fa: "\e09a"; -} - -.fa-austral-sign { - --fa: "\e0a9"; -} - -.fa-baht-sign { - --fa: "\e0ac"; -} - -.fa-bitcoin-sign { - --fa: "\e0b4"; -} - -.fa-bolt-lightning { - --fa: "\e0b7"; -} - -.fa-book-bookmark { - --fa: "\e0bb"; -} - -.fa-camera-rotate { - --fa: "\e0d8"; -} - -.fa-cedi-sign { - --fa: "\e0df"; -} - -.fa-chart-column { - --fa: "\e0e3"; -} - -.fa-chart-gantt { - --fa: "\e0e4"; -} - -.fa-clapperboard { - --fa: "\e131"; -} - -.fa-clover { - --fa: "\e139"; -} - -.fa-code-compare { - --fa: "\e13a"; -} - -.fa-code-fork { - --fa: "\e13b"; -} - -.fa-code-pull-request { - --fa: "\e13c"; -} - -.fa-colon-sign { - --fa: "\e140"; -} - -.fa-cruzeiro-sign { - --fa: "\e152"; -} - -.fa-display { - --fa: "\e163"; -} - -.fa-dong-sign { - --fa: "\e169"; -} - -.fa-elevator { - --fa: "\e16d"; -} - -.fa-filter-circle-xmark { - --fa: "\e17b"; -} - -.fa-florin-sign { - --fa: "\e184"; -} - -.fa-folder-closed { - --fa: "\e185"; -} - -.fa-franc-sign { - --fa: "\e18f"; -} - -.fa-guarani-sign { - --fa: "\e19a"; -} - -.fa-gun { - --fa: "\e19b"; -} - -.fa-hands-clapping { - --fa: "\e1a8"; -} - -.fa-house-user { - --fa: "\e1b0"; -} - -.fa-home-user { - --fa: "\e1b0"; -} - -.fa-indian-rupee-sign { - --fa: "\e1bc"; -} - -.fa-indian-rupee { - --fa: "\e1bc"; -} - -.fa-inr { - --fa: "\e1bc"; -} - -.fa-kip-sign { - --fa: "\e1c4"; -} - -.fa-lari-sign { - --fa: "\e1c8"; -} - -.fa-litecoin-sign { - --fa: "\e1d3"; -} - -.fa-manat-sign { - --fa: "\e1d5"; -} - -.fa-mask-face { - --fa: "\e1d7"; -} - -.fa-mill-sign { - --fa: "\e1ed"; -} - -.fa-money-bills { - --fa: "\e1f3"; -} - -.fa-naira-sign { - --fa: "\e1f6"; -} - -.fa-notdef { - --fa: "\e1fe"; -} - -.fa-panorama { - --fa: "\e209"; -} - -.fa-peseta-sign { - --fa: "\e221"; -} - -.fa-peso-sign { - --fa: "\e222"; -} - -.fa-plane-up { - --fa: "\e22d"; -} - -.fa-rupiah-sign { - --fa: "\e23d"; -} - -.fa-stairs { - --fa: "\e289"; -} - -.fa-timeline { - --fa: "\e29c"; -} - -.fa-truck-front { - --fa: "\e2b7"; -} - -.fa-turkish-lira-sign { - --fa: "\e2bb"; -} - -.fa-try { - --fa: "\e2bb"; -} - -.fa-turkish-lira { - --fa: "\e2bb"; -} - -.fa-vault { - --fa: "\e2c5"; -} - -.fa-wand-magic-sparkles { - --fa: "\e2ca"; -} - -.fa-magic-wand-sparkles { - --fa: "\e2ca"; -} - -.fa-wheat-awn { - --fa: "\e2cd"; -} - -.fa-wheat-alt { - --fa: "\e2cd"; -} - -.fa-wheelchair-move { - --fa: "\e2ce"; -} - -.fa-wheelchair-alt { - --fa: "\e2ce"; -} - -.fa-bangladeshi-taka-sign { - --fa: "\e2e6"; -} - -.fa-bowl-rice { - --fa: "\e2eb"; -} - -.fa-person-pregnant { - --fa: "\e31e"; -} - -.fa-house-chimney { - --fa: "\e3af"; -} - -.fa-home-lg { - --fa: "\e3af"; -} - -.fa-house-crack { - --fa: "\e3b1"; -} - -.fa-house-medical { - --fa: "\e3b2"; -} - -.fa-cent-sign { - --fa: "\e3f5"; -} - -.fa-plus-minus { - --fa: "\e43c"; -} - -.fa-sailboat { - --fa: "\e445"; -} - -.fa-section { - --fa: "\e447"; -} - -.fa-shrimp { - --fa: "\e448"; -} - -.fa-brazilian-real-sign { - --fa: "\e46c"; -} - -.fa-chart-simple { - --fa: "\e473"; -} - -.fa-diagram-next { - --fa: "\e476"; -} - -.fa-diagram-predecessor { - --fa: "\e477"; -} - -.fa-diagram-successor { - --fa: "\e47a"; -} - -.fa-earth-oceania { - --fa: "\e47b"; -} - -.fa-globe-oceania { - --fa: "\e47b"; -} - -.fa-bug-slash { - --fa: "\e490"; -} - -.fa-file-circle-plus { - --fa: "\e494"; -} - -.fa-shop-lock { - --fa: "\e4a5"; -} - -.fa-virus-covid { - --fa: "\e4a8"; -} - -.fa-virus-covid-slash { - --fa: "\e4a9"; -} - -.fa-anchor-circle-check { - --fa: "\e4aa"; -} - -.fa-anchor-circle-exclamation { - --fa: "\e4ab"; -} - -.fa-anchor-circle-xmark { - --fa: "\e4ac"; -} - -.fa-anchor-lock { - --fa: "\e4ad"; -} - -.fa-arrow-down-up-across-line { - --fa: "\e4af"; -} - -.fa-arrow-down-up-lock { - --fa: "\e4b0"; -} - -.fa-arrow-right-to-city { - --fa: "\e4b3"; -} - -.fa-arrow-up-from-ground-water { - --fa: "\e4b5"; -} - -.fa-arrow-up-from-water-pump { - --fa: "\e4b6"; -} - -.fa-arrow-up-right-dots { - --fa: "\e4b7"; -} - -.fa-arrows-down-to-line { - --fa: "\e4b8"; -} - -.fa-arrows-down-to-people { - --fa: "\e4b9"; -} - -.fa-arrows-left-right-to-line { - --fa: "\e4ba"; -} - -.fa-arrows-spin { - --fa: "\e4bb"; -} - -.fa-arrows-split-up-and-left { - --fa: "\e4bc"; -} - -.fa-arrows-to-circle { - --fa: "\e4bd"; -} - -.fa-arrows-to-dot { - --fa: "\e4be"; -} - -.fa-arrows-to-eye { - --fa: "\e4bf"; -} - -.fa-arrows-turn-right { - --fa: "\e4c0"; -} - -.fa-arrows-turn-to-dots { - --fa: "\e4c1"; -} - -.fa-arrows-up-to-line { - --fa: "\e4c2"; -} - -.fa-bore-hole { - --fa: "\e4c3"; -} - -.fa-bottle-droplet { - --fa: "\e4c4"; -} - -.fa-bottle-water { - --fa: "\e4c5"; -} - -.fa-bowl-food { - --fa: "\e4c6"; -} - -.fa-boxes-packing { - --fa: "\e4c7"; -} - -.fa-bridge { - --fa: "\e4c8"; -} - -.fa-bridge-circle-check { - --fa: "\e4c9"; -} - -.fa-bridge-circle-exclamation { - --fa: "\e4ca"; -} - -.fa-bridge-circle-xmark { - --fa: "\e4cb"; -} - -.fa-bridge-lock { - --fa: "\e4cc"; -} - -.fa-bridge-water { - --fa: "\e4ce"; -} - -.fa-bucket { - --fa: "\e4cf"; -} - -.fa-bugs { - --fa: "\e4d0"; -} - -.fa-building-circle-arrow-right { - --fa: "\e4d1"; -} - -.fa-building-circle-check { - --fa: "\e4d2"; -} - -.fa-building-circle-exclamation { - --fa: "\e4d3"; -} - -.fa-building-circle-xmark { - --fa: "\e4d4"; -} - -.fa-building-flag { - --fa: "\e4d5"; -} - -.fa-building-lock { - --fa: "\e4d6"; -} - -.fa-building-ngo { - --fa: "\e4d7"; -} - -.fa-building-shield { - --fa: "\e4d8"; -} - -.fa-building-un { - --fa: "\e4d9"; -} - -.fa-building-user { - --fa: "\e4da"; -} - -.fa-building-wheat { - --fa: "\e4db"; -} - -.fa-burst { - --fa: "\e4dc"; -} - -.fa-car-on { - --fa: "\e4dd"; -} - -.fa-car-tunnel { - --fa: "\e4de"; -} - -.fa-child-combatant { - --fa: "\e4e0"; -} - -.fa-child-rifle { - --fa: "\e4e0"; -} - -.fa-children { - --fa: "\e4e1"; -} - -.fa-circle-nodes { - --fa: "\e4e2"; -} - -.fa-clipboard-question { - --fa: "\e4e3"; -} - -.fa-cloud-showers-water { - --fa: "\e4e4"; -} - -.fa-computer { - --fa: "\e4e5"; -} - -.fa-cubes-stacked { - --fa: "\e4e6"; -} - -.fa-envelope-circle-check { - --fa: "\e4e8"; -} - -.fa-explosion { - --fa: "\e4e9"; -} - -.fa-ferry { - --fa: "\e4ea"; -} - -.fa-file-circle-exclamation { - --fa: "\e4eb"; -} - -.fa-file-circle-minus { - --fa: "\e4ed"; -} - -.fa-file-circle-question { - --fa: "\e4ef"; -} - -.fa-file-shield { - --fa: "\e4f0"; -} - -.fa-fire-burner { - --fa: "\e4f1"; -} - -.fa-fish-fins { - --fa: "\e4f2"; -} - -.fa-flask-vial { - --fa: "\e4f3"; -} - -.fa-glass-water { - --fa: "\e4f4"; -} - -.fa-glass-water-droplet { - --fa: "\e4f5"; -} - -.fa-group-arrows-rotate { - --fa: "\e4f6"; -} - -.fa-hand-holding-hand { - --fa: "\e4f7"; -} - -.fa-handcuffs { - --fa: "\e4f8"; -} - -.fa-hands-bound { - --fa: "\e4f9"; -} - -.fa-hands-holding-child { - --fa: "\e4fa"; -} - -.fa-hands-holding-circle { - --fa: "\e4fb"; -} - -.fa-heart-circle-bolt { - --fa: "\e4fc"; -} - -.fa-heart-circle-check { - --fa: "\e4fd"; -} - -.fa-heart-circle-exclamation { - --fa: "\e4fe"; -} - -.fa-heart-circle-minus { - --fa: "\e4ff"; -} - -.fa-heart-circle-plus { - --fa: "\e500"; -} - -.fa-heart-circle-xmark { - --fa: "\e501"; -} - -.fa-helicopter-symbol { - --fa: "\e502"; -} - -.fa-helmet-un { - --fa: "\e503"; -} - -.fa-hill-avalanche { - --fa: "\e507"; -} - -.fa-hill-rockslide { - --fa: "\e508"; -} - -.fa-house-circle-check { - --fa: "\e509"; -} - -.fa-house-circle-exclamation { - --fa: "\e50a"; -} - -.fa-house-circle-xmark { - --fa: "\e50b"; -} - -.fa-house-fire { - --fa: "\e50c"; -} - -.fa-house-flag { - --fa: "\e50d"; -} - -.fa-house-flood-water { - --fa: "\e50e"; -} - -.fa-house-flood-water-circle-arrow-right { - --fa: "\e50f"; -} - -.fa-house-lock { - --fa: "\e510"; -} - -.fa-house-medical-circle-check { - --fa: "\e511"; -} - -.fa-house-medical-circle-exclamation { - --fa: "\e512"; -} - -.fa-house-medical-circle-xmark { - --fa: "\e513"; -} - -.fa-house-medical-flag { - --fa: "\e514"; -} - -.fa-house-tsunami { - --fa: "\e515"; -} - -.fa-jar { - --fa: "\e516"; -} - -.fa-jar-wheat { - --fa: "\e517"; -} - -.fa-jet-fighter-up { - --fa: "\e518"; -} - -.fa-jug-detergent { - --fa: "\e519"; -} - -.fa-kitchen-set { - --fa: "\e51a"; -} - -.fa-land-mine-on { - --fa: "\e51b"; -} - -.fa-landmark-flag { - --fa: "\e51c"; -} - -.fa-laptop-file { - --fa: "\e51d"; -} - -.fa-lines-leaning { - --fa: "\e51e"; -} - -.fa-location-pin-lock { - --fa: "\e51f"; -} - -.fa-locust { - --fa: "\e520"; -} - -.fa-magnifying-glass-arrow-right { - --fa: "\e521"; -} - -.fa-magnifying-glass-chart { - --fa: "\e522"; -} - -.fa-mars-and-venus-burst { - --fa: "\e523"; -} - -.fa-mask-ventilator { - --fa: "\e524"; -} - -.fa-mattress-pillow { - --fa: "\e525"; -} - -.fa-mobile-retro { - --fa: "\e527"; -} - -.fa-money-bill-transfer { - --fa: "\e528"; -} - -.fa-money-bill-trend-up { - --fa: "\e529"; -} - -.fa-money-bill-wheat { - --fa: "\e52a"; -} - -.fa-mosquito { - --fa: "\e52b"; -} - -.fa-mosquito-net { - --fa: "\e52c"; -} - -.fa-mound { - --fa: "\e52d"; -} - -.fa-mountain-city { - --fa: "\e52e"; -} - -.fa-mountain-sun { - --fa: "\e52f"; -} - -.fa-oil-well { - --fa: "\e532"; -} - -.fa-people-group { - --fa: "\e533"; -} - -.fa-people-line { - --fa: "\e534"; -} - -.fa-people-pulling { - --fa: "\e535"; -} - -.fa-people-robbery { - --fa: "\e536"; -} - -.fa-people-roof { - --fa: "\e537"; -} - -.fa-person-arrow-down-to-line { - --fa: "\e538"; -} - -.fa-person-arrow-up-from-line { - --fa: "\e539"; -} - -.fa-person-breastfeeding { - --fa: "\e53a"; -} - -.fa-person-burst { - --fa: "\e53b"; -} - -.fa-person-cane { - --fa: "\e53c"; -} - -.fa-person-chalkboard { - --fa: "\e53d"; -} - -.fa-person-circle-check { - --fa: "\e53e"; -} - -.fa-person-circle-exclamation { - --fa: "\e53f"; -} - -.fa-person-circle-minus { - --fa: "\e540"; -} - -.fa-person-circle-plus { - --fa: "\e541"; -} - -.fa-person-circle-question { - --fa: "\e542"; -} - -.fa-person-circle-xmark { - --fa: "\e543"; -} - -.fa-person-dress-burst { - --fa: "\e544"; -} - -.fa-person-drowning { - --fa: "\e545"; -} - -.fa-person-falling { - --fa: "\e546"; -} - -.fa-person-falling-burst { - --fa: "\e547"; -} - -.fa-person-half-dress { - --fa: "\e548"; -} - -.fa-person-harassing { - --fa: "\e549"; -} - -.fa-person-military-pointing { - --fa: "\e54a"; -} - -.fa-person-military-rifle { - --fa: "\e54b"; -} - -.fa-person-military-to-person { - --fa: "\e54c"; -} - -.fa-person-rays { - --fa: "\e54d"; -} - -.fa-person-rifle { - --fa: "\e54e"; -} - -.fa-person-shelter { - --fa: "\e54f"; -} - -.fa-person-walking-arrow-loop-left { - --fa: "\e551"; -} - -.fa-person-walking-arrow-right { - --fa: "\e552"; -} - -.fa-person-walking-dashed-line-arrow-right { - --fa: "\e553"; -} - -.fa-person-walking-luggage { - --fa: "\e554"; -} - -.fa-plane-circle-check { - --fa: "\e555"; -} - -.fa-plane-circle-exclamation { - --fa: "\e556"; -} - -.fa-plane-circle-xmark { - --fa: "\e557"; -} - -.fa-plane-lock { - --fa: "\e558"; -} - -.fa-plate-wheat { - --fa: "\e55a"; -} - -.fa-plug-circle-bolt { - --fa: "\e55b"; -} - -.fa-plug-circle-check { - --fa: "\e55c"; -} - -.fa-plug-circle-exclamation { - --fa: "\e55d"; -} - -.fa-plug-circle-minus { - --fa: "\e55e"; -} - -.fa-plug-circle-plus { - --fa: "\e55f"; -} - -.fa-plug-circle-xmark { - --fa: "\e560"; -} - -.fa-ranking-star { - --fa: "\e561"; -} - -.fa-road-barrier { - --fa: "\e562"; -} - -.fa-road-bridge { - --fa: "\e563"; -} - -.fa-road-circle-check { - --fa: "\e564"; -} - -.fa-road-circle-exclamation { - --fa: "\e565"; -} - -.fa-road-circle-xmark { - --fa: "\e566"; -} - -.fa-road-lock { - --fa: "\e567"; -} - -.fa-road-spikes { - --fa: "\e568"; -} - -.fa-rug { - --fa: "\e569"; -} - -.fa-sack-xmark { - --fa: "\e56a"; -} - -.fa-school-circle-check { - --fa: "\e56b"; -} - -.fa-school-circle-exclamation { - --fa: "\e56c"; -} - -.fa-school-circle-xmark { - --fa: "\e56d"; -} - -.fa-school-flag { - --fa: "\e56e"; -} - -.fa-school-lock { - --fa: "\e56f"; -} - -.fa-sheet-plastic { - --fa: "\e571"; -} - -.fa-shield-cat { - --fa: "\e572"; -} - -.fa-shield-dog { - --fa: "\e573"; -} - -.fa-shield-heart { - --fa: "\e574"; -} - -.fa-square-nfi { - --fa: "\e576"; -} - -.fa-square-person-confined { - --fa: "\e577"; -} - -.fa-square-virus { - --fa: "\e578"; -} - -.fa-staff-snake { - --fa: "\e579"; -} - -.fa-rod-asclepius { - --fa: "\e579"; -} - -.fa-rod-snake { - --fa: "\e579"; -} - -.fa-staff-aesculapius { - --fa: "\e579"; -} - -.fa-sun-plant-wilt { - --fa: "\e57a"; -} - -.fa-tarp { - --fa: "\e57b"; -} - -.fa-tarp-droplet { - --fa: "\e57c"; -} - -.fa-tent { - --fa: "\e57d"; -} - -.fa-tent-arrow-down-to-line { - --fa: "\e57e"; -} - -.fa-tent-arrow-left-right { - --fa: "\e57f"; -} - -.fa-tent-arrow-turn-left { - --fa: "\e580"; -} - -.fa-tent-arrows-down { - --fa: "\e581"; -} - -.fa-tents { - --fa: "\e582"; -} - -.fa-toilet-portable { - --fa: "\e583"; -} - -.fa-toilets-portable { - --fa: "\e584"; -} - -.fa-tower-cell { - --fa: "\e585"; -} - -.fa-tower-observation { - --fa: "\e586"; -} - -.fa-tree-city { - --fa: "\e587"; -} - -.fa-trowel { - --fa: "\e589"; -} - -.fa-trowel-bricks { - --fa: "\e58a"; -} - -.fa-truck-arrow-right { - --fa: "\e58b"; -} - -.fa-truck-droplet { - --fa: "\e58c"; -} - -.fa-truck-field { - --fa: "\e58d"; -} - -.fa-truck-field-un { - --fa: "\e58e"; -} - -.fa-truck-plane { - --fa: "\e58f"; -} - -.fa-users-between-lines { - --fa: "\e591"; -} - -.fa-users-line { - --fa: "\e592"; -} - -.fa-users-rays { - --fa: "\e593"; -} - -.fa-users-rectangle { - --fa: "\e594"; -} - -.fa-users-viewfinder { - --fa: "\e595"; -} - -.fa-vial-circle-check { - --fa: "\e596"; -} - -.fa-vial-virus { - --fa: "\e597"; -} - -.fa-wheat-awn-circle-exclamation { - --fa: "\e598"; -} - -.fa-worm { - --fa: "\e599"; -} - -.fa-xmarks-lines { - --fa: "\e59a"; -} - -.fa-child-dress { - --fa: "\e59c"; -} - -.fa-child-reaching { - --fa: "\e59d"; -} - -.fa-file-circle-check { - --fa: "\e5a0"; -} - -.fa-file-circle-xmark { - --fa: "\e5a1"; -} - -.fa-person-through-window { - --fa: "\e5a9"; -} - -.fa-plant-wilt { - --fa: "\e5aa"; -} - -.fa-stapler { - --fa: "\e5af"; -} - -.fa-train-tram { - --fa: "\e5b4"; -} - -.fa-table-cells-column-lock { - --fa: "\e678"; -} - -.fa-table-cells-row-lock { - --fa: "\e67a"; -} - -.fa-web-awesome { - --fa: "\e682"; -} - -.fa-thumbtack-slash { - --fa: "\e68f"; -} - -.fa-thumb-tack-slash { - --fa: "\e68f"; -} - -.fa-table-cells-row-unlock { - --fa: "\e691"; -} - -.fa-chart-diagram { - --fa: "\e695"; -} - -.fa-comment-nodes { - --fa: "\e696"; -} - -.fa-file-fragment { - --fa: "\e697"; -} - -.fa-file-half-dashed { - --fa: "\e698"; -} - -.fa-hexagon-nodes { - --fa: "\e699"; -} - -.fa-hexagon-nodes-bolt { - --fa: "\e69a"; -} - -.fa-square-binary { - --fa: "\e69b"; -} - -.fa-pentagon { - --fa: "\e790"; -} - -.fa-non-binary { - --fa: "\e807"; -} - -.fa-spiral { - --fa: "\e80a"; -} - -.fa-mobile-vibrate { - --fa: "\e816"; -} - -.fa-single-quote-left { - --fa: "\e81b"; -} - -.fa-single-quote-right { - --fa: "\e81c"; -} - -.fa-bus-side { - --fa: "\e81d"; -} - -.fa-septagon { - --fa: "\e820"; -} - -.fa-heptagon { - --fa: "\e820"; -} - -.fa-martini-glass-empty { - --fa: "\f000"; -} - -.fa-glass-martini { - --fa: "\f000"; -} - -.fa-music { - --fa: "\f001"; -} - -.fa-magnifying-glass { - --fa: "\f002"; -} - -.fa-search { - --fa: "\f002"; -} - -.fa-heart { - --fa: "\f004"; -} - -.fa-star { - --fa: "\f005"; -} - -.fa-user { - --fa: "\f007"; -} - -.fa-user-alt { - --fa: "\f007"; -} - -.fa-user-large { - --fa: "\f007"; -} - -.fa-film { - --fa: "\f008"; -} - -.fa-film-alt { - --fa: "\f008"; -} - -.fa-film-simple { - --fa: "\f008"; -} - -.fa-table-cells-large { - --fa: "\f009"; -} - -.fa-th-large { - --fa: "\f009"; -} - -.fa-table-cells { - --fa: "\f00a"; -} - -.fa-th { - --fa: "\f00a"; -} - -.fa-table-list { - --fa: "\f00b"; -} - -.fa-th-list { - --fa: "\f00b"; -} - -.fa-check { - --fa: "\f00c"; -} - -.fa-xmark { - --fa: "\f00d"; -} - -.fa-close { - --fa: "\f00d"; -} - -.fa-multiply { - --fa: "\f00d"; -} - -.fa-remove { - --fa: "\f00d"; -} - -.fa-times { - --fa: "\f00d"; -} - -.fa-magnifying-glass-plus { - --fa: "\f00e"; -} - -.fa-search-plus { - --fa: "\f00e"; -} - -.fa-magnifying-glass-minus { - --fa: "\f010"; -} - -.fa-search-minus { - --fa: "\f010"; -} - -.fa-power-off { - --fa: "\f011"; -} - -.fa-signal { - --fa: "\f012"; -} - -.fa-signal-5 { - --fa: "\f012"; -} - -.fa-signal-perfect { - --fa: "\f012"; -} - -.fa-gear { - --fa: "\f013"; -} - -.fa-cog { - --fa: "\f013"; -} - -.fa-house { - --fa: "\f015"; -} - -.fa-home { - --fa: "\f015"; -} - -.fa-home-alt { - --fa: "\f015"; -} - -.fa-home-lg-alt { - --fa: "\f015"; -} - -.fa-clock { - --fa: "\f017"; -} - -.fa-clock-four { - --fa: "\f017"; -} - -.fa-road { - --fa: "\f018"; -} - -.fa-download { - --fa: "\f019"; -} - -.fa-inbox { - --fa: "\f01c"; -} - -.fa-arrow-rotate-right { - --fa: "\f01e"; -} - -.fa-arrow-right-rotate { - --fa: "\f01e"; -} - -.fa-arrow-rotate-forward { - --fa: "\f01e"; -} - -.fa-redo { - --fa: "\f01e"; -} - -.fa-arrows-rotate { - --fa: "\f021"; -} - -.fa-refresh { - --fa: "\f021"; -} - -.fa-sync { - --fa: "\f021"; -} - -.fa-rectangle-list { - --fa: "\f022"; -} - -.fa-list-alt { - --fa: "\f022"; -} - -.fa-lock { - --fa: "\f023"; -} - -.fa-flag { - --fa: "\f024"; -} - -.fa-headphones { - --fa: "\f025"; -} - -.fa-headphones-alt { - --fa: "\f025"; -} - -.fa-headphones-simple { - --fa: "\f025"; -} - -.fa-volume-off { - --fa: "\f026"; -} - -.fa-volume-low { - --fa: "\f027"; -} - -.fa-volume-down { - --fa: "\f027"; -} - -.fa-volume-high { - --fa: "\f028"; -} - -.fa-volume-up { - --fa: "\f028"; -} - -.fa-qrcode { - --fa: "\f029"; -} - -.fa-barcode { - --fa: "\f02a"; -} - -.fa-tag { - --fa: "\f02b"; -} - -.fa-tags { - --fa: "\f02c"; -} - -.fa-book { - --fa: "\f02d"; -} - -.fa-bookmark { - --fa: "\f02e"; -} - -.fa-print { - --fa: "\f02f"; -} - -.fa-camera { - --fa: "\f030"; -} - -.fa-camera-alt { - --fa: "\f030"; -} - -.fa-font { - --fa: "\f031"; -} - -.fa-bold { - --fa: "\f032"; -} - -.fa-italic { - --fa: "\f033"; -} - -.fa-text-height { - --fa: "\f034"; -} - -.fa-text-width { - --fa: "\f035"; -} - -.fa-align-left { - --fa: "\f036"; -} - -.fa-align-center { - --fa: "\f037"; -} - -.fa-align-right { - --fa: "\f038"; -} - -.fa-align-justify { - --fa: "\f039"; -} - -.fa-list { - --fa: "\f03a"; -} - -.fa-list-squares { - --fa: "\f03a"; -} - -.fa-outdent { - --fa: "\f03b"; -} - -.fa-dedent { - --fa: "\f03b"; -} - -.fa-indent { - --fa: "\f03c"; -} - -.fa-video { - --fa: "\f03d"; -} - -.fa-video-camera { - --fa: "\f03d"; -} - -.fa-image { - --fa: "\f03e"; -} - -.fa-location-pin { - --fa: "\f041"; -} - -.fa-map-marker { - --fa: "\f041"; -} - -.fa-circle-half-stroke { - --fa: "\f042"; -} - -.fa-adjust { - --fa: "\f042"; -} - -.fa-droplet { - --fa: "\f043"; -} - -.fa-tint { - --fa: "\f043"; -} - -.fa-pen-to-square { - --fa: "\f044"; -} - -.fa-edit { - --fa: "\f044"; -} - -.fa-arrows-up-down-left-right { - --fa: "\f047"; -} - -.fa-arrows { - --fa: "\f047"; -} - -.fa-backward-step { - --fa: "\f048"; -} - -.fa-step-backward { - --fa: "\f048"; -} - -.fa-backward-fast { - --fa: "\f049"; -} - -.fa-fast-backward { - --fa: "\f049"; -} - -.fa-backward { - --fa: "\f04a"; -} - -.fa-play { - --fa: "\f04b"; -} - -.fa-pause { - --fa: "\f04c"; -} - -.fa-stop { - --fa: "\f04d"; -} - -.fa-forward { - --fa: "\f04e"; -} - -.fa-forward-fast { - --fa: "\f050"; -} - -.fa-fast-forward { - --fa: "\f050"; -} - -.fa-forward-step { - --fa: "\f051"; -} - -.fa-step-forward { - --fa: "\f051"; -} - -.fa-eject { - --fa: "\f052"; -} - -.fa-chevron-left { - --fa: "\f053"; -} - -.fa-chevron-right { - --fa: "\f054"; -} - -.fa-circle-plus { - --fa: "\f055"; -} - -.fa-plus-circle { - --fa: "\f055"; -} - -.fa-circle-minus { - --fa: "\f056"; -} - -.fa-minus-circle { - --fa: "\f056"; -} - -.fa-circle-xmark { - --fa: "\f057"; -} - -.fa-times-circle { - --fa: "\f057"; -} - -.fa-xmark-circle { - --fa: "\f057"; -} - -.fa-circle-check { - --fa: "\f058"; -} - -.fa-check-circle { - --fa: "\f058"; -} - -.fa-circle-question { - --fa: "\f059"; -} - -.fa-question-circle { - --fa: "\f059"; -} - -.fa-circle-info { - --fa: "\f05a"; -} - -.fa-info-circle { - --fa: "\f05a"; -} - -.fa-crosshairs { - --fa: "\f05b"; -} - -.fa-ban { - --fa: "\f05e"; -} - -.fa-cancel { - --fa: "\f05e"; -} - -.fa-arrow-left { - --fa: "\f060"; -} - -.fa-arrow-right { - --fa: "\f061"; -} - -.fa-arrow-up { - --fa: "\f062"; -} - -.fa-arrow-down { - --fa: "\f063"; -} - -.fa-share { - --fa: "\f064"; -} - -.fa-mail-forward { - --fa: "\f064"; -} - -.fa-expand { - --fa: "\f065"; -} - -.fa-compress { - --fa: "\f066"; -} - -.fa-minus { - --fa: "\f068"; -} - -.fa-subtract { - --fa: "\f068"; -} - -.fa-circle-exclamation { - --fa: "\f06a"; -} - -.fa-exclamation-circle { - --fa: "\f06a"; -} - -.fa-gift { - --fa: "\f06b"; -} - -.fa-leaf { - --fa: "\f06c"; -} - -.fa-fire { - --fa: "\f06d"; -} - -.fa-eye { - --fa: "\f06e"; -} - -.fa-eye-slash { - --fa: "\f070"; -} - -.fa-triangle-exclamation { - --fa: "\f071"; -} - -.fa-exclamation-triangle { - --fa: "\f071"; -} - -.fa-warning { - --fa: "\f071"; -} - -.fa-plane { - --fa: "\f072"; -} - -.fa-calendar-days { - --fa: "\f073"; -} - -.fa-calendar-alt { - --fa: "\f073"; -} - -.fa-shuffle { - --fa: "\f074"; -} - -.fa-random { - --fa: "\f074"; -} - -.fa-comment { - --fa: "\f075"; -} - -.fa-magnet { - --fa: "\f076"; -} - -.fa-chevron-up { - --fa: "\f077"; -} - -.fa-chevron-down { - --fa: "\f078"; -} - -.fa-retweet { - --fa: "\f079"; -} - -.fa-cart-shopping { - --fa: "\f07a"; -} - -.fa-shopping-cart { - --fa: "\f07a"; -} - -.fa-folder { - --fa: "\f07b"; -} - -.fa-folder-blank { - --fa: "\f07b"; -} - -.fa-folder-open { - --fa: "\f07c"; -} - -.fa-arrows-up-down { - --fa: "\f07d"; -} - -.fa-arrows-v { - --fa: "\f07d"; -} - -.fa-arrows-left-right { - --fa: "\f07e"; -} - -.fa-arrows-h { - --fa: "\f07e"; -} - -.fa-chart-bar { - --fa: "\f080"; -} - -.fa-bar-chart { - --fa: "\f080"; -} - -.fa-camera-retro { - --fa: "\f083"; -} - -.fa-key { - --fa: "\f084"; -} - -.fa-gears { - --fa: "\f085"; -} - -.fa-cogs { - --fa: "\f085"; -} - -.fa-comments { - --fa: "\f086"; -} - -.fa-star-half { - --fa: "\f089"; -} - -.fa-arrow-right-from-bracket { - --fa: "\f08b"; -} - -.fa-sign-out { - --fa: "\f08b"; -} - -.fa-thumbtack { - --fa: "\f08d"; -} - -.fa-thumb-tack { - --fa: "\f08d"; -} - -.fa-arrow-up-right-from-square { - --fa: "\f08e"; -} - -.fa-external-link { - --fa: "\f08e"; -} - -.fa-arrow-right-to-bracket { - --fa: "\f090"; -} - -.fa-sign-in { - --fa: "\f090"; -} - -.fa-trophy { - --fa: "\f091"; -} - -.fa-upload { - --fa: "\f093"; -} - -.fa-lemon { - --fa: "\f094"; -} - -.fa-phone { - --fa: "\f095"; -} - -.fa-square-phone { - --fa: "\f098"; -} - -.fa-phone-square { - --fa: "\f098"; -} - -.fa-unlock { - --fa: "\f09c"; -} - -.fa-credit-card { - --fa: "\f09d"; -} - -.fa-credit-card-alt { - --fa: "\f09d"; -} - -.fa-rss { - --fa: "\f09e"; -} - -.fa-feed { - --fa: "\f09e"; -} - -.fa-hard-drive { - --fa: "\f0a0"; -} - -.fa-hdd { - --fa: "\f0a0"; -} - -.fa-bullhorn { - --fa: "\f0a1"; -} - -.fa-certificate { - --fa: "\f0a3"; -} - -.fa-hand-point-right { - --fa: "\f0a4"; -} - -.fa-hand-point-left { - --fa: "\f0a5"; -} - -.fa-hand-point-up { - --fa: "\f0a6"; -} - -.fa-hand-point-down { - --fa: "\f0a7"; -} - -.fa-circle-arrow-left { - --fa: "\f0a8"; -} - -.fa-arrow-circle-left { - --fa: "\f0a8"; -} - -.fa-circle-arrow-right { - --fa: "\f0a9"; -} - -.fa-arrow-circle-right { - --fa: "\f0a9"; -} - -.fa-circle-arrow-up { - --fa: "\f0aa"; -} - -.fa-arrow-circle-up { - --fa: "\f0aa"; -} - -.fa-circle-arrow-down { - --fa: "\f0ab"; -} - -.fa-arrow-circle-down { - --fa: "\f0ab"; -} - -.fa-globe { - --fa: "\f0ac"; -} - -.fa-wrench { - --fa: "\f0ad"; -} - -.fa-list-check { - --fa: "\f0ae"; -} - -.fa-tasks { - --fa: "\f0ae"; -} - -.fa-filter { - --fa: "\f0b0"; -} - -.fa-briefcase { - --fa: "\f0b1"; -} - -.fa-up-down-left-right { - --fa: "\f0b2"; -} - -.fa-arrows-alt { - --fa: "\f0b2"; -} - -.fa-users { - --fa: "\f0c0"; -} - -.fa-link { - --fa: "\f0c1"; -} - -.fa-chain { - --fa: "\f0c1"; -} - -.fa-cloud { - --fa: "\f0c2"; -} - -.fa-flask { - --fa: "\f0c3"; -} - -.fa-scissors { - --fa: "\f0c4"; -} - -.fa-cut { - --fa: "\f0c4"; -} - -.fa-copy { - --fa: "\f0c5"; -} - -.fa-paperclip { - --fa: "\f0c6"; -} - -.fa-floppy-disk { - --fa: "\f0c7"; -} - -.fa-save { - --fa: "\f0c7"; -} - -.fa-square { - --fa: "\f0c8"; -} - -.fa-bars { - --fa: "\f0c9"; -} - -.fa-navicon { - --fa: "\f0c9"; -} - -.fa-list-ul { - --fa: "\f0ca"; -} - -.fa-list-dots { - --fa: "\f0ca"; -} - -.fa-list-ol { - --fa: "\f0cb"; -} - -.fa-list-1-2 { - --fa: "\f0cb"; -} - -.fa-list-numeric { - --fa: "\f0cb"; -} - -.fa-strikethrough { - --fa: "\f0cc"; -} - -.fa-underline { - --fa: "\f0cd"; -} - -.fa-table { - --fa: "\f0ce"; -} - -.fa-wand-magic { - --fa: "\f0d0"; -} - -.fa-magic { - --fa: "\f0d0"; -} - -.fa-truck { - --fa: "\f0d1"; -} - -.fa-money-bill { - --fa: "\f0d6"; -} - -.fa-caret-down { - --fa: "\f0d7"; -} - -.fa-caret-up { - --fa: "\f0d8"; -} - -.fa-caret-left { - --fa: "\f0d9"; -} - -.fa-caret-right { - --fa: "\f0da"; -} - -.fa-table-columns { - --fa: "\f0db"; -} - -.fa-columns { - --fa: "\f0db"; -} - -.fa-sort { - --fa: "\f0dc"; -} - -.fa-unsorted { - --fa: "\f0dc"; -} - -.fa-sort-down { - --fa: "\f0dd"; -} - -.fa-sort-desc { - --fa: "\f0dd"; -} - -.fa-sort-up { - --fa: "\f0de"; -} - -.fa-sort-asc { - --fa: "\f0de"; -} - -.fa-envelope { - --fa: "\f0e0"; -} - -.fa-arrow-rotate-left { - --fa: "\f0e2"; -} - -.fa-arrow-left-rotate { - --fa: "\f0e2"; -} - -.fa-arrow-rotate-back { - --fa: "\f0e2"; -} - -.fa-arrow-rotate-backward { - --fa: "\f0e2"; -} - -.fa-undo { - --fa: "\f0e2"; -} - -.fa-gavel { - --fa: "\f0e3"; -} - -.fa-legal { - --fa: "\f0e3"; -} - -.fa-bolt { - --fa: "\f0e7"; -} - -.fa-zap { - --fa: "\f0e7"; -} - -.fa-sitemap { - --fa: "\f0e8"; -} - -.fa-umbrella { - --fa: "\f0e9"; -} - -.fa-paste { - --fa: "\f0ea"; -} - -.fa-file-clipboard { - --fa: "\f0ea"; -} - -.fa-lightbulb { - --fa: "\f0eb"; -} - -.fa-arrow-right-arrow-left { - --fa: "\f0ec"; -} - -.fa-exchange { - --fa: "\f0ec"; -} - -.fa-cloud-arrow-down { - --fa: "\f0ed"; -} - -.fa-cloud-download { - --fa: "\f0ed"; -} - -.fa-cloud-download-alt { - --fa: "\f0ed"; -} - -.fa-cloud-arrow-up { - --fa: "\f0ee"; -} - -.fa-cloud-upload { - --fa: "\f0ee"; -} - -.fa-cloud-upload-alt { - --fa: "\f0ee"; -} - -.fa-user-doctor { - --fa: "\f0f0"; -} - -.fa-user-md { - --fa: "\f0f0"; -} - -.fa-stethoscope { - --fa: "\f0f1"; -} - -.fa-suitcase { - --fa: "\f0f2"; -} - -.fa-bell { - --fa: "\f0f3"; -} - -.fa-mug-saucer { - --fa: "\f0f4"; -} - -.fa-coffee { - --fa: "\f0f4"; -} - -.fa-hospital { - --fa: "\f0f8"; -} - -.fa-hospital-alt { - --fa: "\f0f8"; -} - -.fa-hospital-wide { - --fa: "\f0f8"; -} - -.fa-truck-medical { - --fa: "\f0f9"; -} - -.fa-ambulance { - --fa: "\f0f9"; -} - -.fa-suitcase-medical { - --fa: "\f0fa"; -} - -.fa-medkit { - --fa: "\f0fa"; -} - -.fa-jet-fighter { - --fa: "\f0fb"; -} - -.fa-fighter-jet { - --fa: "\f0fb"; -} - -.fa-beer-mug-empty { - --fa: "\f0fc"; -} - -.fa-beer { - --fa: "\f0fc"; -} - -.fa-square-h { - --fa: "\f0fd"; -} - -.fa-h-square { - --fa: "\f0fd"; -} - -.fa-square-plus { - --fa: "\f0fe"; -} - -.fa-plus-square { - --fa: "\f0fe"; -} - -.fa-angles-left { - --fa: "\f100"; -} - -.fa-angle-double-left { - --fa: "\f100"; -} - -.fa-angles-right { - --fa: "\f101"; -} - -.fa-angle-double-right { - --fa: "\f101"; -} - -.fa-angles-up { - --fa: "\f102"; -} - -.fa-angle-double-up { - --fa: "\f102"; -} - -.fa-angles-down { - --fa: "\f103"; -} - -.fa-angle-double-down { - --fa: "\f103"; -} - -.fa-angle-left { - --fa: "\f104"; -} - -.fa-angle-right { - --fa: "\f105"; -} - -.fa-angle-up { - --fa: "\f106"; -} - -.fa-angle-down { - --fa: "\f107"; -} - -.fa-laptop { - --fa: "\f109"; -} - -.fa-tablet-button { - --fa: "\f10a"; -} - -.fa-mobile-button { - --fa: "\f10b"; -} - -.fa-quote-left { - --fa: "\f10d"; -} - -.fa-quote-left-alt { - --fa: "\f10d"; -} - -.fa-quote-right { - --fa: "\f10e"; -} - -.fa-quote-right-alt { - --fa: "\f10e"; -} - -.fa-spinner { - --fa: "\f110"; -} - -.fa-circle { - --fa: "\f111"; -} - -.fa-face-smile { - --fa: "\f118"; -} - -.fa-smile { - --fa: "\f118"; -} - -.fa-face-frown { - --fa: "\f119"; -} - -.fa-frown { - --fa: "\f119"; -} - -.fa-face-meh { - --fa: "\f11a"; -} - -.fa-meh { - --fa: "\f11a"; -} - -.fa-gamepad { - --fa: "\f11b"; -} - -.fa-keyboard { - --fa: "\f11c"; -} - -.fa-flag-checkered { - --fa: "\f11e"; -} - -.fa-terminal { - --fa: "\f120"; -} - -.fa-code { - --fa: "\f121"; -} - -.fa-reply-all { - --fa: "\f122"; -} - -.fa-mail-reply-all { - --fa: "\f122"; -} - -.fa-location-arrow { - --fa: "\f124"; -} - -.fa-crop { - --fa: "\f125"; -} - -.fa-code-branch { - --fa: "\f126"; -} - -.fa-link-slash { - --fa: "\f127"; -} - -.fa-chain-broken { - --fa: "\f127"; -} - -.fa-chain-slash { - --fa: "\f127"; -} - -.fa-unlink { - --fa: "\f127"; -} - -.fa-info { - --fa: "\f129"; -} - -.fa-superscript { - --fa: "\f12b"; -} - -.fa-subscript { - --fa: "\f12c"; -} - -.fa-eraser { - --fa: "\f12d"; -} - -.fa-puzzle-piece { - --fa: "\f12e"; -} - -.fa-microphone { - --fa: "\f130"; -} - -.fa-microphone-slash { - --fa: "\f131"; -} - -.fa-shield { - --fa: "\f132"; -} - -.fa-shield-blank { - --fa: "\f132"; -} - -.fa-calendar { - --fa: "\f133"; -} - -.fa-fire-extinguisher { - --fa: "\f134"; -} - -.fa-rocket { - --fa: "\f135"; -} - -.fa-circle-chevron-left { - --fa: "\f137"; -} - -.fa-chevron-circle-left { - --fa: "\f137"; -} - -.fa-circle-chevron-right { - --fa: "\f138"; -} - -.fa-chevron-circle-right { - --fa: "\f138"; -} - -.fa-circle-chevron-up { - --fa: "\f139"; -} - -.fa-chevron-circle-up { - --fa: "\f139"; -} - -.fa-circle-chevron-down { - --fa: "\f13a"; -} - -.fa-chevron-circle-down { - --fa: "\f13a"; -} - -.fa-anchor { - --fa: "\f13d"; -} - -.fa-unlock-keyhole { - --fa: "\f13e"; -} - -.fa-unlock-alt { - --fa: "\f13e"; -} - -.fa-bullseye { - --fa: "\f140"; -} - -.fa-ellipsis { - --fa: "\f141"; -} - -.fa-ellipsis-h { - --fa: "\f141"; -} - -.fa-ellipsis-vertical { - --fa: "\f142"; -} - -.fa-ellipsis-v { - --fa: "\f142"; -} - -.fa-square-rss { - --fa: "\f143"; -} - -.fa-rss-square { - --fa: "\f143"; -} - -.fa-circle-play { - --fa: "\f144"; -} - -.fa-play-circle { - --fa: "\f144"; -} - -.fa-ticket { - --fa: "\f145"; -} - -.fa-square-minus { - --fa: "\f146"; -} - -.fa-minus-square { - --fa: "\f146"; -} - -.fa-arrow-turn-up { - --fa: "\f148"; -} - -.fa-level-up { - --fa: "\f148"; -} - -.fa-arrow-turn-down { - --fa: "\f149"; -} - -.fa-level-down { - --fa: "\f149"; -} - -.fa-square-check { - --fa: "\f14a"; -} - -.fa-check-square { - --fa: "\f14a"; -} - -.fa-square-pen { - --fa: "\f14b"; -} - -.fa-pen-square { - --fa: "\f14b"; -} - -.fa-pencil-square { - --fa: "\f14b"; -} - -.fa-square-arrow-up-right { - --fa: "\f14c"; -} - -.fa-external-link-square { - --fa: "\f14c"; -} - -.fa-share-from-square { - --fa: "\f14d"; -} - -.fa-share-square { - --fa: "\f14d"; -} - -.fa-compass { - --fa: "\f14e"; -} - -.fa-square-caret-down { - --fa: "\f150"; -} - -.fa-caret-square-down { - --fa: "\f150"; -} - -.fa-square-caret-up { - --fa: "\f151"; -} - -.fa-caret-square-up { - --fa: "\f151"; -} - -.fa-square-caret-right { - --fa: "\f152"; -} - -.fa-caret-square-right { - --fa: "\f152"; -} - -.fa-euro-sign { - --fa: "\f153"; -} - -.fa-eur { - --fa: "\f153"; -} - -.fa-euro { - --fa: "\f153"; -} - -.fa-sterling-sign { - --fa: "\f154"; -} - -.fa-gbp { - --fa: "\f154"; -} - -.fa-pound-sign { - --fa: "\f154"; -} - -.fa-rupee-sign { - --fa: "\f156"; -} - -.fa-rupee { - --fa: "\f156"; -} - -.fa-yen-sign { - --fa: "\f157"; -} - -.fa-cny { - --fa: "\f157"; -} - -.fa-jpy { - --fa: "\f157"; -} - -.fa-rmb { - --fa: "\f157"; -} - -.fa-yen { - --fa: "\f157"; -} - -.fa-ruble-sign { - --fa: "\f158"; -} - -.fa-rouble { - --fa: "\f158"; -} - -.fa-rub { - --fa: "\f158"; -} - -.fa-ruble { - --fa: "\f158"; -} - -.fa-won-sign { - --fa: "\f159"; -} - -.fa-krw { - --fa: "\f159"; -} - -.fa-won { - --fa: "\f159"; -} - -.fa-file { - --fa: "\f15b"; -} - -.fa-file-lines { - --fa: "\f15c"; -} - -.fa-file-alt { - --fa: "\f15c"; -} - -.fa-file-text { - --fa: "\f15c"; -} - -.fa-arrow-down-a-z { - --fa: "\f15d"; -} - -.fa-sort-alpha-asc { - --fa: "\f15d"; -} - -.fa-sort-alpha-down { - --fa: "\f15d"; -} - -.fa-arrow-up-a-z { - --fa: "\f15e"; -} - -.fa-sort-alpha-up { - --fa: "\f15e"; -} - -.fa-arrow-down-wide-short { - --fa: "\f160"; -} - -.fa-sort-amount-asc { - --fa: "\f160"; -} - -.fa-sort-amount-down { - --fa: "\f160"; -} - -.fa-arrow-up-wide-short { - --fa: "\f161"; -} - -.fa-sort-amount-up { - --fa: "\f161"; -} - -.fa-arrow-down-1-9 { - --fa: "\f162"; -} - -.fa-sort-numeric-asc { - --fa: "\f162"; -} - -.fa-sort-numeric-down { - --fa: "\f162"; -} - -.fa-arrow-up-1-9 { - --fa: "\f163"; -} - -.fa-sort-numeric-up { - --fa: "\f163"; -} - -.fa-thumbs-up { - --fa: "\f164"; -} - -.fa-thumbs-down { - --fa: "\f165"; -} - -.fa-arrow-down-long { - --fa: "\f175"; -} - -.fa-long-arrow-down { - --fa: "\f175"; -} - -.fa-arrow-up-long { - --fa: "\f176"; -} - -.fa-long-arrow-up { - --fa: "\f176"; -} - -.fa-arrow-left-long { - --fa: "\f177"; -} - -.fa-long-arrow-left { - --fa: "\f177"; -} - -.fa-arrow-right-long { - --fa: "\f178"; -} - -.fa-long-arrow-right { - --fa: "\f178"; -} - -.fa-person-dress { - --fa: "\f182"; -} - -.fa-female { - --fa: "\f182"; -} - -.fa-person { - --fa: "\f183"; -} - -.fa-male { - --fa: "\f183"; -} - -.fa-sun { - --fa: "\f185"; -} - -.fa-moon { - --fa: "\f186"; -} - -.fa-box-archive { - --fa: "\f187"; -} - -.fa-archive { - --fa: "\f187"; -} - -.fa-bug { - --fa: "\f188"; -} - -.fa-square-caret-left { - --fa: "\f191"; -} - -.fa-caret-square-left { - --fa: "\f191"; -} - -.fa-circle-dot { - --fa: "\f192"; -} - -.fa-dot-circle { - --fa: "\f192"; -} - -.fa-wheelchair { - --fa: "\f193"; -} - -.fa-lira-sign { - --fa: "\f195"; -} - -.fa-shuttle-space { - --fa: "\f197"; -} - -.fa-space-shuttle { - --fa: "\f197"; -} - -.fa-square-envelope { - --fa: "\f199"; -} - -.fa-envelope-square { - --fa: "\f199"; -} - -.fa-building-columns { - --fa: "\f19c"; -} - -.fa-bank { - --fa: "\f19c"; -} - -.fa-institution { - --fa: "\f19c"; -} - -.fa-museum { - --fa: "\f19c"; -} - -.fa-university { - --fa: "\f19c"; -} - -.fa-graduation-cap { - --fa: "\f19d"; -} - -.fa-mortar-board { - --fa: "\f19d"; -} - -.fa-language { - --fa: "\f1ab"; -} - -.fa-fax { - --fa: "\f1ac"; -} - -.fa-building { - --fa: "\f1ad"; -} - -.fa-child { - --fa: "\f1ae"; -} - -.fa-paw { - --fa: "\f1b0"; -} - -.fa-cube { - --fa: "\f1b2"; -} - -.fa-cubes { - --fa: "\f1b3"; -} - -.fa-recycle { - --fa: "\f1b8"; -} - -.fa-car { - --fa: "\f1b9"; -} - -.fa-automobile { - --fa: "\f1b9"; -} - -.fa-taxi { - --fa: "\f1ba"; -} - -.fa-cab { - --fa: "\f1ba"; -} - -.fa-tree { - --fa: "\f1bb"; -} - -.fa-database { - --fa: "\f1c0"; -} - -.fa-file-pdf { - --fa: "\f1c1"; -} - -.fa-file-word { - --fa: "\f1c2"; -} - -.fa-file-excel { - --fa: "\f1c3"; -} - -.fa-file-powerpoint { - --fa: "\f1c4"; -} - -.fa-file-image { - --fa: "\f1c5"; -} - -.fa-file-zipper { - --fa: "\f1c6"; -} - -.fa-file-archive { - --fa: "\f1c6"; -} - -.fa-file-audio { - --fa: "\f1c7"; -} - -.fa-file-video { - --fa: "\f1c8"; -} - -.fa-file-code { - --fa: "\f1c9"; -} - -.fa-life-ring { - --fa: "\f1cd"; -} - -.fa-circle-notch { - --fa: "\f1ce"; -} - -.fa-paper-plane { - --fa: "\f1d8"; -} - -.fa-clock-rotate-left { - --fa: "\f1da"; -} - -.fa-history { - --fa: "\f1da"; -} - -.fa-heading { - --fa: "\f1dc"; -} - -.fa-header { - --fa: "\f1dc"; -} - -.fa-paragraph { - --fa: "\f1dd"; -} - -.fa-sliders { - --fa: "\f1de"; -} - -.fa-sliders-h { - --fa: "\f1de"; -} - -.fa-share-nodes { - --fa: "\f1e0"; -} - -.fa-share-alt { - --fa: "\f1e0"; -} - -.fa-square-share-nodes { - --fa: "\f1e1"; -} - -.fa-share-alt-square { - --fa: "\f1e1"; -} - -.fa-bomb { - --fa: "\f1e2"; -} - -.fa-futbol { - --fa: "\f1e3"; -} - -.fa-futbol-ball { - --fa: "\f1e3"; -} - -.fa-soccer-ball { - --fa: "\f1e3"; -} - -.fa-tty { - --fa: "\f1e4"; -} - -.fa-teletype { - --fa: "\f1e4"; -} - -.fa-binoculars { - --fa: "\f1e5"; -} - -.fa-plug { - --fa: "\f1e6"; -} - -.fa-newspaper { - --fa: "\f1ea"; -} - -.fa-wifi { - --fa: "\f1eb"; -} - -.fa-wifi-3 { - --fa: "\f1eb"; -} - -.fa-wifi-strong { - --fa: "\f1eb"; -} - -.fa-calculator { - --fa: "\f1ec"; -} - -.fa-bell-slash { - --fa: "\f1f6"; -} - -.fa-trash { - --fa: "\f1f8"; -} - -.fa-copyright { - --fa: "\f1f9"; -} - -.fa-eye-dropper { - --fa: "\f1fb"; -} - -.fa-eye-dropper-empty { - --fa: "\f1fb"; -} - -.fa-eyedropper { - --fa: "\f1fb"; -} - -.fa-paintbrush { - --fa: "\f1fc"; -} - -.fa-paint-brush { - --fa: "\f1fc"; -} - -.fa-cake-candles { - --fa: "\f1fd"; -} - -.fa-birthday-cake { - --fa: "\f1fd"; -} - -.fa-cake { - --fa: "\f1fd"; -} - -.fa-chart-area { - --fa: "\f1fe"; -} - -.fa-area-chart { - --fa: "\f1fe"; -} - -.fa-chart-pie { - --fa: "\f200"; -} - -.fa-pie-chart { - --fa: "\f200"; -} - -.fa-chart-line { - --fa: "\f201"; -} - -.fa-line-chart { - --fa: "\f201"; -} - -.fa-toggle-off { - --fa: "\f204"; -} - -.fa-toggle-on { - --fa: "\f205"; -} - -.fa-bicycle { - --fa: "\f206"; -} - -.fa-bus { - --fa: "\f207"; -} - -.fa-closed-captioning { - --fa: "\f20a"; -} - -.fa-shekel-sign { - --fa: "\f20b"; -} - -.fa-ils { - --fa: "\f20b"; -} - -.fa-shekel { - --fa: "\f20b"; -} - -.fa-sheqel { - --fa: "\f20b"; -} - -.fa-sheqel-sign { - --fa: "\f20b"; -} - -.fa-cart-plus { - --fa: "\f217"; -} - -.fa-cart-arrow-down { - --fa: "\f218"; -} - -.fa-diamond { - --fa: "\f219"; -} - -.fa-ship { - --fa: "\f21a"; -} - -.fa-user-secret { - --fa: "\f21b"; -} - -.fa-motorcycle { - --fa: "\f21c"; -} - -.fa-street-view { - --fa: "\f21d"; -} - -.fa-heart-pulse { - --fa: "\f21e"; -} - -.fa-heartbeat { - --fa: "\f21e"; -} - -.fa-venus { - --fa: "\f221"; -} - -.fa-mars { - --fa: "\f222"; -} - -.fa-mercury { - --fa: "\f223"; -} - -.fa-mars-and-venus { - --fa: "\f224"; -} - -.fa-transgender { - --fa: "\f225"; -} - -.fa-transgender-alt { - --fa: "\f225"; -} - -.fa-venus-double { - --fa: "\f226"; -} - -.fa-mars-double { - --fa: "\f227"; -} - -.fa-venus-mars { - --fa: "\f228"; -} - -.fa-mars-stroke { - --fa: "\f229"; -} - -.fa-mars-stroke-up { - --fa: "\f22a"; -} - -.fa-mars-stroke-v { - --fa: "\f22a"; -} - -.fa-mars-stroke-right { - --fa: "\f22b"; -} - -.fa-mars-stroke-h { - --fa: "\f22b"; -} - -.fa-neuter { - --fa: "\f22c"; -} - -.fa-genderless { - --fa: "\f22d"; -} - -.fa-server { - --fa: "\f233"; -} - -.fa-user-plus { - --fa: "\f234"; -} - -.fa-user-xmark { - --fa: "\f235"; -} - -.fa-user-times { - --fa: "\f235"; -} - -.fa-bed { - --fa: "\f236"; -} - -.fa-train { - --fa: "\f238"; -} - -.fa-train-subway { - --fa: "\f239"; -} - -.fa-subway { - --fa: "\f239"; -} - -.fa-battery-full { - --fa: "\f240"; -} - -.fa-battery { - --fa: "\f240"; -} - -.fa-battery-5 { - --fa: "\f240"; -} - -.fa-battery-three-quarters { - --fa: "\f241"; -} - -.fa-battery-4 { - --fa: "\f241"; -} - -.fa-battery-half { - --fa: "\f242"; -} - -.fa-battery-3 { - --fa: "\f242"; -} - -.fa-battery-quarter { - --fa: "\f243"; -} - -.fa-battery-2 { - --fa: "\f243"; -} - -.fa-battery-empty { - --fa: "\f244"; -} - -.fa-battery-0 { - --fa: "\f244"; -} - -.fa-arrow-pointer { - --fa: "\f245"; -} - -.fa-mouse-pointer { - --fa: "\f245"; -} - -.fa-i-cursor { - --fa: "\f246"; -} - -.fa-object-group { - --fa: "\f247"; -} - -.fa-object-ungroup { - --fa: "\f248"; -} - -.fa-note-sticky { - --fa: "\f249"; -} - -.fa-sticky-note { - --fa: "\f249"; -} - -.fa-clone { - --fa: "\f24d"; -} - -.fa-scale-balanced { - --fa: "\f24e"; -} - -.fa-balance-scale { - --fa: "\f24e"; -} - -.fa-hourglass-start { - --fa: "\f251"; -} - -.fa-hourglass-1 { - --fa: "\f251"; -} - -.fa-hourglass-half { - --fa: "\f252"; -} - -.fa-hourglass-2 { - --fa: "\f252"; -} - -.fa-hourglass-end { - --fa: "\f253"; -} - -.fa-hourglass-3 { - --fa: "\f253"; -} - -.fa-hourglass { - --fa: "\f254"; -} - -.fa-hourglass-empty { - --fa: "\f254"; -} - -.fa-hand-back-fist { - --fa: "\f255"; -} - -.fa-hand-rock { - --fa: "\f255"; -} - -.fa-hand { - --fa: "\f256"; -} - -.fa-hand-paper { - --fa: "\f256"; -} - -.fa-hand-scissors { - --fa: "\f257"; -} - -.fa-hand-lizard { - --fa: "\f258"; -} - -.fa-hand-spock { - --fa: "\f259"; -} - -.fa-hand-pointer { - --fa: "\f25a"; -} - -.fa-hand-peace { - --fa: "\f25b"; -} - -.fa-trademark { - --fa: "\f25c"; -} - -.fa-registered { - --fa: "\f25d"; -} - -.fa-tv { - --fa: "\f26c"; -} - -.fa-television { - --fa: "\f26c"; -} - -.fa-tv-alt { - --fa: "\f26c"; -} - -.fa-calendar-plus { - --fa: "\f271"; -} - -.fa-calendar-minus { - --fa: "\f272"; -} - -.fa-calendar-xmark { - --fa: "\f273"; -} - -.fa-calendar-times { - --fa: "\f273"; -} - -.fa-calendar-check { - --fa: "\f274"; -} - -.fa-industry { - --fa: "\f275"; -} - -.fa-map-pin { - --fa: "\f276"; -} - -.fa-signs-post { - --fa: "\f277"; -} - -.fa-map-signs { - --fa: "\f277"; -} - -.fa-map { - --fa: "\f279"; -} - -.fa-message { - --fa: "\f27a"; -} - -.fa-comment-alt { - --fa: "\f27a"; -} - -.fa-circle-pause { - --fa: "\f28b"; -} - -.fa-pause-circle { - --fa: "\f28b"; -} - -.fa-circle-stop { - --fa: "\f28d"; -} - -.fa-stop-circle { - --fa: "\f28d"; -} - -.fa-bag-shopping { - --fa: "\f290"; -} - -.fa-shopping-bag { - --fa: "\f290"; -} - -.fa-basket-shopping { - --fa: "\f291"; -} - -.fa-shopping-basket { - --fa: "\f291"; -} - -.fa-universal-access { - --fa: "\f29a"; -} - -.fa-person-walking-with-cane { - --fa: "\f29d"; -} - -.fa-blind { - --fa: "\f29d"; -} - -.fa-audio-description { - --fa: "\f29e"; -} - -.fa-phone-volume { - --fa: "\f2a0"; -} - -.fa-volume-control-phone { - --fa: "\f2a0"; -} - -.fa-braille { - --fa: "\f2a1"; -} - -.fa-ear-listen { - --fa: "\f2a2"; -} - -.fa-assistive-listening-systems { - --fa: "\f2a2"; -} - -.fa-hands-asl-interpreting { - --fa: "\f2a3"; -} - -.fa-american-sign-language-interpreting { - --fa: "\f2a3"; -} - -.fa-asl-interpreting { - --fa: "\f2a3"; -} - -.fa-hands-american-sign-language-interpreting { - --fa: "\f2a3"; -} - -.fa-ear-deaf { - --fa: "\f2a4"; -} - -.fa-deaf { - --fa: "\f2a4"; -} - -.fa-deafness { - --fa: "\f2a4"; -} - -.fa-hard-of-hearing { - --fa: "\f2a4"; -} - -.fa-hands { - --fa: "\f2a7"; -} - -.fa-sign-language { - --fa: "\f2a7"; -} - -.fa-signing { - --fa: "\f2a7"; -} - -.fa-eye-low-vision { - --fa: "\f2a8"; -} - -.fa-low-vision { - --fa: "\f2a8"; -} - -.fa-font-awesome { - --fa: "\f2b4"; -} - -.fa-font-awesome-flag { - --fa: "\f2b4"; -} - -.fa-font-awesome-logo-full { - --fa: "\f2b4"; -} - -.fa-handshake { - --fa: "\f2b5"; -} - -.fa-handshake-alt { - --fa: "\f2b5"; -} - -.fa-handshake-simple { - --fa: "\f2b5"; -} - -.fa-envelope-open { - --fa: "\f2b6"; -} - -.fa-address-book { - --fa: "\f2b9"; -} - -.fa-contact-book { - --fa: "\f2b9"; -} - -.fa-address-card { - --fa: "\f2bb"; -} - -.fa-contact-card { - --fa: "\f2bb"; -} - -.fa-vcard { - --fa: "\f2bb"; -} - -.fa-circle-user { - --fa: "\f2bd"; -} - -.fa-user-circle { - --fa: "\f2bd"; -} - -.fa-id-badge { - --fa: "\f2c1"; -} - -.fa-id-card { - --fa: "\f2c2"; -} - -.fa-drivers-license { - --fa: "\f2c2"; -} - -.fa-temperature-full { - --fa: "\f2c7"; -} - -.fa-temperature-4 { - --fa: "\f2c7"; -} - -.fa-thermometer-4 { - --fa: "\f2c7"; -} - -.fa-thermometer-full { - --fa: "\f2c7"; -} - -.fa-temperature-three-quarters { - --fa: "\f2c8"; -} - -.fa-temperature-3 { - --fa: "\f2c8"; -} - -.fa-thermometer-3 { - --fa: "\f2c8"; -} - -.fa-thermometer-three-quarters { - --fa: "\f2c8"; -} - -.fa-temperature-half { - --fa: "\f2c9"; -} - -.fa-temperature-2 { - --fa: "\f2c9"; -} - -.fa-thermometer-2 { - --fa: "\f2c9"; -} - -.fa-thermometer-half { - --fa: "\f2c9"; -} - -.fa-temperature-quarter { - --fa: "\f2ca"; -} - -.fa-temperature-1 { - --fa: "\f2ca"; -} - -.fa-thermometer-1 { - --fa: "\f2ca"; -} - -.fa-thermometer-quarter { - --fa: "\f2ca"; -} - -.fa-temperature-empty { - --fa: "\f2cb"; -} - -.fa-temperature-0 { - --fa: "\f2cb"; -} - -.fa-thermometer-0 { - --fa: "\f2cb"; -} - -.fa-thermometer-empty { - --fa: "\f2cb"; -} - -.fa-shower { - --fa: "\f2cc"; -} - -.fa-bath { - --fa: "\f2cd"; -} - -.fa-bathtub { - --fa: "\f2cd"; -} - -.fa-podcast { - --fa: "\f2ce"; -} - -.fa-window-maximize { - --fa: "\f2d0"; -} - -.fa-window-minimize { - --fa: "\f2d1"; -} - -.fa-window-restore { - --fa: "\f2d2"; -} - -.fa-square-xmark { - --fa: "\f2d3"; -} - -.fa-times-square { - --fa: "\f2d3"; -} - -.fa-xmark-square { - --fa: "\f2d3"; -} - -.fa-microchip { - --fa: "\f2db"; -} - -.fa-snowflake { - --fa: "\f2dc"; -} - -.fa-spoon { - --fa: "\f2e5"; -} - -.fa-utensil-spoon { - --fa: "\f2e5"; -} - -.fa-utensils { - --fa: "\f2e7"; -} - -.fa-cutlery { - --fa: "\f2e7"; -} - -.fa-rotate-left { - --fa: "\f2ea"; -} - -.fa-rotate-back { - --fa: "\f2ea"; -} - -.fa-rotate-backward { - --fa: "\f2ea"; -} - -.fa-undo-alt { - --fa: "\f2ea"; -} - -.fa-trash-can { - --fa: "\f2ed"; -} - -.fa-trash-alt { - --fa: "\f2ed"; -} - -.fa-rotate { - --fa: "\f2f1"; -} - -.fa-sync-alt { - --fa: "\f2f1"; -} - -.fa-stopwatch { - --fa: "\f2f2"; -} - -.fa-right-from-bracket { - --fa: "\f2f5"; -} - -.fa-sign-out-alt { - --fa: "\f2f5"; -} - -.fa-right-to-bracket { - --fa: "\f2f6"; -} - -.fa-sign-in-alt { - --fa: "\f2f6"; -} - -.fa-rotate-right { - --fa: "\f2f9"; -} - -.fa-redo-alt { - --fa: "\f2f9"; -} - -.fa-rotate-forward { - --fa: "\f2f9"; -} - -.fa-poo { - --fa: "\f2fe"; -} - -.fa-images { - --fa: "\f302"; -} - -.fa-pencil { - --fa: "\f303"; -} - -.fa-pencil-alt { - --fa: "\f303"; -} - -.fa-pen { - --fa: "\f304"; -} - -.fa-pen-clip { - --fa: "\f305"; -} - -.fa-pen-alt { - --fa: "\f305"; -} - -.fa-octagon { - --fa: "\f306"; -} - -.fa-down-long { - --fa: "\f309"; -} - -.fa-long-arrow-alt-down { - --fa: "\f309"; -} - -.fa-left-long { - --fa: "\f30a"; -} - -.fa-long-arrow-alt-left { - --fa: "\f30a"; -} - -.fa-right-long { - --fa: "\f30b"; -} - -.fa-long-arrow-alt-right { - --fa: "\f30b"; -} - -.fa-up-long { - --fa: "\f30c"; -} - -.fa-long-arrow-alt-up { - --fa: "\f30c"; -} - -.fa-hexagon { - --fa: "\f312"; -} - -.fa-file-pen { - --fa: "\f31c"; -} - -.fa-file-edit { - --fa: "\f31c"; -} - -.fa-maximize { - --fa: "\f31e"; -} - -.fa-expand-arrows-alt { - --fa: "\f31e"; -} - -.fa-clipboard { - --fa: "\f328"; -} - -.fa-left-right { - --fa: "\f337"; -} - -.fa-arrows-alt-h { - --fa: "\f337"; -} - -.fa-up-down { - --fa: "\f338"; -} - -.fa-arrows-alt-v { - --fa: "\f338"; -} - -.fa-alarm-clock { - --fa: "\f34e"; -} - -.fa-circle-down { - --fa: "\f358"; -} - -.fa-arrow-alt-circle-down { - --fa: "\f358"; -} - -.fa-circle-left { - --fa: "\f359"; -} - -.fa-arrow-alt-circle-left { - --fa: "\f359"; -} - -.fa-circle-right { - --fa: "\f35a"; -} - -.fa-arrow-alt-circle-right { - --fa: "\f35a"; -} - -.fa-circle-up { - --fa: "\f35b"; -} - -.fa-arrow-alt-circle-up { - --fa: "\f35b"; -} - -.fa-up-right-from-square { - --fa: "\f35d"; -} - -.fa-external-link-alt { - --fa: "\f35d"; -} - -.fa-square-up-right { - --fa: "\f360"; -} - -.fa-external-link-square-alt { - --fa: "\f360"; -} - -.fa-right-left { - --fa: "\f362"; -} - -.fa-exchange-alt { - --fa: "\f362"; -} - -.fa-repeat { - --fa: "\f363"; -} - -.fa-code-commit { - --fa: "\f386"; -} - -.fa-code-merge { - --fa: "\f387"; -} - -.fa-desktop { - --fa: "\f390"; -} - -.fa-desktop-alt { - --fa: "\f390"; -} - -.fa-gem { - --fa: "\f3a5"; -} - -.fa-turn-down { - --fa: "\f3be"; -} - -.fa-level-down-alt { - --fa: "\f3be"; -} - -.fa-turn-up { - --fa: "\f3bf"; -} - -.fa-level-up-alt { - --fa: "\f3bf"; -} - -.fa-lock-open { - --fa: "\f3c1"; -} - -.fa-location-dot { - --fa: "\f3c5"; -} - -.fa-map-marker-alt { - --fa: "\f3c5"; -} - -.fa-microphone-lines { - --fa: "\f3c9"; -} - -.fa-microphone-alt { - --fa: "\f3c9"; -} - -.fa-mobile-screen-button { - --fa: "\f3cd"; -} - -.fa-mobile-alt { - --fa: "\f3cd"; -} - -.fa-mobile { - --fa: "\f3ce"; -} - -.fa-mobile-android { - --fa: "\f3ce"; -} - -.fa-mobile-phone { - --fa: "\f3ce"; -} - -.fa-mobile-screen { - --fa: "\f3cf"; -} - -.fa-mobile-android-alt { - --fa: "\f3cf"; -} - -.fa-money-bill-1 { - --fa: "\f3d1"; -} - -.fa-money-bill-alt { - --fa: "\f3d1"; -} - -.fa-phone-slash { - --fa: "\f3dd"; -} - -.fa-image-portrait { - --fa: "\f3e0"; -} - -.fa-portrait { - --fa: "\f3e0"; -} - -.fa-reply { - --fa: "\f3e5"; -} - -.fa-mail-reply { - --fa: "\f3e5"; -} - -.fa-shield-halved { - --fa: "\f3ed"; -} - -.fa-shield-alt { - --fa: "\f3ed"; -} - -.fa-tablet-screen-button { - --fa: "\f3fa"; -} - -.fa-tablet-alt { - --fa: "\f3fa"; -} - -.fa-tablet { - --fa: "\f3fb"; -} - -.fa-tablet-android { - --fa: "\f3fb"; -} - -.fa-ticket-simple { - --fa: "\f3ff"; -} - -.fa-ticket-alt { - --fa: "\f3ff"; -} - -.fa-rectangle-xmark { - --fa: "\f410"; -} - -.fa-rectangle-times { - --fa: "\f410"; -} - -.fa-times-rectangle { - --fa: "\f410"; -} - -.fa-window-close { - --fa: "\f410"; -} - -.fa-down-left-and-up-right-to-center { - --fa: "\f422"; -} - -.fa-compress-alt { - --fa: "\f422"; -} - -.fa-up-right-and-down-left-from-center { - --fa: "\f424"; -} - -.fa-expand-alt { - --fa: "\f424"; -} - -.fa-baseball-bat-ball { - --fa: "\f432"; -} - -.fa-baseball { - --fa: "\f433"; -} - -.fa-baseball-ball { - --fa: "\f433"; -} - -.fa-basketball { - --fa: "\f434"; -} - -.fa-basketball-ball { - --fa: "\f434"; -} - -.fa-bowling-ball { - --fa: "\f436"; -} - -.fa-chess { - --fa: "\f439"; -} - -.fa-chess-bishop { - --fa: "\f43a"; -} - -.fa-chess-board { - --fa: "\f43c"; -} - -.fa-chess-king { - --fa: "\f43f"; -} - -.fa-chess-knight { - --fa: "\f441"; -} - -.fa-chess-pawn { - --fa: "\f443"; -} - -.fa-chess-queen { - --fa: "\f445"; -} - -.fa-chess-rook { - --fa: "\f447"; -} - -.fa-dumbbell { - --fa: "\f44b"; -} - -.fa-football { - --fa: "\f44e"; -} - -.fa-football-ball { - --fa: "\f44e"; -} - -.fa-golf-ball-tee { - --fa: "\f450"; -} - -.fa-golf-ball { - --fa: "\f450"; -} - -.fa-hockey-puck { - --fa: "\f453"; -} - -.fa-broom-ball { - --fa: "\f458"; -} - -.fa-quidditch { - --fa: "\f458"; -} - -.fa-quidditch-broom-ball { - --fa: "\f458"; -} - -.fa-square-full { - --fa: "\f45c"; -} - -.fa-table-tennis-paddle-ball { - --fa: "\f45d"; -} - -.fa-ping-pong-paddle-ball { - --fa: "\f45d"; -} - -.fa-table-tennis { - --fa: "\f45d"; -} - -.fa-volleyball { - --fa: "\f45f"; -} - -.fa-volleyball-ball { - --fa: "\f45f"; -} - -.fa-hand-dots { - --fa: "\f461"; -} - -.fa-allergies { - --fa: "\f461"; -} - -.fa-bandage { - --fa: "\f462"; -} - -.fa-band-aid { - --fa: "\f462"; -} - -.fa-box { - --fa: "\f466"; -} - -.fa-boxes-stacked { - --fa: "\f468"; -} - -.fa-boxes { - --fa: "\f468"; -} - -.fa-boxes-alt { - --fa: "\f468"; -} - -.fa-briefcase-medical { - --fa: "\f469"; -} - -.fa-fire-flame-simple { - --fa: "\f46a"; -} - -.fa-burn { - --fa: "\f46a"; -} - -.fa-capsules { - --fa: "\f46b"; -} - -.fa-clipboard-check { - --fa: "\f46c"; -} - -.fa-clipboard-list { - --fa: "\f46d"; -} - -.fa-person-dots-from-line { - --fa: "\f470"; -} - -.fa-diagnoses { - --fa: "\f470"; -} - -.fa-dna { - --fa: "\f471"; -} - -.fa-dolly { - --fa: "\f472"; -} - -.fa-dolly-box { - --fa: "\f472"; -} - -.fa-cart-flatbed { - --fa: "\f474"; -} - -.fa-dolly-flatbed { - --fa: "\f474"; -} - -.fa-file-medical { - --fa: "\f477"; -} - -.fa-file-waveform { - --fa: "\f478"; -} - -.fa-file-medical-alt { - --fa: "\f478"; -} - -.fa-kit-medical { - --fa: "\f479"; -} - -.fa-first-aid { - --fa: "\f479"; -} - -.fa-circle-h { - --fa: "\f47e"; -} - -.fa-hospital-symbol { - --fa: "\f47e"; -} - -.fa-id-card-clip { - --fa: "\f47f"; -} - -.fa-id-card-alt { - --fa: "\f47f"; -} - -.fa-notes-medical { - --fa: "\f481"; -} - -.fa-pallet { - --fa: "\f482"; -} - -.fa-pills { - --fa: "\f484"; -} - -.fa-prescription-bottle { - --fa: "\f485"; -} - -.fa-prescription-bottle-medical { - --fa: "\f486"; -} - -.fa-prescription-bottle-alt { - --fa: "\f486"; -} - -.fa-bed-pulse { - --fa: "\f487"; -} - -.fa-procedures { - --fa: "\f487"; -} - -.fa-truck-fast { - --fa: "\f48b"; -} - -.fa-shipping-fast { - --fa: "\f48b"; -} - -.fa-smoking { - --fa: "\f48d"; -} - -.fa-syringe { - --fa: "\f48e"; -} - -.fa-tablets { - --fa: "\f490"; -} - -.fa-thermometer { - --fa: "\f491"; -} - -.fa-vial { - --fa: "\f492"; -} - -.fa-vials { - --fa: "\f493"; -} - -.fa-warehouse { - --fa: "\f494"; -} - -.fa-weight-scale { - --fa: "\f496"; -} - -.fa-weight { - --fa: "\f496"; -} - -.fa-x-ray { - --fa: "\f497"; -} - -.fa-box-open { - --fa: "\f49e"; -} - -.fa-comment-dots { - --fa: "\f4ad"; -} - -.fa-commenting { - --fa: "\f4ad"; -} - -.fa-comment-slash { - --fa: "\f4b3"; -} - -.fa-couch { - --fa: "\f4b8"; -} - -.fa-circle-dollar-to-slot { - --fa: "\f4b9"; -} - -.fa-donate { - --fa: "\f4b9"; -} - -.fa-dove { - --fa: "\f4ba"; -} - -.fa-hand-holding { - --fa: "\f4bd"; -} - -.fa-hand-holding-heart { - --fa: "\f4be"; -} - -.fa-hand-holding-dollar { - --fa: "\f4c0"; -} - -.fa-hand-holding-usd { - --fa: "\f4c0"; -} - -.fa-hand-holding-droplet { - --fa: "\f4c1"; -} - -.fa-hand-holding-water { - --fa: "\f4c1"; -} - -.fa-hands-holding { - --fa: "\f4c2"; -} - -.fa-handshake-angle { - --fa: "\f4c4"; -} - -.fa-hands-helping { - --fa: "\f4c4"; -} - -.fa-parachute-box { - --fa: "\f4cd"; -} - -.fa-people-carry-box { - --fa: "\f4ce"; -} - -.fa-people-carry { - --fa: "\f4ce"; -} - -.fa-piggy-bank { - --fa: "\f4d3"; -} - -.fa-ribbon { - --fa: "\f4d6"; -} - -.fa-route { - --fa: "\f4d7"; -} - -.fa-seedling { - --fa: "\f4d8"; -} - -.fa-sprout { - --fa: "\f4d8"; -} - -.fa-sign-hanging { - --fa: "\f4d9"; -} - -.fa-sign { - --fa: "\f4d9"; -} - -.fa-face-smile-wink { - --fa: "\f4da"; -} - -.fa-smile-wink { - --fa: "\f4da"; -} - -.fa-tape { - --fa: "\f4db"; -} - -.fa-truck-ramp-box { - --fa: "\f4de"; -} - -.fa-truck-loading { - --fa: "\f4de"; -} - -.fa-truck-moving { - --fa: "\f4df"; -} - -.fa-video-slash { - --fa: "\f4e2"; -} - -.fa-wine-glass { - --fa: "\f4e3"; -} - -.fa-user-astronaut { - --fa: "\f4fb"; -} - -.fa-user-check { - --fa: "\f4fc"; -} - -.fa-user-clock { - --fa: "\f4fd"; -} - -.fa-user-gear { - --fa: "\f4fe"; -} - -.fa-user-cog { - --fa: "\f4fe"; -} - -.fa-user-pen { - --fa: "\f4ff"; -} - -.fa-user-edit { - --fa: "\f4ff"; -} - -.fa-user-group { - --fa: "\f500"; -} - -.fa-user-friends { - --fa: "\f500"; -} - -.fa-user-graduate { - --fa: "\f501"; -} - -.fa-user-lock { - --fa: "\f502"; -} - -.fa-user-minus { - --fa: "\f503"; -} - -.fa-user-ninja { - --fa: "\f504"; -} - -.fa-user-shield { - --fa: "\f505"; -} - -.fa-user-slash { - --fa: "\f506"; -} - -.fa-user-alt-slash { - --fa: "\f506"; -} - -.fa-user-large-slash { - --fa: "\f506"; -} - -.fa-user-tag { - --fa: "\f507"; -} - -.fa-user-tie { - --fa: "\f508"; -} - -.fa-users-gear { - --fa: "\f509"; -} - -.fa-users-cog { - --fa: "\f509"; -} - -.fa-scale-unbalanced { - --fa: "\f515"; -} - -.fa-balance-scale-left { - --fa: "\f515"; -} - -.fa-scale-unbalanced-flip { - --fa: "\f516"; -} - -.fa-balance-scale-right { - --fa: "\f516"; -} - -.fa-blender { - --fa: "\f517"; -} - -.fa-book-open { - --fa: "\f518"; -} - -.fa-tower-broadcast { - --fa: "\f519"; -} - -.fa-broadcast-tower { - --fa: "\f519"; -} - -.fa-broom { - --fa: "\f51a"; -} - -.fa-chalkboard { - --fa: "\f51b"; -} - -.fa-blackboard { - --fa: "\f51b"; -} - -.fa-chalkboard-user { - --fa: "\f51c"; -} - -.fa-chalkboard-teacher { - --fa: "\f51c"; -} - -.fa-church { - --fa: "\f51d"; -} - -.fa-coins { - --fa: "\f51e"; -} - -.fa-compact-disc { - --fa: "\f51f"; -} - -.fa-crow { - --fa: "\f520"; -} - -.fa-crown { - --fa: "\f521"; -} - -.fa-dice { - --fa: "\f522"; -} - -.fa-dice-five { - --fa: "\f523"; -} - -.fa-dice-four { - --fa: "\f524"; -} - -.fa-dice-one { - --fa: "\f525"; -} - -.fa-dice-six { - --fa: "\f526"; -} - -.fa-dice-three { - --fa: "\f527"; -} - -.fa-dice-two { - --fa: "\f528"; -} - -.fa-divide { - --fa: "\f529"; -} - -.fa-door-closed { - --fa: "\f52a"; -} - -.fa-door-open { - --fa: "\f52b"; -} - -.fa-feather { - --fa: "\f52d"; -} - -.fa-frog { - --fa: "\f52e"; -} - -.fa-gas-pump { - --fa: "\f52f"; -} - -.fa-glasses { - --fa: "\f530"; -} - -.fa-greater-than-equal { - --fa: "\f532"; -} - -.fa-helicopter { - --fa: "\f533"; -} - -.fa-infinity { - --fa: "\f534"; -} - -.fa-kiwi-bird { - --fa: "\f535"; -} - -.fa-less-than-equal { - --fa: "\f537"; -} - -.fa-memory { - --fa: "\f538"; -} - -.fa-microphone-lines-slash { - --fa: "\f539"; -} - -.fa-microphone-alt-slash { - --fa: "\f539"; -} - -.fa-money-bill-wave { - --fa: "\f53a"; -} - -.fa-money-bill-1-wave { - --fa: "\f53b"; -} - -.fa-money-bill-wave-alt { - --fa: "\f53b"; -} - -.fa-money-check { - --fa: "\f53c"; -} - -.fa-money-check-dollar { - --fa: "\f53d"; -} - -.fa-money-check-alt { - --fa: "\f53d"; -} - -.fa-not-equal { - --fa: "\f53e"; -} - -.fa-palette { - --fa: "\f53f"; -} - -.fa-square-parking { - --fa: "\f540"; -} - -.fa-parking { - --fa: "\f540"; -} - -.fa-diagram-project { - --fa: "\f542"; -} - -.fa-project-diagram { - --fa: "\f542"; -} - -.fa-receipt { - --fa: "\f543"; -} - -.fa-robot { - --fa: "\f544"; -} - -.fa-ruler { - --fa: "\f545"; -} - -.fa-ruler-combined { - --fa: "\f546"; -} - -.fa-ruler-horizontal { - --fa: "\f547"; -} - -.fa-ruler-vertical { - --fa: "\f548"; -} - -.fa-school { - --fa: "\f549"; -} - -.fa-screwdriver { - --fa: "\f54a"; -} - -.fa-shoe-prints { - --fa: "\f54b"; -} - -.fa-skull { - --fa: "\f54c"; -} - -.fa-ban-smoking { - --fa: "\f54d"; -} - -.fa-smoking-ban { - --fa: "\f54d"; -} - -.fa-store { - --fa: "\f54e"; -} - -.fa-shop { - --fa: "\f54f"; -} - -.fa-store-alt { - --fa: "\f54f"; -} - -.fa-bars-staggered { - --fa: "\f550"; -} - -.fa-reorder { - --fa: "\f550"; -} - -.fa-stream { - --fa: "\f550"; -} - -.fa-stroopwafel { - --fa: "\f551"; -} - -.fa-toolbox { - --fa: "\f552"; -} - -.fa-shirt { - --fa: "\f553"; -} - -.fa-t-shirt { - --fa: "\f553"; -} - -.fa-tshirt { - --fa: "\f553"; -} - -.fa-person-walking { - --fa: "\f554"; -} - -.fa-walking { - --fa: "\f554"; -} - -.fa-wallet { - --fa: "\f555"; -} - -.fa-face-angry { - --fa: "\f556"; -} - -.fa-angry { - --fa: "\f556"; -} - -.fa-archway { - --fa: "\f557"; -} - -.fa-book-atlas { - --fa: "\f558"; -} - -.fa-atlas { - --fa: "\f558"; -} - -.fa-award { - --fa: "\f559"; -} - -.fa-delete-left { - --fa: "\f55a"; -} - -.fa-backspace { - --fa: "\f55a"; -} - -.fa-bezier-curve { - --fa: "\f55b"; -} - -.fa-bong { - --fa: "\f55c"; -} - -.fa-brush { - --fa: "\f55d"; -} - -.fa-bus-simple { - --fa: "\f55e"; -} - -.fa-bus-alt { - --fa: "\f55e"; -} - -.fa-cannabis { - --fa: "\f55f"; -} - -.fa-check-double { - --fa: "\f560"; -} - -.fa-martini-glass-citrus { - --fa: "\f561"; -} - -.fa-cocktail { - --fa: "\f561"; -} - -.fa-bell-concierge { - --fa: "\f562"; -} - -.fa-concierge-bell { - --fa: "\f562"; -} - -.fa-cookie { - --fa: "\f563"; -} - -.fa-cookie-bite { - --fa: "\f564"; -} - -.fa-crop-simple { - --fa: "\f565"; -} - -.fa-crop-alt { - --fa: "\f565"; -} - -.fa-tachograph-digital { - --fa: "\f566"; -} - -.fa-digital-tachograph { - --fa: "\f566"; -} - -.fa-face-dizzy { - --fa: "\f567"; -} - -.fa-dizzy { - --fa: "\f567"; -} - -.fa-compass-drafting { - --fa: "\f568"; -} - -.fa-drafting-compass { - --fa: "\f568"; -} - -.fa-drum { - --fa: "\f569"; -} - -.fa-drum-steelpan { - --fa: "\f56a"; -} - -.fa-feather-pointed { - --fa: "\f56b"; -} - -.fa-feather-alt { - --fa: "\f56b"; -} - -.fa-file-contract { - --fa: "\f56c"; -} - -.fa-file-arrow-down { - --fa: "\f56d"; -} - -.fa-file-download { - --fa: "\f56d"; -} - -.fa-file-export { - --fa: "\f56e"; -} - -.fa-arrow-right-from-file { - --fa: "\f56e"; -} - -.fa-file-import { - --fa: "\f56f"; -} - -.fa-arrow-right-to-file { - --fa: "\f56f"; -} - -.fa-file-invoice { - --fa: "\f570"; -} - -.fa-file-invoice-dollar { - --fa: "\f571"; -} - -.fa-file-prescription { - --fa: "\f572"; -} - -.fa-file-signature { - --fa: "\f573"; -} - -.fa-file-arrow-up { - --fa: "\f574"; -} - -.fa-file-upload { - --fa: "\f574"; -} - -.fa-fill { - --fa: "\f575"; -} - -.fa-fill-drip { - --fa: "\f576"; -} - -.fa-fingerprint { - --fa: "\f577"; -} - -.fa-fish { - --fa: "\f578"; -} - -.fa-face-flushed { - --fa: "\f579"; -} - -.fa-flushed { - --fa: "\f579"; -} - -.fa-face-frown-open { - --fa: "\f57a"; -} - -.fa-frown-open { - --fa: "\f57a"; -} - -.fa-martini-glass { - --fa: "\f57b"; -} - -.fa-glass-martini-alt { - --fa: "\f57b"; -} - -.fa-earth-africa { - --fa: "\f57c"; -} - -.fa-globe-africa { - --fa: "\f57c"; -} - -.fa-earth-americas { - --fa: "\f57d"; -} - -.fa-earth { - --fa: "\f57d"; -} - -.fa-earth-america { - --fa: "\f57d"; -} - -.fa-globe-americas { - --fa: "\f57d"; -} - -.fa-earth-asia { - --fa: "\f57e"; -} - -.fa-globe-asia { - --fa: "\f57e"; -} - -.fa-face-grimace { - --fa: "\f57f"; -} - -.fa-grimace { - --fa: "\f57f"; -} - -.fa-face-grin { - --fa: "\f580"; -} - -.fa-grin { - --fa: "\f580"; -} - -.fa-face-grin-wide { - --fa: "\f581"; -} - -.fa-grin-alt { - --fa: "\f581"; -} - -.fa-face-grin-beam { - --fa: "\f582"; -} - -.fa-grin-beam { - --fa: "\f582"; -} - -.fa-face-grin-beam-sweat { - --fa: "\f583"; -} - -.fa-grin-beam-sweat { - --fa: "\f583"; -} - -.fa-face-grin-hearts { - --fa: "\f584"; -} - -.fa-grin-hearts { - --fa: "\f584"; -} - -.fa-face-grin-squint { - --fa: "\f585"; -} - -.fa-grin-squint { - --fa: "\f585"; -} - -.fa-face-grin-squint-tears { - --fa: "\f586"; -} - -.fa-grin-squint-tears { - --fa: "\f586"; -} - -.fa-face-grin-stars { - --fa: "\f587"; -} - -.fa-grin-stars { - --fa: "\f587"; -} - -.fa-face-grin-tears { - --fa: "\f588"; -} - -.fa-grin-tears { - --fa: "\f588"; -} - -.fa-face-grin-tongue { - --fa: "\f589"; -} - -.fa-grin-tongue { - --fa: "\f589"; -} - -.fa-face-grin-tongue-squint { - --fa: "\f58a"; -} - -.fa-grin-tongue-squint { - --fa: "\f58a"; -} - -.fa-face-grin-tongue-wink { - --fa: "\f58b"; -} - -.fa-grin-tongue-wink { - --fa: "\f58b"; -} - -.fa-face-grin-wink { - --fa: "\f58c"; -} - -.fa-grin-wink { - --fa: "\f58c"; -} - -.fa-grip { - --fa: "\f58d"; -} - -.fa-grid-horizontal { - --fa: "\f58d"; -} - -.fa-grip-horizontal { - --fa: "\f58d"; -} - -.fa-grip-vertical { - --fa: "\f58e"; -} - -.fa-grid-vertical { - --fa: "\f58e"; -} - -.fa-headset { - --fa: "\f590"; -} - -.fa-highlighter { - --fa: "\f591"; -} - -.fa-hot-tub-person { - --fa: "\f593"; -} - -.fa-hot-tub { - --fa: "\f593"; -} - -.fa-hotel { - --fa: "\f594"; -} - -.fa-joint { - --fa: "\f595"; -} - -.fa-face-kiss { - --fa: "\f596"; -} - -.fa-kiss { - --fa: "\f596"; -} - -.fa-face-kiss-beam { - --fa: "\f597"; -} - -.fa-kiss-beam { - --fa: "\f597"; -} - -.fa-face-kiss-wink-heart { - --fa: "\f598"; -} - -.fa-kiss-wink-heart { - --fa: "\f598"; -} - -.fa-face-laugh { - --fa: "\f599"; -} - -.fa-laugh { - --fa: "\f599"; -} - -.fa-face-laugh-beam { - --fa: "\f59a"; -} - -.fa-laugh-beam { - --fa: "\f59a"; -} - -.fa-face-laugh-squint { - --fa: "\f59b"; -} - -.fa-laugh-squint { - --fa: "\f59b"; -} - -.fa-face-laugh-wink { - --fa: "\f59c"; -} - -.fa-laugh-wink { - --fa: "\f59c"; -} - -.fa-cart-flatbed-suitcase { - --fa: "\f59d"; -} - -.fa-luggage-cart { - --fa: "\f59d"; -} - -.fa-map-location { - --fa: "\f59f"; -} - -.fa-map-marked { - --fa: "\f59f"; -} - -.fa-map-location-dot { - --fa: "\f5a0"; -} - -.fa-map-marked-alt { - --fa: "\f5a0"; -} - -.fa-marker { - --fa: "\f5a1"; -} - -.fa-medal { - --fa: "\f5a2"; -} - -.fa-face-meh-blank { - --fa: "\f5a4"; -} - -.fa-meh-blank { - --fa: "\f5a4"; -} - -.fa-face-rolling-eyes { - --fa: "\f5a5"; -} - -.fa-meh-rolling-eyes { - --fa: "\f5a5"; -} - -.fa-monument { - --fa: "\f5a6"; -} - -.fa-mortar-pestle { - --fa: "\f5a7"; -} - -.fa-paint-roller { - --fa: "\f5aa"; -} - -.fa-passport { - --fa: "\f5ab"; -} - -.fa-pen-fancy { - --fa: "\f5ac"; -} - -.fa-pen-nib { - --fa: "\f5ad"; -} - -.fa-pen-ruler { - --fa: "\f5ae"; -} - -.fa-pencil-ruler { - --fa: "\f5ae"; -} - -.fa-plane-arrival { - --fa: "\f5af"; -} - -.fa-plane-departure { - --fa: "\f5b0"; -} - -.fa-prescription { - --fa: "\f5b1"; -} - -.fa-face-sad-cry { - --fa: "\f5b3"; -} - -.fa-sad-cry { - --fa: "\f5b3"; -} - -.fa-face-sad-tear { - --fa: "\f5b4"; -} - -.fa-sad-tear { - --fa: "\f5b4"; -} - -.fa-van-shuttle { - --fa: "\f5b6"; -} - -.fa-shuttle-van { - --fa: "\f5b6"; -} - -.fa-signature { - --fa: "\f5b7"; -} - -.fa-face-smile-beam { - --fa: "\f5b8"; -} - -.fa-smile-beam { - --fa: "\f5b8"; -} - -.fa-solar-panel { - --fa: "\f5ba"; -} - -.fa-spa { - --fa: "\f5bb"; -} - -.fa-splotch { - --fa: "\f5bc"; -} - -.fa-spray-can { - --fa: "\f5bd"; -} - -.fa-stamp { - --fa: "\f5bf"; -} - -.fa-star-half-stroke { - --fa: "\f5c0"; -} - -.fa-star-half-alt { - --fa: "\f5c0"; -} - -.fa-suitcase-rolling { - --fa: "\f5c1"; -} - -.fa-face-surprise { - --fa: "\f5c2"; -} - -.fa-surprise { - --fa: "\f5c2"; -} - -.fa-swatchbook { - --fa: "\f5c3"; -} - -.fa-person-swimming { - --fa: "\f5c4"; -} - -.fa-swimmer { - --fa: "\f5c4"; -} - -.fa-water-ladder { - --fa: "\f5c5"; -} - -.fa-ladder-water { - --fa: "\f5c5"; -} - -.fa-swimming-pool { - --fa: "\f5c5"; -} - -.fa-droplet-slash { - --fa: "\f5c7"; -} - -.fa-tint-slash { - --fa: "\f5c7"; -} - -.fa-face-tired { - --fa: "\f5c8"; -} - -.fa-tired { - --fa: "\f5c8"; -} - -.fa-tooth { - --fa: "\f5c9"; -} - -.fa-umbrella-beach { - --fa: "\f5ca"; -} - -.fa-weight-hanging { - --fa: "\f5cd"; -} - -.fa-wine-glass-empty { - --fa: "\f5ce"; -} - -.fa-wine-glass-alt { - --fa: "\f5ce"; -} - -.fa-spray-can-sparkles { - --fa: "\f5d0"; -} - -.fa-air-freshener { - --fa: "\f5d0"; -} - -.fa-apple-whole { - --fa: "\f5d1"; -} - -.fa-apple-alt { - --fa: "\f5d1"; -} - -.fa-atom { - --fa: "\f5d2"; -} - -.fa-bone { - --fa: "\f5d7"; -} - -.fa-book-open-reader { - --fa: "\f5da"; -} - -.fa-book-reader { - --fa: "\f5da"; -} - -.fa-brain { - --fa: "\f5dc"; -} - -.fa-car-rear { - --fa: "\f5de"; -} - -.fa-car-alt { - --fa: "\f5de"; -} - -.fa-car-battery { - --fa: "\f5df"; -} - -.fa-battery-car { - --fa: "\f5df"; -} - -.fa-car-burst { - --fa: "\f5e1"; -} - -.fa-car-crash { - --fa: "\f5e1"; -} - -.fa-car-side { - --fa: "\f5e4"; -} - -.fa-charging-station { - --fa: "\f5e7"; -} - -.fa-diamond-turn-right { - --fa: "\f5eb"; -} - -.fa-directions { - --fa: "\f5eb"; -} - -.fa-draw-polygon { - --fa: "\f5ee"; -} - -.fa-vector-polygon { - --fa: "\f5ee"; -} - -.fa-laptop-code { - --fa: "\f5fc"; -} - -.fa-layer-group { - --fa: "\f5fd"; -} - -.fa-location-crosshairs { - --fa: "\f601"; -} - -.fa-location { - --fa: "\f601"; -} - -.fa-lungs { - --fa: "\f604"; -} - -.fa-microscope { - --fa: "\f610"; -} - -.fa-oil-can { - --fa: "\f613"; -} - -.fa-poop { - --fa: "\f619"; -} - -.fa-shapes { - --fa: "\f61f"; -} - -.fa-triangle-circle-square { - --fa: "\f61f"; -} - -.fa-star-of-life { - --fa: "\f621"; -} - -.fa-gauge { - --fa: "\f624"; -} - -.fa-dashboard { - --fa: "\f624"; -} - -.fa-gauge-med { - --fa: "\f624"; -} - -.fa-tachometer-alt-average { - --fa: "\f624"; -} - -.fa-gauge-high { - --fa: "\f625"; -} - -.fa-tachometer-alt { - --fa: "\f625"; -} - -.fa-tachometer-alt-fast { - --fa: "\f625"; -} - -.fa-gauge-simple { - --fa: "\f629"; -} - -.fa-gauge-simple-med { - --fa: "\f629"; -} - -.fa-tachometer-average { - --fa: "\f629"; -} - -.fa-gauge-simple-high { - --fa: "\f62a"; -} - -.fa-tachometer { - --fa: "\f62a"; -} - -.fa-tachometer-fast { - --fa: "\f62a"; -} - -.fa-teeth { - --fa: "\f62e"; -} - -.fa-teeth-open { - --fa: "\f62f"; -} - -.fa-masks-theater { - --fa: "\f630"; -} - -.fa-theater-masks { - --fa: "\f630"; -} - -.fa-traffic-light { - --fa: "\f637"; -} - -.fa-truck-monster { - --fa: "\f63b"; -} - -.fa-truck-pickup { - --fa: "\f63c"; -} - -.fa-rectangle-ad { - --fa: "\f641"; -} - -.fa-ad { - --fa: "\f641"; -} - -.fa-ankh { - --fa: "\f644"; -} - -.fa-book-bible { - --fa: "\f647"; -} - -.fa-bible { - --fa: "\f647"; -} - -.fa-business-time { - --fa: "\f64a"; -} - -.fa-briefcase-clock { - --fa: "\f64a"; -} - -.fa-city { - --fa: "\f64f"; -} - -.fa-comment-dollar { - --fa: "\f651"; -} - -.fa-comments-dollar { - --fa: "\f653"; -} - -.fa-cross { - --fa: "\f654"; -} - -.fa-dharmachakra { - --fa: "\f655"; -} - -.fa-envelope-open-text { - --fa: "\f658"; -} - -.fa-folder-minus { - --fa: "\f65d"; -} - -.fa-folder-plus { - --fa: "\f65e"; -} - -.fa-filter-circle-dollar { - --fa: "\f662"; -} - -.fa-funnel-dollar { - --fa: "\f662"; -} - -.fa-gopuram { - --fa: "\f664"; -} - -.fa-hamsa { - --fa: "\f665"; -} - -.fa-bahai { - --fa: "\f666"; -} - -.fa-haykal { - --fa: "\f666"; -} - -.fa-jedi { - --fa: "\f669"; -} - -.fa-book-journal-whills { - --fa: "\f66a"; -} - -.fa-journal-whills { - --fa: "\f66a"; -} - -.fa-kaaba { - --fa: "\f66b"; -} - -.fa-khanda { - --fa: "\f66d"; -} - -.fa-landmark { - --fa: "\f66f"; -} - -.fa-envelopes-bulk { - --fa: "\f674"; -} - -.fa-mail-bulk { - --fa: "\f674"; -} - -.fa-menorah { - --fa: "\f676"; -} - -.fa-mosque { - --fa: "\f678"; -} - -.fa-om { - --fa: "\f679"; -} - -.fa-spaghetti-monster-flying { - --fa: "\f67b"; -} - -.fa-pastafarianism { - --fa: "\f67b"; -} - -.fa-peace { - --fa: "\f67c"; -} - -.fa-place-of-worship { - --fa: "\f67f"; -} - -.fa-square-poll-vertical { - --fa: "\f681"; -} - -.fa-poll { - --fa: "\f681"; -} - -.fa-square-poll-horizontal { - --fa: "\f682"; -} - -.fa-poll-h { - --fa: "\f682"; -} - -.fa-person-praying { - --fa: "\f683"; -} - -.fa-pray { - --fa: "\f683"; -} - -.fa-hands-praying { - --fa: "\f684"; -} - -.fa-praying-hands { - --fa: "\f684"; -} - -.fa-book-quran { - --fa: "\f687"; -} - -.fa-quran { - --fa: "\f687"; -} - -.fa-magnifying-glass-dollar { - --fa: "\f688"; -} - -.fa-search-dollar { - --fa: "\f688"; -} - -.fa-magnifying-glass-location { - --fa: "\f689"; -} - -.fa-search-location { - --fa: "\f689"; -} - -.fa-socks { - --fa: "\f696"; -} - -.fa-square-root-variable { - --fa: "\f698"; -} - -.fa-square-root-alt { - --fa: "\f698"; -} - -.fa-star-and-crescent { - --fa: "\f699"; -} - -.fa-star-of-david { - --fa: "\f69a"; -} - -.fa-synagogue { - --fa: "\f69b"; -} - -.fa-scroll-torah { - --fa: "\f6a0"; -} - -.fa-torah { - --fa: "\f6a0"; -} - -.fa-torii-gate { - --fa: "\f6a1"; -} - -.fa-vihara { - --fa: "\f6a7"; -} - -.fa-volume-xmark { - --fa: "\f6a9"; -} - -.fa-volume-mute { - --fa: "\f6a9"; -} - -.fa-volume-times { - --fa: "\f6a9"; -} - -.fa-yin-yang { - --fa: "\f6ad"; -} - -.fa-blender-phone { - --fa: "\f6b6"; -} - -.fa-book-skull { - --fa: "\f6b7"; -} - -.fa-book-dead { - --fa: "\f6b7"; -} - -.fa-campground { - --fa: "\f6bb"; -} - -.fa-cat { - --fa: "\f6be"; -} - -.fa-chair { - --fa: "\f6c0"; -} - -.fa-cloud-moon { - --fa: "\f6c3"; -} - -.fa-cloud-sun { - --fa: "\f6c4"; -} - -.fa-cow { - --fa: "\f6c8"; -} - -.fa-dice-d20 { - --fa: "\f6cf"; -} - -.fa-dice-d6 { - --fa: "\f6d1"; -} - -.fa-dog { - --fa: "\f6d3"; -} - -.fa-dragon { - --fa: "\f6d5"; -} - -.fa-drumstick-bite { - --fa: "\f6d7"; -} - -.fa-dungeon { - --fa: "\f6d9"; -} - -.fa-file-csv { - --fa: "\f6dd"; -} - -.fa-hand-fist { - --fa: "\f6de"; -} - -.fa-fist-raised { - --fa: "\f6de"; -} - -.fa-ghost { - --fa: "\f6e2"; -} - -.fa-hammer { - --fa: "\f6e3"; -} - -.fa-hanukiah { - --fa: "\f6e6"; -} - -.fa-hat-wizard { - --fa: "\f6e8"; -} - -.fa-person-hiking { - --fa: "\f6ec"; -} - -.fa-hiking { - --fa: "\f6ec"; -} - -.fa-hippo { - --fa: "\f6ed"; -} - -.fa-horse { - --fa: "\f6f0"; -} - -.fa-house-chimney-crack { - --fa: "\f6f1"; -} - -.fa-house-damage { - --fa: "\f6f1"; -} - -.fa-hryvnia-sign { - --fa: "\f6f2"; -} - -.fa-hryvnia { - --fa: "\f6f2"; -} - -.fa-mask { - --fa: "\f6fa"; -} - -.fa-mountain { - --fa: "\f6fc"; -} - -.fa-network-wired { - --fa: "\f6ff"; -} - -.fa-otter { - --fa: "\f700"; -} - -.fa-ring { - --fa: "\f70b"; -} - -.fa-person-running { - --fa: "\f70c"; -} - -.fa-running { - --fa: "\f70c"; -} - -.fa-scroll { - --fa: "\f70e"; -} - -.fa-skull-crossbones { - --fa: "\f714"; -} - -.fa-slash { - --fa: "\f715"; -} - -.fa-spider { - --fa: "\f717"; -} - -.fa-toilet-paper { - --fa: "\f71e"; -} - -.fa-toilet-paper-alt { - --fa: "\f71e"; -} - -.fa-toilet-paper-blank { - --fa: "\f71e"; -} - -.fa-tractor { - --fa: "\f722"; -} - -.fa-user-injured { - --fa: "\f728"; -} - -.fa-vr-cardboard { - --fa: "\f729"; -} - -.fa-wand-sparkles { - --fa: "\f72b"; -} - -.fa-wind { - --fa: "\f72e"; -} - -.fa-wine-bottle { - --fa: "\f72f"; -} - -.fa-cloud-meatball { - --fa: "\f73b"; -} - -.fa-cloud-moon-rain { - --fa: "\f73c"; -} - -.fa-cloud-rain { - --fa: "\f73d"; -} - -.fa-cloud-showers-heavy { - --fa: "\f740"; -} - -.fa-cloud-sun-rain { - --fa: "\f743"; -} - -.fa-democrat { - --fa: "\f747"; -} - -.fa-flag-usa { - --fa: "\f74d"; -} - -.fa-hurricane { - --fa: "\f751"; -} - -.fa-landmark-dome { - --fa: "\f752"; -} - -.fa-landmark-alt { - --fa: "\f752"; -} - -.fa-meteor { - --fa: "\f753"; -} - -.fa-person-booth { - --fa: "\f756"; -} - -.fa-poo-storm { - --fa: "\f75a"; -} - -.fa-poo-bolt { - --fa: "\f75a"; -} - -.fa-rainbow { - --fa: "\f75b"; -} - -.fa-republican { - --fa: "\f75e"; -} - -.fa-smog { - --fa: "\f75f"; -} - -.fa-temperature-high { - --fa: "\f769"; -} - -.fa-temperature-low { - --fa: "\f76b"; -} - -.fa-cloud-bolt { - --fa: "\f76c"; -} - -.fa-thunderstorm { - --fa: "\f76c"; -} - -.fa-tornado { - --fa: "\f76f"; -} - -.fa-volcano { - --fa: "\f770"; -} - -.fa-check-to-slot { - --fa: "\f772"; -} - -.fa-vote-yea { - --fa: "\f772"; -} - -.fa-water { - --fa: "\f773"; -} - -.fa-baby { - --fa: "\f77c"; -} - -.fa-baby-carriage { - --fa: "\f77d"; -} - -.fa-carriage-baby { - --fa: "\f77d"; -} - -.fa-biohazard { - --fa: "\f780"; -} - -.fa-blog { - --fa: "\f781"; -} - -.fa-calendar-day { - --fa: "\f783"; -} - -.fa-calendar-week { - --fa: "\f784"; -} - -.fa-candy-cane { - --fa: "\f786"; -} - -.fa-carrot { - --fa: "\f787"; -} - -.fa-cash-register { - --fa: "\f788"; -} - -.fa-minimize { - --fa: "\f78c"; -} - -.fa-compress-arrows-alt { - --fa: "\f78c"; -} - -.fa-dumpster { - --fa: "\f793"; -} - -.fa-dumpster-fire { - --fa: "\f794"; -} - -.fa-ethernet { - --fa: "\f796"; -} - -.fa-gifts { - --fa: "\f79c"; -} - -.fa-champagne-glasses { - --fa: "\f79f"; -} - -.fa-glass-cheers { - --fa: "\f79f"; -} - -.fa-whiskey-glass { - --fa: "\f7a0"; -} - -.fa-glass-whiskey { - --fa: "\f7a0"; -} - -.fa-earth-europe { - --fa: "\f7a2"; -} - -.fa-globe-europe { - --fa: "\f7a2"; -} - -.fa-grip-lines { - --fa: "\f7a4"; -} - -.fa-grip-lines-vertical { - --fa: "\f7a5"; -} - -.fa-guitar { - --fa: "\f7a6"; -} - -.fa-heart-crack { - --fa: "\f7a9"; -} - -.fa-heart-broken { - --fa: "\f7a9"; -} - -.fa-holly-berry { - --fa: "\f7aa"; -} - -.fa-horse-head { - --fa: "\f7ab"; -} - -.fa-icicles { - --fa: "\f7ad"; -} - -.fa-igloo { - --fa: "\f7ae"; -} - -.fa-mitten { - --fa: "\f7b5"; -} - -.fa-mug-hot { - --fa: "\f7b6"; -} - -.fa-radiation { - --fa: "\f7b9"; -} - -.fa-circle-radiation { - --fa: "\f7ba"; -} - -.fa-radiation-alt { - --fa: "\f7ba"; -} - -.fa-restroom { - --fa: "\f7bd"; -} - -.fa-satellite { - --fa: "\f7bf"; -} - -.fa-satellite-dish { - --fa: "\f7c0"; -} - -.fa-sd-card { - --fa: "\f7c2"; -} - -.fa-sim-card { - --fa: "\f7c4"; -} - -.fa-person-skating { - --fa: "\f7c5"; -} - -.fa-skating { - --fa: "\f7c5"; -} - -.fa-person-skiing { - --fa: "\f7c9"; -} - -.fa-skiing { - --fa: "\f7c9"; -} - -.fa-person-skiing-nordic { - --fa: "\f7ca"; -} - -.fa-skiing-nordic { - --fa: "\f7ca"; -} - -.fa-sleigh { - --fa: "\f7cc"; -} - -.fa-comment-sms { - --fa: "\f7cd"; -} - -.fa-sms { - --fa: "\f7cd"; -} - -.fa-person-snowboarding { - --fa: "\f7ce"; -} - -.fa-snowboarding { - --fa: "\f7ce"; -} - -.fa-snowman { - --fa: "\f7d0"; -} - -.fa-snowplow { - --fa: "\f7d2"; -} - -.fa-tenge-sign { - --fa: "\f7d7"; -} - -.fa-tenge { - --fa: "\f7d7"; -} - -.fa-toilet { - --fa: "\f7d8"; -} - -.fa-screwdriver-wrench { - --fa: "\f7d9"; -} - -.fa-tools { - --fa: "\f7d9"; -} - -.fa-cable-car { - --fa: "\f7da"; -} - -.fa-tram { - --fa: "\f7da"; -} - -.fa-fire-flame-curved { - --fa: "\f7e4"; -} - -.fa-fire-alt { - --fa: "\f7e4"; -} - -.fa-bacon { - --fa: "\f7e5"; -} - -.fa-book-medical { - --fa: "\f7e6"; -} - -.fa-bread-slice { - --fa: "\f7ec"; -} - -.fa-cheese { - --fa: "\f7ef"; -} - -.fa-house-chimney-medical { - --fa: "\f7f2"; -} - -.fa-clinic-medical { - --fa: "\f7f2"; -} - -.fa-clipboard-user { - --fa: "\f7f3"; -} - -.fa-comment-medical { - --fa: "\f7f5"; -} - -.fa-crutch { - --fa: "\f7f7"; -} - -.fa-disease { - --fa: "\f7fa"; -} - -.fa-egg { - --fa: "\f7fb"; -} - -.fa-folder-tree { - --fa: "\f802"; -} - -.fa-burger { - --fa: "\f805"; -} - -.fa-hamburger { - --fa: "\f805"; -} - -.fa-hand-middle-finger { - --fa: "\f806"; -} - -.fa-helmet-safety { - --fa: "\f807"; -} - -.fa-hard-hat { - --fa: "\f807"; -} - -.fa-hat-hard { - --fa: "\f807"; -} - -.fa-hospital-user { - --fa: "\f80d"; -} - -.fa-hotdog { - --fa: "\f80f"; -} - -.fa-ice-cream { - --fa: "\f810"; -} - -.fa-laptop-medical { - --fa: "\f812"; -} - -.fa-pager { - --fa: "\f815"; -} - -.fa-pepper-hot { - --fa: "\f816"; -} - -.fa-pizza-slice { - --fa: "\f818"; -} - -.fa-sack-dollar { - --fa: "\f81d"; -} - -.fa-book-tanakh { - --fa: "\f827"; -} - -.fa-tanakh { - --fa: "\f827"; -} - -.fa-bars-progress { - --fa: "\f828"; -} - -.fa-tasks-alt { - --fa: "\f828"; -} - -.fa-trash-arrow-up { - --fa: "\f829"; -} - -.fa-trash-restore { - --fa: "\f829"; -} - -.fa-trash-can-arrow-up { - --fa: "\f82a"; -} - -.fa-trash-restore-alt { - --fa: "\f82a"; -} - -.fa-user-nurse { - --fa: "\f82f"; -} - -.fa-wave-square { - --fa: "\f83e"; -} - -.fa-person-biking { - --fa: "\f84a"; -} - -.fa-biking { - --fa: "\f84a"; -} - -.fa-border-all { - --fa: "\f84c"; -} - -.fa-border-none { - --fa: "\f850"; -} - -.fa-border-top-left { - --fa: "\f853"; -} - -.fa-border-style { - --fa: "\f853"; -} - -.fa-person-digging { - --fa: "\f85e"; -} - -.fa-digging { - --fa: "\f85e"; -} - -.fa-fan { - --fa: "\f863"; -} - -.fa-icons { - --fa: "\f86d"; -} - -.fa-heart-music-camera-bolt { - --fa: "\f86d"; -} - -.fa-phone-flip { - --fa: "\f879"; -} - -.fa-phone-alt { - --fa: "\f879"; -} - -.fa-square-phone-flip { - --fa: "\f87b"; -} - -.fa-phone-square-alt { - --fa: "\f87b"; -} - -.fa-photo-film { - --fa: "\f87c"; -} - -.fa-photo-video { - --fa: "\f87c"; -} - -.fa-text-slash { - --fa: "\f87d"; -} - -.fa-remove-format { - --fa: "\f87d"; -} - -.fa-arrow-down-z-a { - --fa: "\f881"; -} - -.fa-sort-alpha-desc { - --fa: "\f881"; -} - -.fa-sort-alpha-down-alt { - --fa: "\f881"; -} - -.fa-arrow-up-z-a { - --fa: "\f882"; -} - -.fa-sort-alpha-up-alt { - --fa: "\f882"; -} - -.fa-arrow-down-short-wide { - --fa: "\f884"; -} - -.fa-sort-amount-desc { - --fa: "\f884"; -} - -.fa-sort-amount-down-alt { - --fa: "\f884"; -} - -.fa-arrow-up-short-wide { - --fa: "\f885"; -} - -.fa-sort-amount-up-alt { - --fa: "\f885"; -} - -.fa-arrow-down-9-1 { - --fa: "\f886"; -} - -.fa-sort-numeric-desc { - --fa: "\f886"; -} - -.fa-sort-numeric-down-alt { - --fa: "\f886"; -} - -.fa-arrow-up-9-1 { - --fa: "\f887"; -} - -.fa-sort-numeric-up-alt { - --fa: "\f887"; -} - -.fa-spell-check { - --fa: "\f891"; -} - -.fa-voicemail { - --fa: "\f897"; -} - -.fa-hat-cowboy { - --fa: "\f8c0"; -} - -.fa-hat-cowboy-side { - --fa: "\f8c1"; -} - -.fa-computer-mouse { - --fa: "\f8cc"; -} - -.fa-mouse { - --fa: "\f8cc"; -} - -.fa-radio { - --fa: "\f8d7"; -} - -.fa-record-vinyl { - --fa: "\f8d9"; -} - -.fa-walkie-talkie { - --fa: "\f8ef"; -} - -.fa-caravan { - --fa: "\f8ff"; -} diff --git a/src/media/vendor/fa7free/css/regular.css b/src/media/vendor/fa7free/css/regular.css deleted file mode 100644 index 0715c62..0000000 --- a/src/media/vendor/fa7free/css/regular.css +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2025 Fonticons, Inc. - */ -:root, :host { - --fa-family-classic: "Font Awesome 7 Free"; - --fa-font-regular: normal 400 1em/1 var(--fa-family-classic); - /* deprecated: this older custom property will be removed next major release */ - --fa-style-family-classic: var(--fa-family-classic); -} - -@font-face { - font-family: "Font Awesome 7 Free"; - font-style: normal; - font-weight: 400; - font-display: block; - src: url("../webfonts/fa-regular-400.woff2"); -} -.far { - --fa-family: var(--fa-family-classic); - --fa-style: 400; -} - -.fa-classic { - --fa-family: var(--fa-family-classic); -} - -.fa-regular { - --fa-style: 400; -} \ No newline at end of file diff --git a/src/media/vendor/fa7free/css/solid.css b/src/media/vendor/fa7free/css/solid.css deleted file mode 100644 index a70ad2e..0000000 --- a/src/media/vendor/fa7free/css/solid.css +++ /dev/null @@ -1,31 +0,0 @@ -/*! - * Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com - * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - * Copyright 2025 Fonticons, Inc. - */ -:root, :host { - --fa-family-classic: "Font Awesome 7 Free"; - --fa-font-solid: normal 900 1em/1 var(--fa-family-classic); - /* deprecated: this older custom property will be removed next major release */ - --fa-style-family-classic: var(--fa-family-classic); -} - -@font-face { - font-family: "Font Awesome 7 Free"; - font-style: normal; - font-weight: 900; - font-display: block; - src: url("../webfonts/fa-solid-900.woff2"); -} -.fas { - --fa-family: var(--fa-family-classic); - --fa-style: 900; -} - -.fa-classic { - --fa-family: var(--fa-family-classic); -} - -.fa-solid { - --fa-style: 900; -} \ No newline at end of file diff --git a/src/script.php b/src/script.php index e73bab4..d78ceb6 100644 --- a/src/script.php +++ b/src/script.php @@ -88,11 +88,11 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface public function postflight(string $type, InstallerAdapter $parent): bool { - // On install or update: migrate from MokoCassiopeia if it exists if ($type === 'install' || $type === 'update') { $this->migrateFromCassiopeia(); $this->replaceCassiopeiaReferences(); $this->clearFaviconStamp(); + $this->cleanMediaFolder(); $this->lockExtension(); } @@ -402,6 +402,87 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface } } + /** + * Clean the template media folder on install/update. + * + * - Removes stale .min files (regenerated automatically by MokoMinifyHelper) + * - Removes deprecated/renamed files from previous versions + * - Removes unminified vendor files (vendors ship .min only) + */ + private function cleanMediaFolder(): void + { + $mediaRoot = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME; + if (!is_dir($mediaRoot)) { + return; + } + + $removed = 0; + + // 1. Delete all .min.css and .min.js in project dirs (MokoMinifyHelper rebuilds them) + // Skip vendor/ — those are pre-minified originals + $projectDirs = ['css', 'js']; + foreach ($projectDirs as $dir) { + $path = $mediaRoot . '/' . $dir; + if (!is_dir($path)) continue; + $this->deleteMinFilesRecursive($path, $removed); + } + + // 2. Remove unminified vendor files (vendors ship .min only) + $vendorUnminified = [ + 'vendor/fa7free/css/all.css', + 'vendor/fa7free/css/brands.css', + 'vendor/fa7free/css/fontawesome.css', + 'vendor/fa7free/css/regular.css', + 'vendor/fa7free/css/solid.css', + ]; + foreach ($vendorUnminified as $relPath) { + $file = $mediaRoot . '/' . $relPath; + if (is_file($file)) { + @unlink($file); + $removed++; + } + } + + // 3. Remove deprecated files from previous versions + $deprecated = [ + 'css/custom.css', // Renamed to css/user.css + 'js/custom.js', // Renamed to js/user.js + 'css/template-rtl.css', // No longer used + ]; + foreach ($deprecated as $relPath) { + $file = $mediaRoot . '/' . $relPath; + if (is_file($file)) { + @unlink($file); + $removed++; + } + } + + if ($removed > 0) { + $this->logMessage("Cleaned media folder: removed {$removed} stale/deprecated file(s)."); + } + } + + /** + * Recursively delete *.min.css and *.min.js in a directory. + */ + private function deleteMinFilesRecursive(string $dir, int &$count): void + { + $entries = scandir($dir); + if (!$entries) return; + + foreach ($entries as $entry) { + if ($entry === '.' || $entry === '..') continue; + $full = $dir . '/' . $entry; + + if (is_dir($full)) { + $this->deleteMinFilesRecursive($full, $count); + } elseif (preg_match('/\.min\.(css|js)$/', $entry)) { + @unlink($full); + $count++; + } + } + } + private function logMessage(string $message, string $priority = 'info'): void { $priorities = [ diff --git a/src/templateDetails.xml b/src/templateDetails.xml index 72c16a0..47cdd2a 100644 --- a/src/templateDetails.xml +++ b/src/templateDetails.xml @@ -39,13 +39,13 @@ MokoOnyx - 02.01.00 + 02.01.06 script.php - 2026-05-05 + 2026-05-09 Jonathan Miller || Moko Consulting hello@mokoconsulting.tech (C)GNU General Public License Version 3 - 2026 Moko Consulting -

Migrating from MokoCassiopeia?

MokoOnyx automatically imports your MokoCassiopeia settings on first use. To trigger the migration:

  1. Install MokoOnyx via System → Install → Extensions
  2. Go to System → Site Templates and set MokoOnyx as your default template
  3. Visit any page on your site — the migration runs automatically on first page load
  4. Check administrator/logs/mokoonyx_migrate.log.php to confirm migration completed
  5. Uninstall MokoCassiopeia from Extensions → Manage

What gets migrated: template style params, custom colour palettes (light.custom.css, dark.custom.css), user.css, user.js, and update server URL. To re-run the migration, delete templates/mokoonyx/.migrated and reload any page.

Version License Joomla PHP

MokoOnyx

MokoOnyx (formerly MokoCassiopeia) is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility.

Custom Colour Themes

Copy templates/mokoonyx/templates/light.custom.css to media/templates/site/mokoonyx/css/theme/light.custom.css (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab.

Custom CSS & JavaScript

  • media/templates/site/mokoonyx/css/user.css — custom CSS overrides
  • media/templates/site/mokoonyx/js/user.js — custom JavaScript

These files survive template updates.

]]>
+ Version License Joomla PHP

MokoOnyx

MokoOnyx is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility.

Custom Colour Themes

Copy templates/mokoonyx/templates/light.custom.css to media/templates/site/mokoonyx/css/theme/light.custom.css (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab.

Custom CSS & JavaScript

  • media/templates/site/mokoonyx/css/user.css — custom CSS overrides
  • media/templates/site/mokoonyx/js/user.js — custom JavaScript

These files survive template updates.

]]>
1 component.php @@ -107,11 +107,7 @@ - -
- - -
+
@@ -148,12 +144,6 @@
- -
- - -
-
diff --git a/updates.xml b/updates.xml index 4dc3a3d..b0b35ab 100644 --- a/updates.xml +++ b/updates.xml @@ -7,20 +7,21 @@ MokoOnyx - MokoOnyx update + MokoOnyx development build. mokoonyx template - 02.01.00 site - development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable + 02.01.08 + 2026-05-11 + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development - https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/-02.01.00.zip + https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-02.01.08-dev.zip - e86c851b2b6e49ce46abd76235a1193fe98a0d0c82133356dfaaa28328ceb2b9 - + 698e87f9cc67884461287f56dc2611c794caf3a1ab6787503045988a2b829a49 + development Moko Consulting https://mokoconsulting.tech + MokoOnyx