feat: comprehensive accessibility improvements for visually impaired users

Implemented WCAG 2.1 AA compliant accessibility enhancements:

ARIA & Semantic HTML:
- Added semantic landmarks (main, section, article, role attributes)
- Comprehensive ARIA labels and descriptions throughout
- ARIA live regions for dynamic content updates (polite/assertive)
- Screen reader-only text for context (.sr-only class)
- Proper heading hierarchy with IDs for navigation

Keyboard Navigation:
- Full keyboard support for SWOT cards (Arrow keys, Home, End)
- Skip to main content link for keyboard users
- Focus management: auto-focus results heading after analysis
- Focus visible styles already in place from SCSS

Screen Reader Enhancements:
- announceToScreenReader() function for status updates
- ARIA live regions on spinner, status timeline, results
- Descriptive labels for icons (aria-hidden for decorative)
- Category descriptions (e.g., "Strengths - positive internal factors")

Interactive Elements:
- Enhanced button and link labels
- Progress indicators with aria-busy and role="progressbar"
- Modal dialog attributes for spinner overlay
- Feed role for status timeline updates

Files Modified:
- result.html: SWOT cards, summary section with full ARIA support
- app.js: Screen reader announcements, keyboard nav, focus management
- base.html: Skip link and semantic main element
- EmptyState.jinja: role="status" with aria-live
- ErrorState.jinja: role="alert" for error announcements
- Spinner.jinja: Modal dialog with progress indicator
- status.html: Feed role for timeline updates
- _components.scss: sr-only and skip-link utility classes

These improvements ensure the SWOT analyzer is fully accessible to
visually impaired users using screen readers and keyboard navigation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 16:22:01 -05:00
parent 67fce2656b
commit 8e514ded43
10 changed files with 212 additions and 51 deletions

View File

@ -3,6 +3,38 @@
// Reusable UI components
// ============================================
// Accessibility
// ===================================
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: $brand-primary;
color: white;
padding: 0.75rem 1.5rem;
text-decoration: none;
font-weight: 600;
z-index: 10000;
border-radius: 0 0 $radius-md 0;
transition: top $transition-fast;
&:focus {
top: 0;
}
}
// Loading Spinner
// ===================================
.spinner-wrapper {

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../scss/styles.scss","../../scss/_variables.scss","../../scss/_typography.scss","../../scss/_animations.scss","../../scss/_components.scss","../../scss/_layout.scss","../../scss/_states.scss","../../scss/_responsive.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GCMQ,gICFR,KACE,YDIU,8FCHV,kDACA,mCACA,kCAGF,mCAGE,YDJa,mCCKb,uBACA,gBAIF,YACE,iCACA,gBACA,WDmBkB,kDClBlB,6BACA,sCACA,qBACA,mBACA,gBCxBF,gBACE,KACE,uBAEF,GACE,0BAIJ,iBACE,QACE,0BAEF,IACE,6BAIJ,iBACE,QACE,uCAEF,IACE,0CAIJ,mBACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,kBACE,KACE,UAEF,GACE,WAIJ,iBACE,gCACA,gCACA,gCAGF,qBACE,QACE,mBAEF,IACE,sBAIJ,2BACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,wBACE,KACE,UACA,4BAEF,GACE,UACA,yBAIJ,uBACE,QACE,WF3CQ,6DE6CV,IACE,2CCtFJ,iBACE,eACA,MACA,OACA,WACA,YACA,qCACA,0BACA,aACA,uBACA,mBACA,aAGF,iBACE,kBAGF,QACE,qCACA,6BACA,kBACA,WACA,YACA,mCACA,cAGF,cACE,kBAEA,iBACE,kBACA,gBACA,MHHU,QGIV,oBAGF,gBACE,kBACA,MHZU,QGkBd,kBACE,gBACA,cACA,kBAGF,aACE,WAGF,oBACE,aACA,mBACA,gBACA,cHSY,OGRZ,cACA,WHpBU,iEGqBV,kDAEA,iCACE,2CACA,2BAIJ,aACE,eACA,MH/CY,QGgDZ,kBAGF,cACE,OACA,YACA,aACA,qBACA,eACA,yBACA,MHrDY,QGuDZ,2BACE,MH7DU,QGiEd,eACE,WHtFc,QGuFd,WACA,YACA,cH1BY,OG2BZ,qBACA,gBACA,eACA,eACA,aACA,mBACA,UACA,kDACA,mBAEA,qBACE,WHnGiB,QGoGjB,0BACA,WHpEQ,+DGuEV,sBACE,sBAGF,qBACE,0BACA,mBAGF,0BACE,kBACA,oBACA,oBAEA,iCACE,WACA,kBACA,WACA,YACA,QACA,SACA,iBACA,gBACA,+BACA,sBACA,kBACA,mCAKN,aACE,gBACA,kBACA,MHtHY,QGuHZ,kBAEA,eACE,oBACA,MHjJY,QGsJhB,2CAKE,kDAIF,eACE,0BA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","file":"pygentic_ai.css"}
{"version":3,"sourceRoot":"","sources":["../../scss/styles.scss","../../scss/_variables.scss","../../scss/_typography.scss","../../scss/_animations.scss","../../scss/_components.scss","../../scss/_layout.scss","../../scss/_states.scss","../../scss/_responsive.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA,GCMQ,gICFR,KACE,YDIU,8FCHV,kDACA,mCACA,kCAGF,mCAGE,YDJa,mCCKb,uBACA,gBAIF,YACE,iCACA,gBACA,WDmBkB,kDClBlB,6BACA,sCACA,qBACA,mBACA,gBCxBF,gBACE,KACE,uBAEF,GACE,0BAIJ,iBACE,QACE,0BAEF,IACE,6BAIJ,iBACE,QACE,uCAEF,IACE,0CAIJ,mBACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,kBACE,KACE,UAEF,GACE,WAIJ,iBACE,gCACA,gCACA,gCAGF,qBACE,QACE,mBAEF,IACE,sBAIJ,2BACE,KACE,UACA,2BAEF,GACE,UACA,yBAIJ,wBACE,KACE,UACA,4BAEF,GACE,UACA,yBAIJ,uBACE,QACE,WF3CQ,6DE6CV,IACE,2CCtFJ,SACE,kBACA,UACA,WACA,UACA,YACA,gBACA,sBACA,mBACA,eAGF,WACE,kBACA,UACA,OACA,WHVc,QGWd,WACA,sBACA,qBACA,gBACA,cACA,yBACA,kDAEA,iBACE,MAMJ,iBACE,eACA,MACA,OACA,WACA,YACA,qCACA,0BACA,aACA,uBACA,mBACA,aAGF,iBACE,kBAGF,QACE,qCACA,6BACA,kBACA,WACA,YACA,mCACA,cAGF,cACE,kBAEA,iBACE,kBACA,gBACA,MHnCU,QGoCV,oBAGF,gBACE,kBACA,MH5CU,QGkDd,kBACE,gBACA,cACA,kBAGF,aACE,WAGF,oBACE,aACA,mBACA,gBACA,cHvBY,OGwBZ,cACA,WHpDU,iEGqDV,kDAEA,iCACE,2CACA,2BAIJ,aACE,eACA,MH/EY,QGgFZ,kBAGF,cACE,OACA,YACA,aACA,qBACA,eACA,yBACA,MHrFY,QGuFZ,2BACE,MH7FU,QGiGd,eACE,WHtHc,QGuHd,WACA,YACA,cH1DY,OG2DZ,qBACA,gBACA,eACA,eACA,aACA,mBACA,UACA,kDACA,mBAEA,qBACE,WHnIiB,QGoIjB,0BACA,WHpGQ,+DGuGV,sBACE,sBAGF,qBACE,0BACA,mBAGF,0BACE,kBACA,oBACA,oBAEA,iCACE,WACA,kBACA,WACA,YACA,QACA,SACA,iBACA,gBACA,+BACA,sBACA,kBACA,mCAKN,aACE,gBACA,kBACA,MHtJY,QGuJZ,kBAEA,eACE,oBACA,MHjLY,QGsLhB,2CAKE,kDAIF,eACE,0BACA,mBCvMF,eACE,wEACA,kBACA,gBAIA,eACE,+BACA,wCAEA,qBACE,wCACA,8EAKN,kBACE,4BAKF,WACE,aACA,2DACA,IJiCW,OIhCX,WJiCW,KI9Bb,WACE,gBACA,cJkCU,KIjCV,QJ0BW,OIzBX,WJMU,+DILV,kDACA,uCACA,yCAEA,iBACE,2BACA,WJCS,kCICT,kCACE,iCAKJ,qBACE,sBAGF,qBACE,sBAGF,wBACE,sBAGF,mBACE,sBAIJ,mBACE,aACA,mBACA,WACA,mBACA,oBACA,gCAGF,iBACE,WACA,YACA,cJbU,KIcV,6BACA,aACA,mBACA,uBACA,WACA,iBACA,cAGF,kBACE,gBACA,mBACA,MJ7DY,QI8DZ,YACA,SAGF,kBACE,WJ3EY,QI4EZ,sBACA,cJhCU,KIiCV,kBACA,gBACA,MJ1EY,QI2EZ,cAGF,iBACE,gBAGF,WACE,gBACA,UACA,SAGF,iBACE,aACA,uBACA,WACA,iBACA,gCACA,wCAEA,4BACE,mBAKA,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,8BACE,qBADF,+BACE,mBAKN,mBACE,UACA,WACA,kBACA,6BACA,iBACA,cAGF,iBACE,OACA,MJvHY,QIwHZ,gBAKA,wBACE,qBADF,wBACE,qBADF,wBACE,qBADF,wBACE,qBAKJ,6BACE,uCAKF,iBACE,kBACA,kBAEA,yBACE,WACA,kBACA,UACA,SACA,YACA,UACA,WJzJU,QI6Jd,aACE,kBACA,yBACA,oCAIE,mFAEE,MJ9Ka,QImLf,yFAEE,MJvLU,QI0LZ,4CACE,8CAKF,yFAEE,MJpMU,QIyMZ,qFAEE,MJrMQ,QI0Md,wBACE,kBACA,OACA,YACA,WACA,YACA,kBACA,aACA,mBACA,uBACA,eACA,gBACA,0BACA,UAGF,sBACE,gBACA,cJ5KU,KI6KV,qBACA,WJxMU,6DIyMV,kDAGF,yCACE,WJ5MU,+DI6MV,0BAGF,qBACE,gBACA,kBACA,yBACA,qBACA,oBAGF,sBACE,MJrOY,QIsOZ,gBACA,mBCpQF,aACE,aACA,mBACA,uBACA,iBACA,oBAGF,sBACE,kBACA,gBAGF,mBACE,YACA,aACA,mBACA,kBACA,6DACA,aACA,mBACA,uBACA,eACA,MLGY,QKFZ,wCAGF,oBACE,kBACA,gBACA,cACA,qBAGF,0BACE,mBACA,MLRY,QKSZ,gBACA,qBAGF,kBACE,oBACA,mBACA,UACA,qBACA,WLxCc,QKyCd,WACA,cLqBY,OKpBZ,qBACA,gBACA,kDACA,WLZU,6DKcV,wBACE,WL/CiB,QKgDjB,2BACA,WLhBQ,+DKiBR,WAMJ,aACE,aACA,mBACA,uBACA,iBACA,oBAGF,sBACE,kBACA,gBAGF,mBACE,YACA,aACA,mBACA,kBACA,6DACA,aACA,mBACA,uBACA,eACA,MLrEY,QKsEZ,gCAGF,oBACE,kBACA,gBACA,MLhEY,QKiEZ,qBAGF,0BACE,mBACA,MLxEY,QKyEZ,gBACA,qBAGF,sBACE,WLnFY,QKoFZ,cLzCU,KK0CV,aACA,gBACA,oCACA,kBACA,MLnFY,QKoFZ,gBACA,gBAGF,sBACE,aACA,SACA,uBACA,eAGF,qBACE,oBACA,mBACA,UACA,qBACA,cL5DY,OK6DZ,gBACA,kDACA,WL5FU,6DK6FV,eACA,YACA,qBAEA,8BACE,WLpIY,QKqIZ,WAEA,oCACE,WLtIe,QKuIf,2BACA,WLvGM,+DKwGN,WAIJ,gCACE,gBACA,ML1HU,QK2HV,yBAEA,sCACE,WLpIQ,QKqIR,aLlIQ,QKmIR,2BACA,MLhIQ,QM/Bd,yBACE,oBACE,mBAGF,WACE,0BAGF,YACE,eAGF,oBACE,sBACA,UACA,cNqDQ,KMpDR,aAGF,cACE,WACA,kBAGF,eACE,WACA,uBAGF,mBACE,eAGF,kBACE,SACA,kBAKJ,yBACE,WACE,aAGF,iBACE,WACA,YACA,kBAGF,kBACE,mBAGF,YACE,kBAGF,iBACE,oBAGF,aACE,qBP3CJ,MAEE,yBACA,+BACA,8BAGA,yBACA,yBACA,4BACA,uBAGA,sBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBACA,uBAGA,6CACA,mFACA,qFACA,uFACA,oDAGA,sDACA,sDACA","file":"pygentic_ai.css"}

View File

@ -71,15 +71,47 @@
}
/**
* Smooth scroll to results
* Announce to screen readers
*/
function announceToScreenReader(message, priority = 'polite') {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
// Remove after announcement is made
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
/**
* Smooth scroll to results and manage focus
*/
function scrollToResults() {
const resultsSection = document.getElementById('result-container');
const resultsHeading = document.getElementById('results-heading');
if (resultsSection) {
// Announce completion to screen readers
announceToScreenReader('Analysis complete. Results are now available.', 'assertive');
// Scroll to results
resultsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Move focus to results heading for keyboard users
if (resultsHeading) {
setTimeout(() => {
resultsHeading.focus();
}, 600);
}
}
}
@ -96,6 +128,44 @@
return pollInterval;
}
/**
* Initialize keyboard navigation for SWOT cards
*/
function initializeKeyboardNavigation() {
const cards = document.querySelectorAll('.swot-card');
cards.forEach((card, index) => {
card.addEventListener('keydown', function(e) {
let targetCard = null;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
targetCard = cards[index + 1] || cards[0];
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
targetCard = cards[index - 1] || cards[cards.length - 1];
break;
case 'Home':
e.preventDefault();
targetCard = cards[0];
break;
case 'End':
e.preventDefault();
targetCard = cards[cards.length - 1];
break;
}
if (targetCard) {
targetCard.focus();
}
});
});
}
/**
* Initialize form submission handler
*/
@ -104,6 +174,9 @@
if (!form) return;
form.addEventListener('submit', function(e) {
// Announce to screen readers
announceToScreenReader('Analysis started. Please wait while we process your request.', 'assertive');
// Start loading messages when form is submitted
startLoadingMessages();
@ -135,6 +208,9 @@
spinner.classList.add('is-hidden');
}
// Initialize keyboard navigation for SWOT cards
initializeKeyboardNavigation();
// Scroll to results after a brief delay
setTimeout(scrollToResults, 500);
@ -219,6 +295,7 @@
window.PygenticAI = {
startLoadingMessages,
stopLoadingMessages,
scrollToResults
scrollToResults,
announceToScreenReader
};
})();

