fix(a11y): improve accessibility across all views
- FAQ: add aria-expanded/aria-controls/role=region via initFaqA11y() - TabBar/Auth/Center/Preview tabs: add role=tablist/tab/aria-selected - View switching: focus management + aria-live announcement region - Role cards: role=button, tabindex=0, Enter/Space keyboard support - Login form: autocomplete=username/current-password (was off) - Register form: autocomplete=username/new-password - Add skip-link for keyboard users - Add :focus-visible outlines on all interactive elements - Improve placeholder contrast (0.45 → 0.7 opacity) - Add prefers-reduced-motion media query - Add aria-live=polite on dynamic role-list/income-list containers - Add aria-label on all view sections
This commit is contained in:
parent
7725cf1f65
commit
5a7155ecbc
90
app.js
90
app.js
|
|
@ -96,6 +96,19 @@
|
||||||
let viewHistory = ['landing'];
|
let viewHistory = ['landing'];
|
||||||
|
|
||||||
// --- U9: Unified showView with history and tab-bar sync ---
|
// --- U9: Unified showView with history and tab-bar sync ---
|
||||||
|
// Human-readable labels for screen-reader announcements
|
||||||
|
const viewLabels = {
|
||||||
|
landing: '首页',
|
||||||
|
auth: '登录 / 注册',
|
||||||
|
'role-library': '角色库',
|
||||||
|
'role-detail': '角色详情',
|
||||||
|
distill: '蒸馏前任',
|
||||||
|
about: '关于 Eternal AI',
|
||||||
|
onboarding: '创作者入驻',
|
||||||
|
'creator-center': '创作者管理中心',
|
||||||
|
creator: '角色编辑',
|
||||||
|
};
|
||||||
|
|
||||||
function showView(name, trackHistory = true) {
|
function showView(name, trackHistory = true) {
|
||||||
Object.entries(views).forEach(([key, el]) => {
|
Object.entries(views).forEach(([key, el]) => {
|
||||||
if (el) el.classList.toggle('active', key === name);
|
if (el) el.classList.toggle('active', key === name);
|
||||||
|
|
@ -105,6 +118,20 @@
|
||||||
}
|
}
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
updateTabBar(name);
|
updateTabBar(name);
|
||||||
|
|
||||||
|
// a11y: move focus to the new view so screen readers announce it
|
||||||
|
const target = views[name];
|
||||||
|
if (target) {
|
||||||
|
target.setAttribute('tabindex', '-1');
|
||||||
|
// Defer focus to after the scroll/layout settles
|
||||||
|
setTimeout(() => target.focus({ preventScroll: true }), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a11y: announce the view change to screen readers via live region
|
||||||
|
const announcer = document.getElementById('sr-announce');
|
||||||
|
if (announcer) {
|
||||||
|
announcer.textContent = viewLabels[name] ? `已进入${viewLabels[name]}` : '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
|
|
@ -127,7 +154,9 @@
|
||||||
};
|
};
|
||||||
const activeTab = tabMap[viewName] || 'tab-home';
|
const activeTab = tabMap[viewName] || 'tab-home';
|
||||||
document.querySelectorAll('.tab-bar__item').forEach((item) => {
|
document.querySelectorAll('.tab-bar__item').forEach((item) => {
|
||||||
item.classList.toggle('active', item.dataset.tabAction === activeTab);
|
const isActive = item.dataset.tabAction === activeTab;
|
||||||
|
item.classList.toggle('active', isActive);
|
||||||
|
item.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +209,9 @@
|
||||||
function switchAuthTab(tab) {
|
function switchAuthTab(tab) {
|
||||||
activeAuthTab = tab;
|
activeAuthTab = tab;
|
||||||
document.querySelectorAll('.auth-tab').forEach((t) => {
|
document.querySelectorAll('.auth-tab').forEach((t) => {
|
||||||
t.classList.toggle('active', t.dataset.tab === tab);
|
const isActive = t.dataset.tab === tab;
|
||||||
|
t.classList.toggle('active', isActive);
|
||||||
|
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
document.querySelectorAll('.auth-form').forEach((f) => {
|
document.querySelectorAll('.auth-form').forEach((f) => {
|
||||||
f.classList.toggle('active', f.dataset.form === tab);
|
f.classList.toggle('active', f.dataset.form === tab);
|
||||||
|
|
@ -235,7 +266,7 @@
|
||||||
listEl.innerHTML = mockRoles
|
listEl.innerHTML = mockRoles
|
||||||
.map(
|
.map(
|
||||||
(role) => `
|
(role) => `
|
||||||
<article class="role-card" data-role-id="${role.id}">
|
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.desc},每月${role.price}元">
|
||||||
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
||||||
<div class="role-card__info">
|
<div class="role-card__info">
|
||||||
<h3 class="role-card__name">${role.name}</h3>
|
<h3 class="role-card__name">${role.name}</h3>
|
||||||
|
|
@ -279,6 +310,35 @@
|
||||||
const isOpen = answer.style.display === 'block';
|
const isOpen = answer.style.display === 'block';
|
||||||
answer.style.display = isOpen ? 'none' : 'block';
|
answer.style.display = isOpen ? 'none' : 'block';
|
||||||
icon.textContent = isOpen ? '+' : '−';
|
icon.textContent = isOpen ? '+' : '−';
|
||||||
|
// a11y: sync aria-expanded so screen readers know the toggle state
|
||||||
|
button.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// a11y: wire up FAQ buttons with aria-expanded / aria-controls on load
|
||||||
|
function initFaqA11y() {
|
||||||
|
const faqButtons = document.querySelectorAll('.faq-q');
|
||||||
|
faqButtons.forEach((btn, index) => {
|
||||||
|
const item = btn.closest('.faq-item');
|
||||||
|
const answer = item.querySelector('.faq-a');
|
||||||
|
const answerId = `faq-a-${index + 1}`;
|
||||||
|
if (answer) {
|
||||||
|
answer.id = answerId;
|
||||||
|
answer.setAttribute('role', 'region');
|
||||||
|
answer.setAttribute('aria-labelledby', `faq-q-${index + 1}`);
|
||||||
|
}
|
||||||
|
btn.id = `faq-q-${index + 1}`;
|
||||||
|
btn.setAttribute('aria-expanded', 'false');
|
||||||
|
btn.setAttribute('aria-controls', answerId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// a11y: label each view section for screen-reader navigation
|
||||||
|
function initViewA11y() {
|
||||||
|
Object.entries(views).forEach(([key, el]) => {
|
||||||
|
if (el && viewLabels[key]) {
|
||||||
|
el.setAttribute('aria-label', viewLabels[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- U7: Creator Center ---
|
// --- U7: Creator Center ---
|
||||||
|
|
@ -294,7 +354,7 @@
|
||||||
listEl.innerHTML = roles
|
listEl.innerHTML = roles
|
||||||
.map(
|
.map(
|
||||||
(role) => `
|
(role) => `
|
||||||
<article class="role-card" data-role-id="${role.id}">
|
<article class="role-card" data-role-id="${role.id}" role="button" tabindex="0" aria-label="${role.name},${role.status === 'running' ? '运行中' : '已停止'}">
|
||||||
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
<img class="role-card__avatar" src="${role.avatar}" alt="${role.name}" />
|
||||||
<div class="role-card__info">
|
<div class="role-card__info">
|
||||||
<h3 class="role-card__name">${role.name}</h3>
|
<h3 class="role-card__name">${role.name}</h3>
|
||||||
|
|
@ -336,7 +396,9 @@
|
||||||
function switchCenterTab(tab) {
|
function switchCenterTab(tab) {
|
||||||
activeCenterTab = tab;
|
activeCenterTab = tab;
|
||||||
document.querySelectorAll('.center-tab').forEach((t) => {
|
document.querySelectorAll('.center-tab').forEach((t) => {
|
||||||
t.classList.toggle('active', t.dataset.centerTab === tab);
|
const isActive = t.dataset.centerTab === tab;
|
||||||
|
t.classList.toggle('active', isActive);
|
||||||
|
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
document.querySelectorAll('.center-panel').forEach((p) => {
|
document.querySelectorAll('.center-panel').forEach((p) => {
|
||||||
p.classList.toggle('active', p.id === `center-${tab}`);
|
p.classList.toggle('active', p.id === `center-${tab}`);
|
||||||
|
|
@ -571,11 +633,25 @@ ${data.secrets || '无'}
|
||||||
|
|
||||||
function updateTabs() {
|
function updateTabs() {
|
||||||
document.querySelectorAll('.preview-tab').forEach((tab) => {
|
document.querySelectorAll('.preview-tab').forEach((tab) => {
|
||||||
tab.classList.toggle('active', tab.dataset.tab === activePreview);
|
const isActive = tab.dataset.tab === activePreview;
|
||||||
|
tab.classList.toggle('active', isActive);
|
||||||
|
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Event delegation ---
|
// --- Event delegation ---
|
||||||
|
// a11y: keyboard support for role cards (Enter / Space activates them)
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||||
|
const roleCard = e.target.closest('.role-card');
|
||||||
|
if (roleCard && !e.target.closest('[data-action]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const roleId = roleCard.dataset.roleId;
|
||||||
|
renderRoleDetail(roleId);
|
||||||
|
showView('role-detail');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
// FAQ toggle (U4)
|
// FAQ toggle (U4)
|
||||||
const faqBtn = e.target.closest('.faq-q');
|
const faqBtn = e.target.closest('.faq-q');
|
||||||
|
|
@ -826,4 +902,6 @@ ${data.secrets || '无'}
|
||||||
updateSystemPromptPreview();
|
updateSystemPromptPreview();
|
||||||
updateLandingCard();
|
updateLandingCard();
|
||||||
updateTabBar('landing');
|
updateTabBar('landing');
|
||||||
|
initFaqA11y();
|
||||||
|
initViewA11y();
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
60
index.html
60
index.html
|
|
@ -7,9 +7,10 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="styles.css?v=6" />
|
<link rel="stylesheet" href="styles.css?v=7" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<a href="#landing" class="skip-link">跳到主内容</a>
|
||||||
<main class="app">
|
<main class="app">
|
||||||
<!-- P1: Landing View -->
|
<!-- P1: Landing View -->
|
||||||
<section id="landing" class="view view--landing active">
|
<section id="landing" class="view view--landing active">
|
||||||
|
|
@ -53,20 +54,20 @@
|
||||||
<div class="stepper" aria-hidden="true"></div>
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="auth-tabs">
|
<div class="auth-tabs" role="tablist" aria-label="登录与注册">
|
||||||
<button class="auth-tab active" type="button" data-tab="login">登录</button>
|
<button class="auth-tab active" type="button" role="tab" aria-selected="true" data-tab="login">登录</button>
|
||||||
<button class="auth-tab" type="button" data-tab="register">注册</button>
|
<button class="auth-tab" type="button" role="tab" aria-selected="false" data-tab="register">注册</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="login-form" class="auth-form active" autocomplete="off" data-form="login">
|
<form id="login-form" class="auth-form active" autocomplete="on" data-form="login">
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">手机号 / 用户名</span>
|
<span class="field__label">手机号 / 用户名</span>
|
||||||
<input class="field__input" name="account" type="text" placeholder="请输入手机号或用户名" required />
|
<input class="field__input" name="account" type="text" autocomplete="username" placeholder="请输入手机号或用户名" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">密码</span>
|
<span class="field__label">密码</span>
|
||||||
<input class="field__input" name="password" type="password" placeholder="请输入密码" required />
|
<input class="field__input" name="password" type="password" autocomplete="current-password" placeholder="请输入密码" required />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|
@ -75,19 +76,19 @@
|
||||||
<p class="auth-hint">登录后可查看角色库、管理已订阅的角色。</p>
|
<p class="auth-hint">登录后可查看角色库、管理已订阅的角色。</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form id="register-form" class="auth-form" autocomplete="off" data-form="register">
|
<form id="register-form" class="auth-form" autocomplete="on" data-form="register">
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">手机号 / 用户名</span>
|
<span class="field__label">手机号 / 用户名</span>
|
||||||
<input class="field__input" name="account" type="text" placeholder="设置登录账号" required />
|
<input class="field__input" name="account" type="text" autocomplete="username" placeholder="设置登录账号" required />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">密码</span>
|
<span class="field__label">密码</span>
|
||||||
<input class="field__input" name="password" type="password" placeholder="设置密码" required minlength="6" />
|
<input class="field__input" name="password" type="password" autocomplete="new-password" placeholder="设置密码" required minlength="6" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">确认密码</span>
|
<span class="field__label">确认密码</span>
|
||||||
<input class="field__input" name="confirmPassword" type="password" placeholder="再次输入密码" required />
|
<input class="field__input" name="confirmPassword" type="password" autocomplete="new-password" placeholder="再次输入密码" required />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|
@ -108,7 +109,7 @@
|
||||||
<div class="stepper" aria-hidden="true"></div>
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="role-list" id="role-list"></div>
|
<div class="role-list" id="role-list" aria-live="polite" aria-label="角色列表"></div>
|
||||||
|
|
||||||
<div class="empty-state" id="library-empty" hidden>
|
<div class="empty-state" id="library-empty" hidden>
|
||||||
<p class="empty-state__text">你还没有绑定专属创作者</p>
|
<p class="empty-state__text">你还没有绑定专属创作者</p>
|
||||||
|
|
@ -324,15 +325,15 @@
|
||||||
<div class="stepper" aria-hidden="true"></div>
|
<div class="stepper" aria-hidden="true"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="center-tabs">
|
<div class="center-tabs" role="tablist" aria-label="创作者管理中心">
|
||||||
<button class="center-tab active" type="button" data-center-tab="roles">我的角色</button>
|
<button class="center-tab active" type="button" role="tab" aria-selected="true" data-center-tab="roles">我的角色</button>
|
||||||
<button class="center-tab" type="button" data-center-tab="income">收入</button>
|
<button class="center-tab" type="button" role="tab" aria-selected="false" data-center-tab="income">收入</button>
|
||||||
<button class="center-tab" type="button" data-center-tab="settings">我的</button>
|
<button class="center-tab" type="button" role="tab" aria-selected="false" data-center-tab="settings">我的</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Roles -->
|
<!-- Tab: Roles -->
|
||||||
<div class="center-panel active" id="center-roles">
|
<div class="center-panel active" id="center-roles">
|
||||||
<div class="role-list" id="creator-role-list"></div>
|
<div class="role-list" id="creator-role-list" aria-live="polite" aria-label="我的角色列表"></div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn btn--primary btn--block" type="button" data-action="new-role">+ 新建角色</button>
|
<button class="btn btn--primary btn--block" type="button" data-action="new-role">+ 新建角色</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -346,7 +347,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="income-section">
|
<div class="income-section">
|
||||||
<h3 class="income-section__title">流水明细</h3>
|
<h3 class="income-section__title">流水明细</h3>
|
||||||
<div class="income-list" id="income-list">
|
<div class="income-list" id="income-list" aria-live="polite" aria-label="收入流水">
|
||||||
<p class="income-empty">暂无流水记录</p>
|
<p class="income-empty">暂无流水记录</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -561,9 +562,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-panel__preview">
|
<div class="result-panel__preview">
|
||||||
<div class="preview-tabs">
|
<div class="preview-tabs" role="tablist" aria-label="配置文件预览">
|
||||||
<button class="preview-tab active" type="button" data-tab="soul">Soul.md</button>
|
<button class="preview-tab active" type="button" role="tab" aria-selected="true" data-tab="soul">Soul.md</button>
|
||||||
<button class="preview-tab" type="button" data-tab="config">config.yaml</button>
|
<button class="preview-tab" type="button" role="tab" aria-selected="false" data-tab="config">config.yaml</button>
|
||||||
</div>
|
</div>
|
||||||
<pre class="preview-code" id="preview-code"><code></code></pre>
|
<pre class="preview-code" id="preview-code"><code></code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -576,21 +577,24 @@
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Tab Bar (U8) -->
|
<!-- Tab Bar (U8) -->
|
||||||
<nav class="tab-bar" id="tab-bar">
|
<nav class="tab-bar" id="tab-bar" role="tablist" aria-label="主导航">
|
||||||
<button class="tab-bar__item active" type="button" data-tab-action="tab-home">
|
<button class="tab-bar__item active" type="button" role="tab" aria-selected="true" data-tab-action="tab-home">
|
||||||
<span class="tab-bar__icon">⌂</span>
|
<span class="tab-bar__icon" aria-hidden="true">⌂</span>
|
||||||
<span class="tab-bar__label">首页</span>
|
<span class="tab-bar__label">首页</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-bar__item" type="button" data-tab-action="tab-distill">
|
<button class="tab-bar__item" type="button" role="tab" aria-selected="false" data-tab-action="tab-distill">
|
||||||
<span class="tab-bar__icon">♡</span>
|
<span class="tab-bar__icon" aria-hidden="true">♡</span>
|
||||||
<span class="tab-bar__label">蒸馏前任</span>
|
<span class="tab-bar__label">蒸馏前任</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-bar__item" type="button" data-tab-action="tab-mine">
|
<button class="tab-bar__item" type="button" role="tab" aria-selected="false" data-tab-action="tab-mine">
|
||||||
<span class="tab-bar__icon">☺</span>
|
<span class="tab-bar__icon" aria-hidden="true">☺</span>
|
||||||
<span class="tab-bar__label" id="tab-mine-label">我的</span>
|
<span class="tab-bar__label" id="tab-mine-label">我的</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Screen reader live region for view change announcements -->
|
||||||
|
<div id="sr-announce" class="visually-hidden" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
77
styles.css
77
styles.css
|
|
@ -433,7 +433,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.field__input::placeholder {
|
.field__input::placeholder {
|
||||||
color: rgba(154, 163, 194, 0.45);
|
color: rgba(154, 163, 194, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field__input:focus {
|
.field__input:focus {
|
||||||
|
|
@ -1339,3 +1339,78 @@ textarea.field__input--tall {
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Accessibility (a11y) ===== */
|
||||||
|
|
||||||
|
/* Visually hidden, but available to screen readers */
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Visible keyboard focus for all interactive elements */
|
||||||
|
.btn:focus-visible,
|
||||||
|
.icon-btn:focus-visible,
|
||||||
|
.tab-bar__item:focus-visible,
|
||||||
|
.auth-tab:focus-visible,
|
||||||
|
.center-tab:focus-visible,
|
||||||
|
.faq-q:focus-visible,
|
||||||
|
.preview-tab:focus-visible,
|
||||||
|
.footer-link:focus-visible,
|
||||||
|
.role-card:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-soft);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Views can receive focus via JS for screen-reader navigation */
|
||||||
|
.view {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FAQ answer region: ensure it's focusable-friendly */
|
||||||
|
.faq-a {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skip link for keyboard users */
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
left: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: top 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Respect reduced motion preference */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue