Files
kennethreitzandClaude Fable 5 087e5a59d8 Second pass on the family tree landing: let it breathe
The first pass only tokenized colors; the page was still squeezed into
a 55% container (~420px) with boxes nested in boxes. Now:

- container widened to a 56rem measure; the intro reads at full prose
  width with per-section measures instead of one global squeeze
- Find a Person is an open lookup block with a bronze label, not a
  320px card
- Interactive Tree / Messianic Lineage are proper hero cards with
  bronze "Explore the tree →" / "Trace the line →" affordances
- the Overview is a stat-card row in the manner of the books index
  (479 TOTAL PEOPLE, 41 TOTAL GENERATIONS...), value-first markup with
  the JS-populated ids preserved
- Overview/The Generations headings take the gold-tipped small-caps
  treatment; generation rows get bronze small-caps person counts and
  accent hover; section boxes unboxed

All 976 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:49:51 -04:00

939 lines
28 KiB
HTML

{% extends "base.html" %}
{% block title %}Biblical Family Tree - KJV Study{% endblock %}
{% block description %}Explore biblical genealogies from Adam to Jesus Christ.{% endblock %}
{% block head %}
<style>
{% set female_names = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'ruth', 'mary', 'tamar', 'rahab', 'bathsheba', 'dinah', 'keturah', 'hagar', 'zilpah', 'bilhah', 'jochebed', 'miriam', 'deborah', 'hannah', 'abigail', 'esther', 'naomi', 'naamah', 'milcah', 'adah', 'zillah', 'asenath', 'basemath'] %}
.family-tree-page {
max-width: 56rem;
}
.page-header {
margin-bottom: 1.5rem;
}
.page-header h1 {
margin-bottom: 0.25rem;
}
.page-header .subtitle {
color: var(--text-secondary);
margin: 0;
}
.page-intro {
max-width: 46rem;
font-size: 1.2rem;
margin-bottom: 2rem;
line-height: 1.9;
}
/* Search — an open lookup block, not a cramped box */
.search-card {
max-width: 42rem;
margin-bottom: 2.5rem;
}
.search-card h2 {
font-size: 0.72rem !important;
font-weight: 600;
font-style: normal !important;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--bronze) !important;
margin: 0 0 0.6rem 0;
}
.search-form {
position: relative;
}
.search-form input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 1px solid var(--border-color-darker);
border-radius: 8px;
background: var(--bg-elevated);
color: var(--text-color);
font-family: inherit;
box-sizing: border-box;
box-shadow: var(--shadow-soft);
}
.search-form input::placeholder {
color: var(--text-tertiary);
font-style: italic;
}
.search-form input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-elevated);
border: 1px solid var(--border-color-darker);
border-top: none;
border-radius: 0 0 8px 8px;
box-shadow: var(--shadow-pop);
max-height: 280px;
overflow-y: auto;
display: none;
z-index: 10;
}
.search-dropdown.open {
display: block;
}
.search-dropdown button {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
text-align: left;
font-size: 0.95rem;
cursor: pointer;
font-family: inherit;
}
.search-dropdown button:hover,
.search-dropdown button.active {
background: var(--code-bg);
}
/* Feature cards — the two grand doors into the tree */
.feature-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;
max-width: 52rem;
margin-bottom: 2.5rem;
}
.feature-card {
display: flex;
flex-direction: column;
padding: 1.6rem 1.75rem 1.4rem;
border: 1px solid var(--border-color-dark);
border-radius: 8px;
background: var(--bg-elevated);
transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.feature-card:hover {
box-shadow: var(--shadow-soft);
border-color: var(--accent);
transform: translateY(-2px);
}
.feature-card h3 {
font-size: 1.35rem;
font-style: normal;
margin: 0 0 0.6rem 0;
}
.feature-card h3 a {
color: var(--text-strong) !important;
text-decoration: none;
border-bottom: none;
transition: color 0.15s ease;
}
.feature-card:hover h3 a {
color: var(--accent-strong) !important;
}
.feature-card p {
font-size: 0.95rem;
color: var(--text-secondary);
margin: 0 0 1rem;
line-height: 1.65;
flex: 1;
}
.feature-go {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--bronze);
}
.feature-card:hover .feature-go {
color: var(--accent-strong);
}
/* Overview — stat cards in the manner of the books index */
.stats-card {
max-width: 52rem;
margin-bottom: 2.5rem;
}
.stats-card h2,
.section-card h2 {
position: relative;
font-size: 0.95rem !important;
font-weight: 600;
font-style: normal !important;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--text-secondary) !important;
margin: 0 0 1.25rem 0;
padding-bottom: 0.6rem;
border-bottom: 1px solid var(--border-color);
}
.stats-card h2::after,
.section-card h2::after {
content: '';
position: absolute;
left: 0;
bottom: -1px;
width: 2.75rem;
height: 1px;
background: var(--gold);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem;
}
.stat-item {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
padding: 1.1rem 1rem;
background: var(--bg-elevated);
border: 1px solid var(--border-color-dark);
border-radius: 8px;
}
.stat-value {
font-family: et-book-roman-old-style, et-book, Georgia, serif;
font-size: 1.45rem;
font-weight: 400;
color: var(--text-strong);
line-height: 1.35;
}
.stat-value a {
color: var(--accent-strong);
border-bottom: none;
}
.stat-label {
font-size: 0.64rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--bronze);
margin-top: 0.4rem;
}
/* Generation list */
.section-card {
max-width: 52rem;
margin-bottom: 2rem;
}
.generation-grid {
display: grid;
gap: 0.75rem;
}
.gen-card {
display: flex;
align-items: baseline;
gap: 0.75rem;
padding: 0.7rem 1rem;
border: 1px solid var(--border-color-dark);
border-radius: 8px;
background: var(--bg-elevated);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.gen-card:hover {
border-color: var(--accent);
box-shadow: var(--shadow-soft);
}
.gen-card-num {
font-weight: 600;
min-width: 40px;
color: var(--accent-strong);
}
.gen-card-num a {
color: inherit;
text-decoration: none;
}
.gen-card-num a:hover {
text-decoration: underline;
}
.gen-card-count {
font-size: 0.64rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--bronze);
min-width: 90px;
}
.gen-card-people {
flex: 1;
font-size: 0.9rem;
line-height: 1.6;
}
.gen-card-people a {
color: var(--text-secondary);
}
/* Notable figures grid */
.notable-grid {
display: grid;
gap: 0.75rem;
}
.person-card {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-elevated);
transition: background 0.15s ease;
}
.person-card:hover {
background: var(--code-bg);
}
.person-card.male {
border-left: 3px solid var(--cat-law);
}
.person-card.female {
border-left: 3px solid var(--cat-general);
}
.person-card.kekule {
border-left-color: var(--gold);
background: linear-gradient(to right, var(--gold-soft), var(--bg-elevated));
}
.person-card-header {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.person-card-name {
font-size: 1.1rem;
font-weight: 600;
}
.person-card-name a {
color: inherit;
text-decoration: none;
}
.person-card-name a:hover {
text-decoration: underline;
}
.person-card-badge {
font-size: 0.62rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 0.15rem 0.55rem;
border-radius: 999px;
background: var(--gold-soft);
border: 1px solid var(--border-color-dark);
color: var(--bronze);
}
.person-card-meta {
font-size: 0.85rem;
color: var(--text-secondary);
}
.person-card-details {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.6;
}
.person-card-details a {
color: var(--accent-strong);
}
.person-card-verse {
font-size: 0.85rem;
color: var(--text-secondary);
font-style: italic;
margin-top: 0.25rem;
}
.person-card-verse a {
color: var(--accent-strong);
}
@media (max-width: 768px) {
.family-tree-page {
max-width: 100%;
}
.feature-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
/* Dark mode */
</style>
{% endblock %}
{% block content %}
{% set female_names = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'ruth', 'mary', 'tamar', 'rahab', 'bathsheba', 'dinah', 'keturah', 'hagar', 'zilpah', 'bilhah', 'jochebed', 'miriam', 'deborah', 'hannah', 'abigail', 'esther', 'naomi', 'naamah', 'milcah', 'adah', 'zillah', 'asenath', 'basemath'] %}
<div class="family-tree-page">
<div class="page-header">
<span class="page-kicker">The Generations of Scripture</span>
<h1>Biblical Family Tree</h1>
<p class="subtitle">From Adam to Jesus Christ</p>
</div>
<p class="page-intro">
The Bible contains detailed genealogies that trace God's plan through specific family lines, culminating in the birth of Jesus Christ. This record spans from the creation of Adam through countless generations to the birth of our Lord.
</p>
<!-- Search -->
<div class="search-card">
<h2>Find a Person</h2>
<div class="search-form">
<form method="get" action="/family-tree/search" id="family-tree-search-form">
<input
type="text"
name="q"
placeholder="Search for a person..."
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
data-form-type="other"
id="family-tree-search-input"
>
</form>
<div class="search-dropdown" id="family-tree-search-dropdown"></div>
</div>
</div>
<!-- Feature Cards -->
<div class="feature-grid">
<div class="feature-card">
<h3><a href="/family-tree/interactive">Interactive Tree</a></h3>
<p>Explore the genealogy with a collapsible text-based tree. Expand and collapse branches, search for any person, and navigate with keyboard shortcuts.</p>
<span class="feature-go">Explore the tree →</span>
</div>
<div class="feature-card">
<h3><a href="/family-tree/lineage">Messianic Lineage</a></h3>
<p>View the direct paternal line from Adam to Jesus Christ in a vertical genealogy chart showing each generation.</p>
<span class="feature-go">Trace the line →</span>
</div>
</div>
{% if family_tree_data and generations %}
<!-- Statistics -->
<div class="stats-card">
<h2>Overview</h2>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="stat-total-people">{{ family_tree_data|length }}</span>
<span class="stat-label">Total People</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-total-generations">{{ generations|length }}</span>
<span class="stat-label">Total Generations</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-longest-lived">Loading...</span>
<span class="stat-label">Longest Lived</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-most-children">Loading...</span>
<span class="stat-label">Most Children</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-most-siblings">Loading...</span>
<span class="stat-label">Most Siblings</span>
</div>
<div class="stat-item">
<span class="stat-value" id="stat-close-marriages">Loading...</span>
<span class="stat-label">Close Family Marriages</span>
</div>
</div>
</div>
<!-- Generations -->
<div class="section-card">
<h2>The Generations</h2>
<div class="generation-grid">
{% for gen_num in generations.keys() | sort %}
<div class="gen-card">
<div class="gen-card-num">
<a href="/family-tree/generation/{{ gen_num }}">Gen {{ gen_num }}</a>
</div>
<div class="gen-card-count">{{ generations[gen_num]|length }} person{% if generations[gen_num]|length != 1 %}s{% endif %}</div>
<div class="gen-card-people">
{% for person_id in generations[gen_num][:8] %}
<a href="/family-tree/person/{{ person_id }}">{{ family_tree_data[person_id].name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}{% if generations[gen_num]|length > 8 %}, +{{ generations[gen_num]|length - 8 }} more{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Notable Figures -->
<div class="section-card">
<h2>Notable Figures</h2>
<div class="notable-grid">
{% set notable = ["Adam", "Noah", "Abraham", "Isaac", "Jacob", "Joseph", "Moses", "David", "Solomon", "Jesus"] %}
{% for person_id, person in family_tree_data.items() %}
{% if person.name in notable %}
{% set is_female = person.name|lower in female_names or (female_names | select('in', person.name|lower) | list | length > 0) %}
<div class="person-card{% if person.kekule_number is not none %} kekule{% elif is_female %} female{% else %} male{% endif %}">
<div class="person-card-header">
<span class="person-card-name">
<a href="/family-tree/person/{{ person_id }}">{{ person.name }}</a>
</span>
{% if person.kekule_number is not none %}
<span class="person-card-badge">Kekulé #{{ person.kekule_number }}</span>
{% endif %}
</div>
<div class="person-card-meta">
{% if person.generation %}Generation {{ person.generation }}{% endif %}{% if person.birth_year != "Unknown" %} · Born {{ person.birth_year }}{% endif %}{% if person.age_at_death != "Unknown" %} · Lived {{ person.age_at_death }}{% endif %}
</div>
<div class="person-card-details">
{% if person.spouse %}
Married to {{ person.spouse }}.
{% endif %}
{% if person.parents|length > 0 %}
Child of
{% for parent_id in person.parents %}
{% if parent_id in family_tree_data %}<a href="/family-tree/person/{{ parent_id }}">{{ family_tree_data[parent_id].name }}</a>{% if not loop.last %} and {% endif %}{% endif %}
{% endfor %}.
{% endif %}
{% if person.children|length > 0 %}
Father of
{% for child_id in person.children[:3] %}
{% if child_id in family_tree_data %}<a href="/family-tree/person/{{ child_id }}">{{ family_tree_data[child_id].name }}</a>{% if not loop.last %}, {% endif %}{% endif %}
{% endfor %}{% if person.children|length > 3 %}, +{{ person.children|length - 3 }} more{% endif %}.
{% endif %}
</div>
{% if person.verses %}
<div class="person-card-verse">
{% for verse in person.verses[:1] %}
{% set ref_parts = verse.reference.split(' ') %}
{% if ref_parts|length >= 2 %}
{% set chapter_verse = ref_parts[-1] %}
{% if ':' in chapter_verse %}
{% set chapter = chapter_verse.split(':')[0] %}
{% set verse_part = chapter_verse.split(':')[1] %}
{% if '-' in verse_part %}
{% set verse_num = verse_part.split('-')[0] %}
{% else %}
{% set verse_num = verse_part %}
{% endif %}
{% set book = ' '.join(ref_parts[:-1]) %}
<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num }}">{{ verse.reference }}</a>
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% else %}
<div class="section-card">
<p>Family tree data could not be loaded.</p>
</div>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Fetch and display family tree statistics
fetch('/api/family-tree/stats')
.then(response => response.json())
.then(data => {
// Update the statistics
document.getElementById('stat-total-people').textContent = data.total_people;
document.getElementById('stat-total-generations').textContent = data.total_generations;
// Longest lived person
if (data.longest_lived && data.longest_lived.value > 0) {
const personName = data.longest_lived.name;
const personId = data.longest_lived.person_id;
document.getElementById('stat-longest-lived').innerHTML =
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.longest_lived.value} yrs)`;
} else {
document.getElementById('stat-longest-lived').textContent = '—';
}
// Most children
if (data.most_children && data.most_children.value > 0) {
const personName = data.most_children.name;
const personId = data.most_children.person_id;
document.getElementById('stat-most-children').innerHTML =
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.most_children.value})`;
} else {
document.getElementById('stat-most-children').textContent = '—';
}
// Most siblings
if (data.most_siblings && data.most_siblings.value > 0) {
const personName = data.most_siblings.name;
const personId = data.most_siblings.person_id;
document.getElementById('stat-most-siblings').innerHTML =
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.most_siblings.value})`;
} else {
document.getElementById('stat-most-siblings').textContent = '—';
}
// Close family marriages
if (data.close_family_marriages !== undefined) {
document.getElementById('stat-close-marriages').textContent = data.close_family_marriages;
} else {
document.getElementById('stat-close-marriages').textContent = '—';
}
})
.catch(error => {
console.error('Error loading family tree statistics:', error);
document.getElementById('stat-longest-lived').textContent = '—';
document.getElementById('stat-most-children').textContent = '—';
document.getElementById('stat-most-siblings').textContent = '—';
document.getElementById('stat-close-marriages').textContent = '—';
});
// Search functionality
const names = {{ person_names|default([])|tojson }};
const input = document.getElementById('family-tree-search-input');
const dropdown = document.getElementById('family-tree-search-dropdown');
const form = document.getElementById('family-tree-search-form');
if (!input || !dropdown || !form || names.length === 0) {
return;
}
let filtered = [];
let activeIndex = -1;
function hideDropdown() {
dropdown.classList.remove('open');
dropdown.innerHTML = '';
filtered = [];
activeIndex = -1;
}
function renderDropdown(items) {
if (!items.length) {
hideDropdown();
return;
}
dropdown.innerHTML = items
.map((name, idx) => `<button type="button" data-index="${idx}">${name}</button>`)
.join('');
dropdown.classList.add('open');
activeIndex = -1;
}
function setActive(index) {
const buttons = dropdown.querySelectorAll('button');
buttons.forEach(btn => btn.classList.remove('active'));
if (buttons[index]) {
buttons[index].classList.add('active');
}
}
function chooseName(name) {
input.value = name;
hideDropdown();
form.submit();
}
input.addEventListener('input', function() {
const query = input.value.trim().toLowerCase();
if (!query) {
hideDropdown();
return;
}
filtered = names.filter(name => name.toLowerCase().includes(query)).slice(0, 8);
renderDropdown(filtered);
});
input.addEventListener('keydown', function(event) {
if (!dropdown.classList.contains('open')) {
return;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
activeIndex = (activeIndex + 1) % filtered.length;
setActive(activeIndex);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
activeIndex = (activeIndex - 1 + filtered.length) % filtered.length;
setActive(activeIndex);
} else if (event.key === 'Enter') {
if (activeIndex >= 0 && filtered[activeIndex]) {
event.preventDefault();
chooseName(filtered[activeIndex]);
}
} else if (event.key === 'Escape') {
hideDropdown();
}
});
dropdown.addEventListener('mousedown', function(event) {
const button = event.target.closest('button');
if (!button) {
return;
}
const index = Number(button.dataset.index);
if (!Number.isNaN(index) && filtered[index]) {
chooseName(filtered[index]);
}
});
document.addEventListener('click', function(event) {
if (!dropdown.contains(event.target) && event.target !== input) {
hideDropdown();
}
});
// Keyboard navigation for page elements
const featureCards = Array.from(document.querySelectorAll('.feature-card'));
const genCards = Array.from(document.querySelectorAll('.gen-card'));
const personCards = Array.from(document.querySelectorAll('.person-card'));
// Combine all navigable items
const allItems = featureCards.concat(genCards).concat(personCards);
if (allItems.length === 0) return;
let selectedIndex = -1;
// Define section boundaries
const featureEnd = featureCards.length;
const genEnd = featureEnd + genCards.length;
function getLink(item) {
const link = item.querySelector('a');
return link ? link.href : null;
}
function selectItem(index) {
// Remove previous selection
if (selectedIndex >= 0 && selectedIndex < allItems.length) {
allItems[selectedIndex].style.outline = '';
allItems[selectedIndex].style.outlineOffset = '';
allItems[selectedIndex].classList.remove('selected');
}
// Bounds check
selectedIndex = Math.max(0, Math.min(index, allItems.length - 1));
// Add selection
allItems[selectedIndex].style.outline = '2px solid var(--accent)';
allItems[selectedIndex].style.outlineOffset = '2px';
allItems[selectedIndex].classList.add('selected');
// Scroll into view
allItems[selectedIndex].scrollIntoView({
behavior: 'auto',
block: 'center'
});
}
function getGridColumns() {
// Feature cards are 2 columns, others are 1 column
if (selectedIndex < featureEnd) {
return window.innerWidth <= 768 ? 1 : 2;
}
return 1;
}
function moveUp() {
const cols = getGridColumns();
if (selectedIndex < featureEnd) {
// In feature section - move up by cols, or jump to before section
const newIndex = selectedIndex - cols;
if (newIndex >= 0) {
selectItem(newIndex);
}
} else if (selectedIndex < genEnd) {
// In generation section - up moves to previous gen or last feature
if (selectedIndex === featureEnd) {
selectItem(featureEnd - 1);
} else {
selectItem(selectedIndex - 1);
}
} else {
// In person section - up moves to previous person or last gen
if (selectedIndex === genEnd) {
selectItem(genEnd - 1);
} else {
selectItem(selectedIndex - 1);
}
}
}
function moveDown() {
const cols = getGridColumns();
if (selectedIndex < featureEnd) {
// In feature section - move down by cols or jump to generations
const newIndex = selectedIndex + cols;
if (newIndex < featureEnd) {
selectItem(newIndex);
} else {
selectItem(featureEnd);
}
} else if (selectedIndex < genEnd) {
// In generation section - down moves to next gen or first person
if (selectedIndex === genEnd - 1) {
selectItem(genEnd);
} else {
selectItem(selectedIndex + 1);
}
} else {
// In person section - down moves to next person
selectItem(selectedIndex + 1);
}
}
function moveLeft() {
if (selectedIndex < featureEnd) {
// In feature section - move left within row
if (selectedIndex > 0) {
selectItem(selectedIndex - 1);
} else {
history.back();
}
} else {
// Other sections have no left movement, go back
history.back();
}
}
function moveRight() {
if (selectedIndex < featureEnd) {
// In feature section - move right within row
const cols = getGridColumns();
const row = Math.floor(selectedIndex / cols);
const nextInRow = (row * cols) + ((selectedIndex % cols) + 1);
if (nextInRow < featureEnd && nextInRow < (row + 1) * cols) {
selectItem(nextInRow);
}
}
// Other sections have no right movement
}
document.addEventListener('keydown', function(e) {
// Don't handle if user is typing in an input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (KJVNav.sidebarActive) return;
if (e.key === 'ArrowDown' || e.key === 'j') {
e.preventDefault();
if (selectedIndex < 0) {
selectItem(0);
} else {
moveDown();
}
} else if (e.key === 'ArrowUp' || e.key === 'k') {
e.preventDefault();
if (selectedIndex < 0) {
selectItem(0);
} else {
moveUp();
}
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
if (selectedIndex < 0) {
history.back();
} else {
moveLeft();
}
} else if (e.key === 'ArrowRight' || e.key === 'l') {
e.preventDefault();
if (selectedIndex >= 0) {
moveRight();
}
} else if (e.key === 'Enter' && selectedIndex >= 0) {
e.preventDefault();
const href = getLink(allItems[selectedIndex]);
if (href) window.location.href = href;
} else if (e.key === 'Escape') {
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < allItems.length) {
allItems[selectedIndex].style.outline = '';
allItems[selectedIndex].style.outlineOffset = '';
allItems[selectedIndex].classList.remove('selected');
}
selectedIndex = -1;
}
});
});
</script>
{% endblock %}