View File

@ -8,9 +8,13 @@
</head>
<body>
<a href="#main-content"
class="skip-link">Skip to main content</a>
{% include "components/main/nav.html" %}
{% block content%}
{% endblock %}
<main id="main-content">
{% block content%}
{% endblock %}
</main>
{% block js_content %}
{% endblock js_content %}
{% include "components/snippets/js.html" %}

View File

@ -4,9 +4,12 @@
icon: str = "fa-chart-simple"
#}
<div class="empty-state">
<div class="empty-state"
role="status"
aria-live="polite">
<div class="empty-state__content">
<div class="empty-state__icon">
<div class="empty-state__icon"
aria-hidden="true">
<i class="fas {{ icon }}"></i>
</div>
<h3 class="empty-state__title">{{ title }}</h3>

View File

@ -5,16 +5,21 @@
show_retry: bool = True
#}
<div class="error-state">
<div class="error-state"
role="alert"
aria-live="assertive">
<div class="error-state__content">
<div class="error-state__icon">
<div class="error-state__icon"
aria-hidden="true">
<i class="fas fa-triangle-exclamation"></i>
</div>
<h3 class="error-state__title">{{ title }}</h3>
<p class="error-state__description">{{ description }}</p>
{% if error_details %}
<div class="error-state__details">
<div class="error-state__details"
role="region"
aria-label="Error details">
{{ error_details }}
</div>
{% endif %}
@ -22,14 +27,18 @@
<div class="error-state__actions">
{% if show_retry %}
<button class="error-state__button error-state__button--primary"
onclick="location.reload()">
<i class="fas fa-rotate-right"></i>
onclick="location.reload()"
aria-label="Try analyzing again">
<i class="fas fa-rotate-right"
aria-hidden="true"></i>
<span>Try Again</span>
</button>
{% endif %}
<a href="/"
class="error-state__button error-state__button--secondary">
<i class="fas fa-home"></i>
class="error-state__button error-state__button--secondary"
aria-label="Return to home page">
<i class="fas fa-home"
aria-hidden="true"></i>
<span>Go Home</span>
</a>
</div>

View File

@ -1,10 +1,19 @@
<div class="spinner-wrapper is-overlay is-hidden"
id="spinner">
id="spinner"
role="dialog"
aria-modal="true"
aria-labelledby="spinner-title"
aria-describedby="loading-status">
<div class="loading-content">
<div class="loader"></div>
<div class="loader"
role="progressbar"
aria-label="Loading in progress"
aria-busy="true"></div>
<div class="loading-text">
<h3>Analyzing...</h3>
<p id="loading-status">Fetching URL content</p>
<h3 id="spinner-title">Analyzing...</h3>
<p id="loading-status"
aria-live="polite"
aria-atomic="true">Fetching URL content</p>
</div>
</div>
</div>

View File

@ -1,52 +1,78 @@
{% if result %}
<section class="section"
id="result-container">
id="result-container"
aria-live="polite"
aria-atomic="false"
role="region"
aria-label="SWOT Analysis Results">
<div class="container">
<h2 class="title is-2 has-text-centered mb-6">Analysis Complete ✨</h2>
<div class="swot-grid">
<h2 class="title is-2 has-text-centered mb-6"
id="results-heading"
tabindex="-1">
<span aria-label="Analysis Complete">Analysis Complete</span>
</h2>
<div class="swot-grid"
role="list"
aria-label="SWOT Analysis Categories">
{% for cat, val in result.dict().items() %}
{% if cat != 'summary' %}
{# Determine card class and icon based on category #}
{% set card_class = 'strength' if cat == 'strengths' else ('weakness' if cat == 'weaknesses' else ('opportunity' if cat == 'opportunities' else 'threat')) %}
{% set icon = 'fa-arrow-trend-up' if cat == 'strengths' else ('fa-arrow-trend-down' if cat == 'weaknesses' else ('fa-lightbulb' if cat == 'opportunities' else 'fa-triangle-exclamation')) %}
{% set category_label = 'Strengths - positive internal factors' if cat == 'strengths' else ('Weaknesses - negative internal factors' if cat == 'weaknesses' else ('Opportunities - positive external factors' if cat == 'opportunities' else 'Threats - negative external factors')) %}
<div class="swot-card swot-card--{{ card_class }}">
<article class="swot-card swot-card--{{ card_class }}"
role="listitem"
aria-labelledby="card-title-{{ cat }}"
tabindex="0">
<div class="swot-card__header">
<div class="swot-card__icon">
<div class="swot-card__icon"
aria-hidden="true">
<i class="fas {{ icon }}"></i>
</div>
<h3 class="swot-card__title">{{ cat.title() }}</h3>
<span class="swot-card__count">{{ val|length }}</span>
<h3 class="swot-card__title"
id="card-title-{{ cat }}">
{{ cat.title() }}
<span class="sr-only">: {{ category_label }}</span>
</h3>
<span class="swot-card__count"
aria-label="{{ val|length }} items">{{ val|length }}</span>
</div>
<div class="swot-card__body">
<ul class="swot-list">
<ul class="swot-list"
aria-label="{{ cat.title() }} list">
{% for value in val %}
<li class="swot-list__item">
<span class="swot-list__bullet"></span>
<span class="swot-list__bullet"
aria-hidden="true"></span>
<span class="swot-list__text">{{ value }}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
</article>
{% endif %}
{% endfor %}
</div>
{# Summary Section #}
{% if result.summary %}
<div class="box mt-6"
style="border-radius: 16px; border-left: 4px solid var(--brand-primary); box-shadow: var(--shadow-lg);">
<section class="box mt-6"
role="region"
aria-labelledby="summary-heading"
style="border-radius: 16px; border-left: 4px solid var(--brand-primary); box-shadow: var(--shadow-lg);">
<h3 class="title is-4"
id="summary-heading"
style="color: var(--brand-primary);">
<i class="fas fa-clipboard-list mr-2"></i>
<i class="fas fa-clipboard-list mr-2"
aria-hidden="true"></i>
Executive Summary
</h3>
<div class="content"
style="color: var(--neutral-700); line-height: 1.8;">
{{ result.summary }}
</div>
</div>
</section>
{% endif %}
</div>
</section>

View File

@ -1,15 +1,26 @@
{% if messages %}
<section class="section" id="status-container">
<div class="container" style="max-width: 800px;">
<div class="status-timeline">
<section class="section"
id="status-container"
role="region"
aria-label="Analysis Progress"
aria-live="polite">
<div class="container"
style="max-width: 800px;">
<div class="status-timeline"
role="feed"
aria-label="Status updates">
{% for message in messages %}
{% set is_error = message.startswith('Error:') %}
{% set is_loading = loop.last and not result %}
{% set is_tool_message = message.startswith('Using tool') %}
{% set is_complete = "complete" in message.lower() or "done" in message.lower() %}
{% set status_label = 'Error' if is_error else ('Complete' if is_complete else ('In Progress' if is_loading else ('Processing' if is_tool_message else 'Status'))) %}
<div class="status-item {% if is_error %}status-item--error{% elif is_complete %}status-item--success{% elif is_loading %}status-item--loading{% else %}status-item--info{% endif %}">
<div class="status-item__indicator">
<article class="status-item {% if is_error %}status-item--error{% elif is_complete %}status-item--success{% elif is_loading %}status-item--loading{% else %}status-item--info{% endif %}"
role="article"
aria-label="{{ status_label }}: {{ message }}">
<div class="status-item__indicator"
aria-hidden="true">
{% if is_error %}
<i class="fas fa-circle-xmark"></i>
{% elif is_complete %}
@ -22,25 +33,15 @@
</div>
<div class="status-item__content">
<div class="status-item__header">
{% if is_error %}
Error
{% elif is_complete %}
Complete
{% elif is_loading %}
In Progress
{% elif is_tool_message %}
Processing
{% else %}
Status
{% endif %}
{{ status_label }}
</div>
<div class="status-item__message">
{{ message }}
</div>
</div>
</div>
</article>
{% endfor %}
</div>
</div>
</section>
{% endif %}
{% endif %}