Adds the entire /<faithLang>/{apologetik,apologetics} section:
- Landing page introducing the contra/pro split with shield/flame cards.
- Contra (objections): 23 objections, each answered by multiple archetype
voices (Aquinas, Pascal, Augustine, Lewis, Chesterton, plus Logician,
Mystic, Scientist, Pastor archetypes); index + per-argument detail pages
with archetype filter and inter-argument navigation.
- Pro (positive case): 12 arguments across three layers (supernatural,
theism, christianity) voiced by Habermas, Polkinghorne, Newman, Hart,
Lewis, Wright, Hahn, Plantinga, Eliade, Feser, Chesterton, Guénon;
cumulative-case visual + per-argument detail pages.
- DE/EN content via per-language data modules; LA stub layout 307-redirects
to English.
- Per-language slug via apologetikSlug matcher; canonical-slug enforcement
redirects mismatches.
- Shared ApologetikToc component (also reused on zehn-gebote katechese).
- CaseTabs component for contra/pro switching.
- DeepL translation script for regenerating DE data from EN source.
- Server-side scripture lookup helper.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.48.1",
|
||||
"version": "1.49.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Translates apologetik English data → target language via DeepL.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm exec vite-node scripts/translate-apologetik.ts # default DE
|
||||
* pnpm exec vite-node scripts/translate-apologetik.ts -- --lang=DE
|
||||
*
|
||||
* Reads: src/lib/data/apologetik.ts (English source of truth)
|
||||
* Writes: src/lib/data/apologetik.<lang>.ts
|
||||
*
|
||||
* Note: DeepL does not support Latin. For LA, translate manually or wire a
|
||||
* different provider.
|
||||
*/
|
||||
|
||||
import { writeFileSync, readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// Minimal .env loader — avoid extra deps.
|
||||
function loadEnv() {
|
||||
try {
|
||||
const raw = readFileSync(resolve(process.cwd(), '.env'), 'utf8');
|
||||
for (const line of raw.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let value = trimmed.slice(eq + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (!(key in process.env)) process.env[key] = value;
|
||||
}
|
||||
} catch {
|
||||
// no .env — fine, rely on process env
|
||||
}
|
||||
}
|
||||
loadEnv();
|
||||
|
||||
import {
|
||||
ARCHETYPES,
|
||||
ARGUMENTS,
|
||||
POS_VOICES,
|
||||
POS_LAYERS,
|
||||
POS_ARGUMENTS,
|
||||
type Archetype,
|
||||
type Argument,
|
||||
type Counter,
|
||||
type PosVoice,
|
||||
type PosLayer,
|
||||
type PosArgument,
|
||||
type PosCounter
|
||||
} from '../src/lib/data/apologetik';
|
||||
|
||||
const DEEPL_API_KEY = process.env.DEEPL_API_KEY;
|
||||
const DEEPL_API_URL = process.env.DEEPL_API_URL || 'https://api-free.deepl.com/v2/translate';
|
||||
|
||||
if (!DEEPL_API_KEY) {
|
||||
console.error('DEEPL_API_KEY missing from .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const argLang = process.argv.find((a) => a.startsWith('--lang='))?.split('=')[1];
|
||||
const TARGET_LANG = (argLang ?? 'DE').toUpperCase();
|
||||
const FILE_LANG = TARGET_LANG.toLowerCase();
|
||||
|
||||
const BATCH_SIZE = 50;
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
// Manual overrides applied after DeepL translation, keyed by English source.
|
||||
// Use for cases where DeepL produces a wrong / inconsistent German rendering
|
||||
// that should survive regeneration.
|
||||
const OVERRIDES: Record<string, Record<string, string>> = {
|
||||
DE: {
|
||||
// generic-masculine for archetype role names
|
||||
'The Scientist': 'Der Wissenschaftler'
|
||||
}
|
||||
};
|
||||
|
||||
async function translateBatch(texts: string[]): Promise<string[]> {
|
||||
const out: string[] = [];
|
||||
const toFetch: { idx: number; text: string }[] = [];
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
const cached = cache.get(texts[i]);
|
||||
if (cached !== undefined) out[i] = cached;
|
||||
else toFetch.push({ idx: i, text: texts[i] });
|
||||
}
|
||||
for (let i = 0; i < toFetch.length; i += BATCH_SIZE) {
|
||||
const chunk = toFetch.slice(i, i + BATCH_SIZE);
|
||||
const body = {
|
||||
text: chunk.map((c) => c.text),
|
||||
source_lang: 'EN',
|
||||
target_lang: TARGET_LANG,
|
||||
preserve_formatting: true,
|
||||
formality: 'prefer_more'
|
||||
};
|
||||
const resp = await fetch(DEEPL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${DEEPL_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const t = await resp.text();
|
||||
throw new Error(`DeepL ${resp.status}: ${t}`);
|
||||
}
|
||||
const data = (await resp.json()) as { translations: { text: string }[] };
|
||||
data.translations.forEach((tr, j) => {
|
||||
const slot = chunk[j];
|
||||
out[slot.idx] = tr.text;
|
||||
cache.set(slot.text, tr.text);
|
||||
});
|
||||
process.stdout.write(` · translated ${Math.min(i + BATCH_SIZE, toFetch.length)}/${toFetch.length}\n`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Helper: collect translatable strings from an object's selected fields,
|
||||
// queue them, and return a setter that applies the translations back.
|
||||
type Job = {
|
||||
get: () => string;
|
||||
set: (v: string) => void;
|
||||
};
|
||||
|
||||
const jobs: Job[] = [];
|
||||
|
||||
function field<T extends object, K extends keyof T>(obj: T, key: K) {
|
||||
if (typeof obj[key] !== 'string') return;
|
||||
jobs.push({
|
||||
get: () => obj[key] as unknown as string,
|
||||
set: (v) => {
|
||||
(obj as any)[key] = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function arrayField<T>(arr: T[], key: keyof T) {
|
||||
for (const item of arr) field(item as any, key as any);
|
||||
}
|
||||
|
||||
function stringArray(arr: string[]) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const idx = i;
|
||||
jobs.push({
|
||||
get: () => arr[idx],
|
||||
set: (v) => {
|
||||
arr[idx] = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- clone source data ----------
|
||||
function cloneArchetype(a: Archetype): Archetype {
|
||||
return { ...a };
|
||||
}
|
||||
function cloneCounter(c: Counter): Counter {
|
||||
return { ...c, body: [...c.body], cites: [...c.cites] };
|
||||
}
|
||||
function cloneArgument(a: Argument): Argument {
|
||||
const counters: Record<string, Counter> = {};
|
||||
for (const [k, v] of Object.entries(a.counters)) counters[k] = cloneCounter(v);
|
||||
return { ...a, related: [...a.related], counters };
|
||||
}
|
||||
function clonePosVoice(v: PosVoice): PosVoice {
|
||||
return { ...v };
|
||||
}
|
||||
function clonePosLayer(l: PosLayer): PosLayer {
|
||||
return { ...l };
|
||||
}
|
||||
function clonePosCounter(c: PosCounter): PosCounter {
|
||||
return { ...c, body: [...c.body], cites: [...c.cites] };
|
||||
}
|
||||
function clonePosArgument(a: PosArgument): PosArgument {
|
||||
const voices: Record<string, PosCounter> = {};
|
||||
for (const [k, v] of Object.entries(a.voices)) voices[k] = clonePosCounter(v);
|
||||
return {
|
||||
...a,
|
||||
related: [...a.related],
|
||||
voices,
|
||||
scripture: { ...a.scripture }
|
||||
};
|
||||
}
|
||||
|
||||
const archetypesOut: Record<string, Archetype> = {};
|
||||
for (const [k, v] of Object.entries(ARCHETYPES)) archetypesOut[k] = cloneArchetype(v);
|
||||
const argumentsOut: Argument[] = ARGUMENTS.map(cloneArgument);
|
||||
const posVoicesOut: Record<string, PosVoice> = {};
|
||||
for (const [k, v] of Object.entries(POS_VOICES)) posVoicesOut[k] = clonePosVoice(v);
|
||||
const posLayersOut: PosLayer[] = POS_LAYERS.map(clonePosLayer);
|
||||
const posArgsOut: PosArgument[] = POS_ARGUMENTS.map(clonePosArgument);
|
||||
|
||||
// ---------- queue translation jobs ----------
|
||||
//
|
||||
// What we DON'T translate:
|
||||
// - id, n, related (cross-link keys)
|
||||
// - color, colorSoft, colorHex, glyph, font (visual)
|
||||
// - era (numeric / dates)
|
||||
// - cites (bibliographic — keep canonical English)
|
||||
// - scripture.ref (book chapter:verse)
|
||||
// - layer (enum key)
|
||||
// - strength (number)
|
||||
|
||||
// archetypes — translate name + sub. DeepL leaves canonical proper nouns alone
|
||||
// (e.g. "Pascal") and localizes ones with established forms ("Thomas von Aquin",
|
||||
// "Franz von Assisi", "Augustinus"). Role names ("The Logician") get translated
|
||||
// idiomatically.
|
||||
for (const a of Object.values(archetypesOut)) {
|
||||
field(a, 'name');
|
||||
field(a, 'sub');
|
||||
}
|
||||
|
||||
// arguments
|
||||
for (const a of argumentsOut) {
|
||||
field(a, 'title');
|
||||
field(a, 'short');
|
||||
field(a, 'steel');
|
||||
field(a, 'quote');
|
||||
field(a, 'quoteBy');
|
||||
field(a, 'pub');
|
||||
for (const c of Object.values(a.counters)) {
|
||||
field(c, 'lede');
|
||||
stringArray(c.body);
|
||||
}
|
||||
}
|
||||
|
||||
// pos voices — translate name + sub (same rationale as archetypes).
|
||||
for (const v of Object.values(posVoicesOut)) {
|
||||
field(v, 'name');
|
||||
field(v, 'sub');
|
||||
}
|
||||
|
||||
// pos layers
|
||||
for (const l of posLayersOut) {
|
||||
field(l, 'title');
|
||||
field(l, 'sub');
|
||||
}
|
||||
|
||||
// pos arguments
|
||||
for (const a of posArgsOut) {
|
||||
field(a, 'title');
|
||||
field(a, 'claim');
|
||||
field(a, 'thesis');
|
||||
if (a.note) field(a, 'note');
|
||||
field(a.scripture, 'text');
|
||||
for (const c of Object.values(a.voices)) {
|
||||
field(c, 'lede');
|
||||
stringArray(c.body);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Queued ${jobs.length} translation jobs · target ${TARGET_LANG}`);
|
||||
|
||||
// Site is Swiss High German — no ß. Bible quotes are sourced from Allioli at
|
||||
// runtime and untouched by this pass, so this only affects translated prose.
|
||||
function postProcess(s: string): string {
|
||||
if (TARGET_LANG === 'DE') return s.replace(/ß/g, 'ss');
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- run translations ----------
|
||||
const inputs = jobs.map((j) => j.get());
|
||||
const outputs = await translateBatch(inputs);
|
||||
const overrides = OVERRIDES[TARGET_LANG] ?? {};
|
||||
let overrideHits = 0;
|
||||
jobs.forEach((j, i) => {
|
||||
const en = inputs[i];
|
||||
if (overrides[en] !== undefined) {
|
||||
j.set(postProcess(overrides[en]));
|
||||
overrideHits++;
|
||||
} else {
|
||||
j.set(postProcess(outputs[i]));
|
||||
}
|
||||
});
|
||||
if (overrideHits) console.log(`Applied ${overrideHits} manual override(s)`);
|
||||
|
||||
console.log(`Done · cache hits saved ${jobs.length - cache.size} duplicate calls`);
|
||||
|
||||
// ---------- emit file ----------
|
||||
function ts(value: unknown, indent = 0): string {
|
||||
const pad = '\t'.repeat(indent);
|
||||
if (value === null) return 'null';
|
||||
if (typeof value === 'string') return JSON.stringify(value);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return '[]';
|
||||
const inner = value.map((v) => `${pad}\t${ts(v, indent + 1)}`).join(',\n');
|
||||
return `[\n${inner}\n${pad}]`;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const entries = Object.entries(value as object);
|
||||
if (entries.length === 0) return '{}';
|
||||
const inner = entries
|
||||
.map(([k, v]) => `${pad}\t${JSON.stringify(k)}: ${ts(v, indent + 1)}`)
|
||||
.join(',\n');
|
||||
return `{\n${inner}\n${pad}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
const header = `// AUTO-GENERATED by scripts/translate-apologetik.ts — DO NOT EDIT BY HAND.
|
||||
// Source: src/lib/data/apologetik.ts (EN) · Target: ${TARGET_LANG} · Generated ${new Date().toISOString()}
|
||||
//
|
||||
// To regenerate: pnpm exec vite-node scripts/translate-apologetik.ts -- --lang=${TARGET_LANG}
|
||||
|
||||
import type {
|
||||
\tArchetype,
|
||||
\tArgument,
|
||||
\tPosArgument,
|
||||
\tPosLayer,
|
||||
\tPosVoice
|
||||
} from './apologetik';
|
||||
|
||||
`;
|
||||
|
||||
const content = [
|
||||
header,
|
||||
`export const ARCHETYPES_${TARGET_LANG}: Record<string, Archetype> = ${ts(archetypesOut)};`,
|
||||
'',
|
||||
`export const ARGUMENTS_${TARGET_LANG}: Argument[] = ${ts(argumentsOut)};`,
|
||||
'',
|
||||
`export const POS_VOICES_${TARGET_LANG}: Record<string, PosVoice> = ${ts(posVoicesOut)};`,
|
||||
'',
|
||||
`export const POS_LAYERS_${TARGET_LANG}: PosLayer[] = ${ts(posLayersOut)};`,
|
||||
'',
|
||||
`export const POS_ARGUMENTS_${TARGET_LANG}: PosArgument[] = ${ts(posArgsOut)};`,
|
||||
''
|
||||
].join('\n');
|
||||
|
||||
const outPath = resolve(process.cwd(), `src/lib/data/apologetik.${FILE_LANG}.ts`);
|
||||
writeFileSync(outPath, content, 'utf8');
|
||||
console.log(`✓ Wrote ${outPath}`);
|
||||
@@ -0,0 +1,151 @@
|
||||
<script lang="ts">
|
||||
type Item = {
|
||||
id: string;
|
||||
n?: number;
|
||||
short: string;
|
||||
title?: string;
|
||||
href: string;
|
||||
group?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
items: Item[];
|
||||
activeId: string;
|
||||
onItemClick?: (e: MouseEvent, id: string) => void;
|
||||
};
|
||||
|
||||
let { title, items, activeId, onItemClick }: Props = $props();
|
||||
|
||||
function handle(e: MouseEvent, id: string) {
|
||||
if (onItemClick) onItemClick(e, id);
|
||||
}
|
||||
|
||||
function groupChanged(items: Item[], i: number): boolean {
|
||||
if (i === 0) return Boolean(items[0].group);
|
||||
return items[i].group !== items[i - 1].group;
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="toc" aria-label={title}>
|
||||
<div class="toc-title">{title}</div>
|
||||
<ol class="toc-list">
|
||||
{#each items as item, i (item.id)}
|
||||
{#if item.group && groupChanged(items, i)}
|
||||
<li class="toc-group">{item.group}</li>
|
||||
{/if}
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class:active={activeId === item.id}
|
||||
class:no-num={item.n === undefined}
|
||||
onclick={(e) => handle(e, item.id)}
|
||||
title={item.title ?? item.short}
|
||||
>
|
||||
{#if item.n !== undefined}
|
||||
<span class="toc-num">{String(item.n).padStart(2, '0')}</span>
|
||||
{/if}
|
||||
<span class="toc-label">{item.short}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.toc {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 96px;
|
||||
max-height: calc(100vh - 120px);
|
||||
overflow-y: auto;
|
||||
width: 220px;
|
||||
padding: 14px 14px 14px 16px;
|
||||
background: color-mix(in oklab, var(--color-bg-secondary) 88%, transparent);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
z-index: 50;
|
||||
font-family: var(--font-sans);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.toc-title {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.toc-group {
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
margin: 10px 0 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.toc-list li:first-child.toc-group {
|
||||
margin-top: 0;
|
||||
}
|
||||
.toc-list a {
|
||||
display: grid;
|
||||
grid-template-columns: 26px 1fr;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.25;
|
||||
border-left: 2px solid transparent;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
color var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
}
|
||||
.toc-list a.no-num {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.toc-list a:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.toc-list a.active {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
border-left-color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.toc-num {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.toc-list a.active .toc-num {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.toc-label {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.toc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import Shield from '@lucide/svelte/icons/shield';
|
||||
import Flame from '@lucide/svelte/icons/flame';
|
||||
|
||||
type Props = {
|
||||
faithLang: string;
|
||||
active: 'contra' | 'pro';
|
||||
labels?: { contra: string; pro: string };
|
||||
};
|
||||
|
||||
let { faithLang, active, labels }: Props = $props();
|
||||
|
||||
const l = $derived(labels ?? { contra: 'Contra', pro: 'Pro' });
|
||||
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
|
||||
</script>
|
||||
|
||||
<nav class="case-tabs" aria-label="Argument case">
|
||||
<a
|
||||
class="case-tab"
|
||||
class:active={active === 'contra'}
|
||||
href="/{faithLang}/{slug}/contra"
|
||||
>
|
||||
<Shield class="ct-glyph" size={14} strokeWidth={2} aria-hidden="true" />
|
||||
<span>{l.contra}</span>
|
||||
</a>
|
||||
<a
|
||||
class="case-tab"
|
||||
class:active={active === 'pro'}
|
||||
href="/{faithLang}/{slug}/pro"
|
||||
>
|
||||
<Flame class="ct-glyph" size={14} strokeWidth={2} aria-hidden="true" />
|
||||
<span>{l.pro}</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.case-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
max-width: 760px;
|
||||
margin: 28px auto 0;
|
||||
padding: 4px;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pill);
|
||||
width: fit-content;
|
||||
}
|
||||
.case-tab {
|
||||
padding: 8px 22px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background var(--transition-normal),
|
||||
color var(--transition-normal),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
.case-tab:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.case-tab.active {
|
||||
background: var(--color-text-primary);
|
||||
color: var(--color-bg-primary);
|
||||
}
|
||||
.case-tab:active,
|
||||
.case-tab:focus-visible {
|
||||
transform: scale(0.97);
|
||||
outline: none;
|
||||
}
|
||||
:global(.ct-glyph) {
|
||||
opacity: 0.9;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.case-tabs {
|
||||
width: calc(100% - 48px);
|
||||
max-width: none;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
.case-tab {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85rem;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
import { lookupReference } from './bible';
|
||||
import { resolveStaticAsset } from './staticAsset';
|
||||
|
||||
// English-bookname → German citation parts.
|
||||
// tsv: abbreviation that allioli.tsv understands (used to look up verse)
|
||||
// full: the full German book name as written in Allioli (used for display)
|
||||
const EN_TO_DE_BOOK: Record<string, { tsv: string; full: string }> = {
|
||||
Genesis: { tsv: '1Mo', full: '1 Mose' },
|
||||
Exodus: { tsv: '2Mo', full: '2 Mose' },
|
||||
Leviticus: { tsv: '3Mo', full: '3 Mose' },
|
||||
Numbers: { tsv: '4Mo', full: '4 Mose' },
|
||||
Deuteronomy: { tsv: '5Mo', full: '5 Mose' },
|
||||
Joshua: { tsv: 'Jos', full: 'Josua' },
|
||||
Judges: { tsv: 'Ri', full: 'Richter' },
|
||||
Ruth: { tsv: 'Rt', full: 'Rut' },
|
||||
'1 Samuel': { tsv: '1Sam', full: '1 Samuel' },
|
||||
'2 Samuel': { tsv: '2Sam', full: '2 Samuel' },
|
||||
'1 Kings': { tsv: '1Kö', full: '1 Könige' },
|
||||
'2 Kings': { tsv: '2Kö', full: '2 Könige' },
|
||||
'1 Chronicles': { tsv: '1Chr', full: '1 Chronik' },
|
||||
'2 Chronicles': { tsv: '2Chr', full: '2 Chronik' },
|
||||
Ezra: { tsv: 'Esr', full: 'Esra' },
|
||||
Nehemiah: { tsv: 'Neh', full: 'Nehemia' },
|
||||
Esther: { tsv: 'Est', full: 'Ester' },
|
||||
Job: { tsv: 'Hi', full: 'Hiob' },
|
||||
Psalm: { tsv: 'Ps', full: 'Psalm' },
|
||||
Psalms: { tsv: 'Ps', full: 'Psalm' },
|
||||
Proverbs: { tsv: 'Spr', full: 'Sprüche' },
|
||||
Ecclesiastes: { tsv: 'Pred', full: 'Prediger' },
|
||||
'Song of Solomon': { tsv: 'Hl', full: 'Hohelied' },
|
||||
Isaiah: { tsv: 'Jes', full: 'Jesaja' },
|
||||
Jeremiah: { tsv: 'Jer', full: 'Jeremia' },
|
||||
Lamentations: { tsv: 'Kla', full: 'Klagelieder' },
|
||||
Ezekiel: { tsv: 'Hes', full: 'Hesekiel' },
|
||||
Daniel: { tsv: 'Dan', full: 'Daniel' },
|
||||
Hosea: { tsv: 'Hos', full: 'Hosea' },
|
||||
Joel: { tsv: 'Joe', full: 'Joel' },
|
||||
Amos: { tsv: 'Am', full: 'Amos' },
|
||||
Obadiah: { tsv: 'Ob', full: 'Obadja' },
|
||||
Jonah: { tsv: 'Jon', full: 'Jona' },
|
||||
Micah: { tsv: 'Mi', full: 'Micha' },
|
||||
Nahum: { tsv: 'Nah', full: 'Nahum' },
|
||||
Habakkuk: { tsv: 'Hab', full: 'Habakuk' },
|
||||
Zephaniah: { tsv: 'Zeph', full: 'Zephanja' },
|
||||
Haggai: { tsv: 'Hagg', full: 'Haggai' },
|
||||
Zechariah: { tsv: 'Sach', full: 'Sacharja' },
|
||||
Malachi: { tsv: 'Mal', full: 'Maleachi' },
|
||||
Matthew: { tsv: 'Mt', full: 'Matthäus' },
|
||||
Mark: { tsv: 'Mk', full: 'Markus' },
|
||||
Luke: { tsv: 'Lk', full: 'Lukas' },
|
||||
John: { tsv: 'Joh', full: 'Johannes' },
|
||||
Acts: { tsv: 'Apg', full: 'Apostelgeschichte' },
|
||||
Romans: { tsv: 'Röm', full: 'Römer' },
|
||||
'1 Corinthians': { tsv: '1Kor', full: '1 Korinther' },
|
||||
'2 Corinthians': { tsv: '2Kor', full: '2 Korinther' },
|
||||
Galatians: { tsv: 'Gal', full: 'Galater' },
|
||||
Ephesians: { tsv: 'Eph', full: 'Epheser' },
|
||||
Philippians: { tsv: 'Phil', full: 'Philipper' },
|
||||
Colossians: { tsv: 'Kol', full: 'Kolosser' },
|
||||
'1 Thessalonians': { tsv: '1Thes', full: '1 Thessalonicher' },
|
||||
'2 Thessalonians': { tsv: '2Thes', full: '2 Thessalonicher' },
|
||||
'1 Timothy': { tsv: '1Tim', full: '1 Timotheus' },
|
||||
'2 Timothy': { tsv: '2Tim', full: '2 Timotheus' },
|
||||
Titus: { tsv: 'Tit', full: 'Titus' },
|
||||
Philemon: { tsv: 'Phim', full: 'Philemon' },
|
||||
Hebrews: { tsv: 'Heb', full: 'Hebräer' },
|
||||
James: { tsv: 'Jak', full: 'Jakobus' },
|
||||
'1 Peter': { tsv: '1Petr', full: '1 Petrus' },
|
||||
'2 Peter': { tsv: '2Petr', full: '2 Petrus' },
|
||||
'1 John': { tsv: '1Jo', full: '1 Johannes' },
|
||||
'2 John': { tsv: '2Jo', full: '2 Johannes' },
|
||||
'3 John': { tsv: '3Jo', full: '3 Johannes' },
|
||||
Jude: { tsv: 'Jud', full: 'Judas' },
|
||||
Revelation: { tsv: 'Offb', full: 'Offenbarung' }
|
||||
};
|
||||
|
||||
// Splits "1 Corinthians 15:17" into ["1 Corinthians", "15:17"].
|
||||
function splitRef(ref: string): { book: string; chapVerse: string } | null {
|
||||
const m = ref.match(/^(.+?)\s+(\d+\s*[:,]\s*\d+(?:\s*-\s*\d+)?)$/);
|
||||
if (!m) return null;
|
||||
return { book: m[1].trim(), chapVerse: m[2].replace(/\s+/g, '') };
|
||||
}
|
||||
|
||||
export type ResolvedScripture = { text: string; ref: string };
|
||||
|
||||
export function resolveScriptureForLang(
|
||||
enRef: string,
|
||||
lang: 'en' | 'de' | 'la'
|
||||
): ResolvedScripture {
|
||||
const split = splitRef(enRef);
|
||||
if (lang === 'de') {
|
||||
const map = split ? EN_TO_DE_BOOK[split.book] : null;
|
||||
if (split && map) {
|
||||
const lookupRef = `${map.tsv} ${split.chapVerse}`;
|
||||
const tsvPath = resolveStaticAsset('allioli.tsv');
|
||||
const result = lookupReference(lookupRef, tsvPath);
|
||||
if (result && result.verses.length) {
|
||||
const text = result.verses.map((v) => v.text).join(' ');
|
||||
const display = `${map.full} ${split.chapVerse.replace(':', ',')}`;
|
||||
return { text, ref: display };
|
||||
}
|
||||
}
|
||||
// Fallback: return original English ref unchanged
|
||||
return { text: '', ref: enRef };
|
||||
}
|
||||
|
||||
// Default: English (faith / fides — drb.tsv)
|
||||
const tsvPath = resolveStaticAsset('drb.tsv');
|
||||
const result = lookupReference(enRef, tsvPath);
|
||||
if (result && result.verses.length) {
|
||||
const text = result.verses.map((v) => v.text).join(' ');
|
||||
return { text, ref: enRef };
|
||||
}
|
||||
return { text: '', ref: enRef };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ParamMatcher } from '@sveltejs/kit';
|
||||
|
||||
export const match: ParamMatcher = (param) => {
|
||||
return param === 'apologetik' || param === 'apologetics';
|
||||
};
|
||||
@@ -13,6 +13,7 @@ const eastertide = isEastertide();
|
||||
const prayersHref = $derived(isLatin ? '/fides/orationes' : `/${data.faithLang}/${isEnglish ? 'prayers' : 'gebete'}`);
|
||||
const rosaryHref = $derived(`/${data.faithLang}/${isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz'}`);
|
||||
const calendarHref = $derived(`/${data.faithLang}/${isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender'}`);
|
||||
const apologetikHref = $derived(isLatin ? '/faith/apologetics' : `/${data.faithLang}/${isEnglish ? 'apologetics' : 'apologetik'}`);
|
||||
const angelusHref = $derived(eastertide
|
||||
? `${prayersHref}/regina-caeli`
|
||||
: `${prayersHref}/angelus`);
|
||||
@@ -22,6 +23,7 @@ const labels = $derived({
|
||||
prayers: isLatin ? 'Orationes' : isEnglish ? 'Prayers' : 'Gebete',
|
||||
rosary: isLatin ? 'Rosarium' : isEnglish ? 'Rosary' : 'Rosenkranz',
|
||||
catechesis: isEnglish ? 'Catechesis' : 'Katechese',
|
||||
apologetics: isLatin ? 'Apologetica' : isEnglish ? 'Apologetics' : 'Apologetik',
|
||||
calendar: isLatin ? 'Calendarium' : isEnglish ? 'Calendar' : 'Kalender'
|
||||
});
|
||||
|
||||
@@ -49,6 +51,7 @@ const prayersActive = $derived(isActive(prayersHref) && !isActive(angelusHref));
|
||||
<li style="--active-fill: var(--nord14)"><a href={angelusHref} class:active={isActive(angelusHref)} title={angelusLabel}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="6 -274 564 548" fill="currentColor"><path d="M392-162c-4-10-9-18-15-26 5-4 7-8 7-12 0-18-43-32-96-32s-96 14-96 32c0 4 3 8 7 12-6 8-11 16-15 26-15-11-24-24-24-38 0-35 57-64 128-64s128 29 128 64c0 14-9 27-24 38zm-104-22c35 0 64 29 64 64s-29 64-64 64-64-29-64-64 29-64 64-64zM82 159c3-22-3-48-20-64C34 68 16 30 16-12v-64c0-42 34-76 76-76 23 0 44 10 59 27l65 78c-21 16-37 40-43 67l-43 195c-4 17-2 34 5 49h-21c-26 0-46-24-42-50l10-55zm364 56L403 20c-6-27-21-51-42-67l64-77c15-18 36-28 59-28 42 0 76 34 76 76v64c0 42-18 80-46 107-17 16-23 42-20 64l10 56c4 26-16 49-42 49h-20c6-15 8-32 4-49zM220 31c7-32 35-55 68-55s61 23 68 55l43 194c5 20-11 39-31 39H208c-21 0-36-19-31-39l43-194z"/></svg><span class="nav-label">{angelusLabel}</span></a></li>
|
||||
{/if}
|
||||
<li style="--active-fill: var(--nord13)"><a href="/{data.faithLang}/katechese" class:active={isActive(`/${data.faithLang}/katechese`)} title={labels.catechesis}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 12h2"/><path d="M6 8h2"/></svg><span class="nav-label">{labels.catechesis}</span></a></li>
|
||||
<li style="--active-fill: var(--nord10)"><a href={apologetikHref} class:active={isActive(apologetikHref)} title={labels.apologetics}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/><path d="M7 21h10"/><path d="M12 3v18"/><path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/></svg><span class="nav-label">{labels.apologetics}</span></a></li>
|
||||
<li style="--active-fill: var(--nord15)"><a href={calendarHref} class:active={isActive(calendarHref)} title={labels.calendar}><svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg><span class="nav-label">{labels.calendar}</span></a></li>
|
||||
</ul>
|
||||
{/snippet}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
const prayersPath = $derived(isLatin ? 'orationes' : isEnglish ? 'prayers' : 'gebete');
|
||||
const rosaryPath = $derived(isLatin ? 'rosarium' : isEnglish ? 'rosary' : 'rosenkranz');
|
||||
const calendarPath = $derived(isLatin ? 'calendarium' : isEnglish ? 'calendar' : 'kalender');
|
||||
const apologetikHref = $derived(isLatin ? '/faith/apologetics' : `/${data.faithLang}/${isEnglish ? 'apologetics' : 'apologetik'}`);
|
||||
const eastertide = isEastertide();
|
||||
|
||||
const labels = $derived({
|
||||
@@ -19,6 +20,7 @@
|
||||
: 'Hier findet man einige Gebete und einen interaktiven Rosenkranz zum katholischen Glauben. Ein Fokus auf Latein und den alten Ritus wird zu bemerken sein.',
|
||||
prayers: isLatin ? 'Orationes' : isEnglish ? 'Prayers' : 'Gebete',
|
||||
rosary: isLatin ? 'Rosarium Vivum' : isEnglish ? 'Rosary' : 'Rosenkranz',
|
||||
apologetics: isLatin ? 'Apologetica' : isEnglish ? 'Apologetics' : 'Apologetik',
|
||||
calendar: isLatin ? 'Calendarium' : isEnglish ? 'Calendar' : 'Kalender'
|
||||
});
|
||||
</script>
|
||||
@@ -128,6 +130,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 532 506"><path d="M256-107v310l1-1c54-22 113-34 172-34h19v-320h-19c-42 0-84 8-123 25-17 7-34 14-50 20zm-25-79 25 10 25-10c47-20 97-30 148-30h35c27 0 48 22 48 48v352c0 27-21 48-48 48h-35c-51 0-101 10-148 30l-13 5c-8 3-16 3-24 0l-13-5c-47-20-97-30-148-30H48c-26 0-48-21-48-48v-352c0-26 22-48 48-48h35c51 0 101 10 148 30z"/></svg>
|
||||
<h3>Katechese</h3>
|
||||
</a>
|
||||
<a href={apologetikHref}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M384 32H512c17.7 0 32 14.3 32 32s-14.3 32-32 32H398.4c-5.2 25.8-22.9 47.1-46.4 57.3V448H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H320 96c-17.7 0-32-14.3-32-32s14.3-32 32-32H288V153.3c-23.5-10.3-41.2-31.6-46.4-57.3H128c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c14.6-19.4 37.8-32 64-32s49.4 12.6 64 32zm55.6 288H584.4L512 195.8 439.6 320zM512 416c-62.9 0-115.2-34-126-78.9c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C627.2 382 574.9 416 512 416zM126.8 195.8L54.4 320H199.3L126.8 195.8zM.9 337.1c-2.6-11 1-22.3 6.7-32.1l95.2-163.2c5-8.6 14.2-13.8 24.1-13.8s19.1 5.3 24.1 13.8l95.2 163.2c5.7 9.8 9.3 21.1 6.7 32.1C242 382 189.7 416 126.8 416S11.7 382 .9 337.1z"/></svg>
|
||||
<h3>{labels.apologetics}</h3>
|
||||
</a>
|
||||
<a href="/{data.faithLang}/{calendarPath}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M128 0c17.7 0 32 14.3 32 32V64H288V32c0-17.7 14.3-32 32-32s32 14.3 32 32V64h48c26.5 0 48 21.5 48 48v48H0V112C0 85.5 21.5 64 48 64H96V32c0-17.7 14.3-32 32-32zM0 192H448V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V192zm64 80v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm128 0v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H208c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V272c0-8.8-7.2-16-16-16H336zM64 400v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H80c-8.8 0-16 7.2-16 16zm144-16c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H208zm112 16v32c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H336c-8.8 0-16 7.2-16 16z"/></svg>
|
||||
<h3>{labels.calendar}</h3>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
const expectedSlug = { de: 'apologetik', en: 'apologetics' } as const;
|
||||
|
||||
export const load: LayoutServerLoad = async ({ params, parent, url }) => {
|
||||
const { lang, faithLang } = await parent();
|
||||
|
||||
const prefix = `/${faithLang}/${params.apologetikSlug}`;
|
||||
const tail = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : '';
|
||||
|
||||
if (lang === 'la') {
|
||||
throw redirect(307, `/faith/apologetics${tail}${url.search}`);
|
||||
}
|
||||
|
||||
const want = expectedSlug[lang as 'de' | 'en'];
|
||||
if (params.apologetikSlug !== want) {
|
||||
throw redirect(307, `/${faithLang}/${want}${tail}${url.search}`);
|
||||
}
|
||||
|
||||
return { apologetikSlug: want };
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
<script lang="ts">
|
||||
import Shield from '@lucide/svelte/icons/shield';
|
||||
import Flame from '@lucide/svelte/icons/flame';
|
||||
|
||||
let { data } = $props();
|
||||
const faithLang = $derived(data?.faithLang ?? 'faith');
|
||||
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
|
||||
const isGerman = $derived(data?.lang === 'de');
|
||||
|
||||
const t = $derived(
|
||||
isGerman
|
||||
|
||||
? {
|
||||
title: 'Apologetik',
|
||||
lede:
|
||||
'Apologetik hat zwei Seiten. Die eine antwortet auf Einwände gegen den Glauben. Die andere trägt die Gründe für ihn vor. Hier sind beide — getrennt vorgelegt, doch im selben Werk.',
|
||||
contraTitle: 'Contra',
|
||||
contraSub: 'Was, wenn nicht?',
|
||||
contraDesc:
|
||||
'Dreiundzwanzig Einwände, wie sie ein Atheist erheben mag, je in mehreren Stimmen beantwortet — historische Gestalten (Aquin, Pascal, Augustinus, Lewis, Chesterton) und Archetypen (der Logiker, der Mystiker, die Wissenschaft, der Pfarrer).',
|
||||
contraCta: 'Zu den Einwänden',
|
||||
proTitle: 'Pro',
|
||||
proSub: 'Warum es so ist.',
|
||||
proDesc:
|
||||
'Zwölf Fäden in drei Schichten: das Übernatürliche ist wirklich, es gibt einen Gott, das Christentum ist seine Offenbarung. Stimmen: Habermas, Polkinghorne, Newman, Hart, Lewis, Wright, Hahn, Plantinga.',
|
||||
proCta: 'Zu den Argumenten'
|
||||
}
|
||||
: {
|
||||
title: 'Apologetics',
|
||||
lede:
|
||||
'Apologetics has two arms. One answers objections raised against the faith. The other lays out the case for it. Both are kept here — distinct, but part of one work.',
|
||||
contraTitle: 'Contra',
|
||||
contraSub: 'What if not?',
|
||||
contraDesc:
|
||||
'Twenty-three objections an atheist might raise, each answered in several voices — historical figures (Aquinas, Pascal, Augustine, Lewis, Chesterton) alongside archetypes (the Logician, the Mystic, the Scientist, the Pastor).',
|
||||
contraCta: 'To the objections',
|
||||
proTitle: 'Pro',
|
||||
proSub: 'Why it is so.',
|
||||
proDesc:
|
||||
'Twelve threads across three layers: the supernatural is real, there is one God, Christianity is that revelation. Voices: Habermas, Polkinghorne, Newman, Hart, Lewis, Wright, Hahn, Plantinga.',
|
||||
proCta: 'To the arguments'
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t.title} · bocken.org</title>
|
||||
<meta name="description" content={t.lede} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="apologetik-landing">
|
||||
<section class="page-head">
|
||||
<h1>{t.title}</h1>
|
||||
<p class="lede">{t.lede}</p>
|
||||
</section>
|
||||
|
||||
<section class="cards" aria-label={t.title}>
|
||||
<a class="case-card contra" href="/{faithLang}/{slug}/contra">
|
||||
<div class="card-glyph" aria-hidden="true"><Shield size={28} strokeWidth={2} /></div>
|
||||
<div class="card-body">
|
||||
<div class="card-sub">{t.contraSub}</div>
|
||||
<h2>{t.contraTitle}</h2>
|
||||
<p>{t.contraDesc}</p>
|
||||
<span class="card-cta">{t.contraCta} <span aria-hidden="true">→</span></span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="case-card pro" href="/{faithLang}/{slug}/pro">
|
||||
<div class="card-glyph" aria-hidden="true"><Flame size={28} strokeWidth={2} /></div>
|
||||
<div class="card-body">
|
||||
<div class="card-sub">{t.proSub}</div>
|
||||
<h2>{t.proTitle}</h2>
|
||||
<p>{t.proDesc}</p>
|
||||
<span class="card-cta">{t.proCta} <span aria-hidden="true">→</span></span>
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apologetik-landing {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 80px;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.page-head {
|
||||
max-width: 760px;
|
||||
margin: 56px auto 8px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.page-head h1 {
|
||||
font-size: clamp(2rem, 4.4vw, 3.2rem);
|
||||
line-height: 1.08;
|
||||
font-weight: 700;
|
||||
margin: 0 0 18px;
|
||||
letter-spacing: -0.01em;
|
||||
text-align: left;
|
||||
}
|
||||
.page-head .lede {
|
||||
font-size: 1.12rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 60ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cards {
|
||||
max-width: 1100px;
|
||||
margin: 48px auto 0;
|
||||
padding: 0 24px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.case-card {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
padding: 28px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-normal),
|
||||
background var(--transition-normal),
|
||||
border-color var(--transition-normal);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.case-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
.case-card.contra::before {
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--nord11) 8%, transparent), transparent 60%);
|
||||
}
|
||||
.case-card.pro::before {
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--nord14) 8%, transparent), transparent 60%);
|
||||
}
|
||||
.case-card:hover,
|
||||
.case-card:focus-visible {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
background: var(--color-bg-elevated);
|
||||
outline: none;
|
||||
}
|
||||
.case-card:hover::before,
|
||||
.case-card:focus-visible::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-glyph {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: var(--radius-pill);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.6rem;
|
||||
color: white;
|
||||
flex: none;
|
||||
}
|
||||
.contra .card-glyph {
|
||||
background: var(--nord11);
|
||||
}
|
||||
.pro .card-glyph {
|
||||
background: var(--nord14);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
min-width: 0;
|
||||
}
|
||||
.card-sub {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.case-card h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.18;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.case-card p {
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0 0 18px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.card-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
transition: gap var(--transition-fast);
|
||||
}
|
||||
.case-card:hover .card-cta {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.cards {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.case-card {
|
||||
padding: 22px;
|
||||
grid-template-columns: 48px 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
.card-glyph {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.case-card h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.page-head h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,553 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const faithLang = $derived(data?.faithLang ?? 'faith');
|
||||
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
|
||||
const isLatin = $derived(data?.lang === 'la');
|
||||
const isGerman = $derived(data?.lang === 'de');
|
||||
|
||||
const ARCHETYPES = $derived(data.archetypes);
|
||||
const ARGUMENTS = $derived(data.args);
|
||||
|
||||
let activeId = $state<string>('');
|
||||
let filterArchId = $state<string | null>(null);
|
||||
|
||||
const filteredArguments = $derived.by(() => {
|
||||
const archId = filterArchId;
|
||||
if (!archId) return ARGUMENTS;
|
||||
return ARGUMENTS.filter((a) => Object.keys(a.counters).includes(archId));
|
||||
});
|
||||
|
||||
let io: IntersectionObserver | null = null;
|
||||
|
||||
function attachObserver() {
|
||||
if (io) io.disconnect();
|
||||
const els = document.querySelectorAll<HTMLElement>('.feed .arg-row');
|
||||
if (!els.length) return;
|
||||
io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||
if (visible[0]) {
|
||||
activeId = visible[0].target.id.replace(/^arg-/, '');
|
||||
}
|
||||
},
|
||||
{ rootMargin: '-64px 0px -60% 0px', threshold: 0 }
|
||||
);
|
||||
els.forEach((el) => io!.observe(el));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
attachObserver();
|
||||
return () => io?.disconnect();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// re-attach observer when filter changes the rendered list
|
||||
void filteredArguments;
|
||||
tick().then(attachObserver);
|
||||
});
|
||||
|
||||
function jumpTo(e: MouseEvent, id: string) {
|
||||
const el = document.getElementById(`arg-${id}`);
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.replaceState(null, '', `#arg-${id}`);
|
||||
}
|
||||
|
||||
function toggleFilter(archId: string) {
|
||||
filterArchId = filterArchId === archId ? null : archId;
|
||||
}
|
||||
|
||||
const heading = $derived(
|
||||
isLatin
|
||||
? 'Argumenta pro fide.'
|
||||
: isGerman
|
||||
? 'Argumente gegen den Glauben, beantwortet.'
|
||||
: 'Arguments against the faith, with reply.'
|
||||
);
|
||||
const lede = $derived(
|
||||
isLatin
|
||||
? 'Viginti tres obiectiones quas atheus opponere possit, singulae pluribus vocibus responsae.'
|
||||
: isGerman
|
||||
? 'Dreiundzwanzig Einwände, wie sie ein Atheist erheben mag, je in mehreren Stimmen beantwortet.'
|
||||
: 'Twenty-three objections an atheist might raise, each answered in several voices.'
|
||||
);
|
||||
const tocLabel = $derived(
|
||||
isLatin ? 'Obiectiones' : isGerman ? 'Einwände' : 'Objections'
|
||||
);
|
||||
const legendTitle = $derived(
|
||||
isLatin ? 'Voces respondentes' : isGerman ? 'Die antwortenden Stimmen' : 'The voices that answer'
|
||||
);
|
||||
const objectionLabel = $derived(
|
||||
isLatin ? 'OBIECTIO' : isGerman ? 'EINWAND' : 'OBJECTION'
|
||||
);
|
||||
const answeredByLabel = $derived(
|
||||
isLatin ? 'Respondetur a' : isGerman ? 'Beantwortet von' : 'Answered by'
|
||||
);
|
||||
const filterLabels = $derived(
|
||||
isLatin
|
||||
? { filteringBy: 'Filtrum:', showAll: 'omnia ostendere' }
|
||||
: isGerman
|
||||
? { filteringBy: 'Gefiltert nach:', showAll: 'alle zeigen' }
|
||||
: { filteringBy: 'Filtering by:', showAll: 'show all' }
|
||||
);
|
||||
|
||||
const archetypes = $derived(Object.values(ARCHETYPES));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{heading} · bocken.org</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Common objections to Christianity, each answered in several historical voices: Aquinas, Pascal, Augustine, Francis, Lewis, Chesterton, the Logician, the Mystic, the Scientist, the Pastor."
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Spectral:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="apologetik">
|
||||
<CaseTabs {faithLang} active="contra" />
|
||||
|
||||
<section class="page-head">
|
||||
<h1>{heading}</h1>
|
||||
<p class="lede">{lede}</p>
|
||||
</section>
|
||||
|
||||
<section class="legend" aria-label={legendTitle}>
|
||||
<div class="legend-title">{legendTitle}</div>
|
||||
<div class="legend-row">
|
||||
{#each archetypes as a (a.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="legend-item"
|
||||
class:active={filterArchId === a.id}
|
||||
class:dimmed={filterArchId !== null && filterArchId !== a.id}
|
||||
aria-pressed={filterArchId === a.id}
|
||||
onclick={() => toggleFilter(a.id)}
|
||||
title={a.sub}
|
||||
>
|
||||
<span class="glyph lg" aria-hidden="true" style="background:{a.color};">
|
||||
{a.glyph}
|
||||
</span>
|
||||
<span><span class="nm">{a.name}</span> <span class="sb">— {a.sub}</span></span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if filterArchId}
|
||||
{@const sel = ARCHETYPES[filterArchId]}
|
||||
<div class="filter-banner" role="status">
|
||||
<span>
|
||||
{filterLabels.filteringBy}
|
||||
<strong>
|
||||
<span class="glyph" aria-hidden="true" style="background:{sel.color};">
|
||||
{sel.glyph}
|
||||
</span>
|
||||
{sel.name}
|
||||
</strong>
|
||||
· {filteredArguments.length}/{ARGUMENTS.length}
|
||||
</span>
|
||||
<button type="button" class="filter-clear" onclick={() => (filterArchId = null)}
|
||||
>{filterLabels.showAll} ✕</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<ApologetikToc
|
||||
title={tocLabel}
|
||||
items={filteredArguments.map((a) => ({
|
||||
id: a.id,
|
||||
n: a.n,
|
||||
short: a.short,
|
||||
title: a.title,
|
||||
href: `#arg-${a.id}`
|
||||
}))}
|
||||
{activeId}
|
||||
onItemClick={(e, id) => jumpTo(e, id)}
|
||||
/>
|
||||
|
||||
<section class="feed" aria-label="Arguments">
|
||||
{#each filteredArguments as arg (arg.id)}
|
||||
<article class="arg-row" id="arg-{arg.id}">
|
||||
<a
|
||||
class="card-link"
|
||||
href="/{faithLang}/{slug}/contra/{arg.id}"
|
||||
aria-label={arg.title}
|
||||
></a>
|
||||
<div class="arg-num">
|
||||
{String(arg.n).padStart(2, '0')}
|
||||
<small>{objectionLabel}</small>
|
||||
</div>
|
||||
<div class="arg-body">
|
||||
<span class="arg-short">{arg.short}</span>
|
||||
<h2>{arg.title}</h2>
|
||||
<p class="arg-steel">{arg.steel}</p>
|
||||
<blockquote class="arg-quote"><q>{arg.quote}</q></blockquote>
|
||||
<div class="arg-quote-by">— {arg.quoteBy}</div>
|
||||
|
||||
<div class="answer-rail">
|
||||
<span class="label">{answeredByLabel}</span>
|
||||
{#each Object.keys(arg.counters) as archId (archId)}
|
||||
{@const a = ARCHETYPES[archId]}
|
||||
<a
|
||||
class="archetype-badge"
|
||||
href="/{faithLang}/{slug}/contra/{arg.id}#voice-{archId}"
|
||||
title="{a.name} — {a.sub}"
|
||||
>
|
||||
<span class="glyph" aria-hidden="true" style="background:{a.color};">
|
||||
{a.glyph}
|
||||
</span>
|
||||
<span>{a.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.apologetik {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 80px;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.page-head {
|
||||
max-width: 760px;
|
||||
margin: 56px auto 8px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.page-head h1 {
|
||||
font-size: clamp(2rem, 4.4vw, 3.2rem);
|
||||
line-height: 1.08;
|
||||
font-weight: 700;
|
||||
margin: 0 0 18px;
|
||||
letter-spacing: -0.01em;
|
||||
text-align: left;
|
||||
}
|
||||
.page-head .lede {
|
||||
font-size: 1.12rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 60ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend {
|
||||
max-width: 1100px;
|
||||
margin: 38px auto 0;
|
||||
padding: 18px 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.legend-title {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: 14px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.legend-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
padding: 6px 12px 6px 6px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
opacity var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
.legend-item:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
.legend-item:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
.legend-item.active {
|
||||
background: var(--color-bg-elevated);
|
||||
border-color: var(--color-text-primary);
|
||||
}
|
||||
.legend-item.dimmed {
|
||||
opacity: 0.45;
|
||||
}
|
||||
.legend-item .nm {
|
||||
font-weight: 600;
|
||||
}
|
||||
.legend-item .sb {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.filter-banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 14px;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.filter-banner strong {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.filter-banner .glyph {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-pill);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.filter-clear {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
.filter-clear:hover {
|
||||
background: var(--color-bg-elevated);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.glyph {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-pill);
|
||||
color: white;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.glyph.lg {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.feed {
|
||||
max-width: 760px;
|
||||
margin: 28px auto 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.arg-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
gap: 22px;
|
||||
padding: 28px 18px;
|
||||
margin: 0 -18px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
align-items: start;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background var(--transition-fast);
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
.arg-row:hover {
|
||||
background: color-mix(in oklab, var(--color-bg-secondary) 60%, transparent);
|
||||
}
|
||||
.arg-row:has(.card-link:focus-visible) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.arg-row:has(.card-link:hover) h2 {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.arg-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.card-link {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.card-link:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
.arg-row > .arg-num,
|
||||
.arg-row > .arg-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.arg-body h2 {
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
.archetype-badge {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.arg-num {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
color: var(--color-text-tertiary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.arg-num small {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.14em;
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-muted, var(--color-text-tertiary));
|
||||
}
|
||||
.arg-body h2 {
|
||||
font-size: 1.55rem;
|
||||
line-height: 1.18;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px;
|
||||
text-align: left;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
.arg-short {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 3px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
margin-bottom: 14px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.arg-steel {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 14px;
|
||||
max-width: 60ch;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.arg-quote {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-secondary);
|
||||
border-left: 2px solid var(--color-border);
|
||||
padding: 4px 0 4px 14px;
|
||||
margin: 0 0 6px;
|
||||
max-width: 56ch;
|
||||
}
|
||||
.arg-quote q::before {
|
||||
content: '\201C';
|
||||
}
|
||||
.arg-quote q::after {
|
||||
content: '\201D';
|
||||
}
|
||||
.arg-quote-by {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 18px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.answer-rail {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.answer-rail .label {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: 4px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.archetype-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px 5px 5px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-normal),
|
||||
background var(--transition-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
.archetype-badge:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
.archetype-badge:active,
|
||||
.archetype-badge:focus-visible {
|
||||
transform: scale(0.95);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.arg-row {
|
||||
grid-template-columns: 40px 1fr;
|
||||
gap: 14px;
|
||||
padding: 22px 0;
|
||||
}
|
||||
.arg-num {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.arg-body h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.page-head h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getArchetypes, getArguments } from '$lib/data/apologetik';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { lang } = await parent();
|
||||
const [archetypes, args] = await Promise.all([getArchetypes(lang), getArguments(lang)]);
|
||||
return { archetypes, args };
|
||||
};
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const faithLang = $derived(data?.faithLang ?? 'faith');
|
||||
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
|
||||
const isLatin = $derived(data?.lang === 'la');
|
||||
const isGerman = $derived(data?.lang === 'de');
|
||||
const arg = $derived(data.argument);
|
||||
const ARCHETYPES = $derived(data.archetypes);
|
||||
|
||||
const tocLabel = $derived(
|
||||
isLatin ? 'Obiectiones' : isGerman ? 'Einwände' : 'Objections'
|
||||
);
|
||||
const tocItems = $derived(
|
||||
data.args.map((a) => ({
|
||||
id: a.id,
|
||||
n: a.n,
|
||||
short: a.short,
|
||||
title: a.title,
|
||||
href: `/${faithLang}/${slug}/contra/${a.id}`
|
||||
}))
|
||||
);
|
||||
|
||||
const archIds = $derived(Object.keys(arg.counters));
|
||||
let userSelected = $state<string | null>(null);
|
||||
const activeId = $derived(
|
||||
userSelected && archIds.includes(userSelected) ? userSelected : (archIds[0] ?? '')
|
||||
);
|
||||
const arch = $derived(ARCHETYPES[activeId]);
|
||||
const counter = $derived(arg.counters[activeId]);
|
||||
|
||||
const labels = $derived(
|
||||
isLatin
|
||||
? {
|
||||
back: '← Ad omnia argumenta',
|
||||
eyebrowPrefix: 'Obiectio',
|
||||
objectionTitle: 'Obiectio in plenum',
|
||||
plain: 'aut, simplicius —',
|
||||
citations: 'Citationes',
|
||||
related: 'Obiectiones connexae'
|
||||
}
|
||||
: isGerman
|
||||
? {
|
||||
back: '← Alle Einwände',
|
||||
eyebrowPrefix: 'Einwand',
|
||||
objectionTitle: 'Der Einwand, im Vollen',
|
||||
plain: 'oder, einfach gesagt —',
|
||||
citations: 'Quellen',
|
||||
related: 'Verwandte Einwände'
|
||||
}
|
||||
: {
|
||||
back: '← All arguments',
|
||||
eyebrowPrefix: 'Objection',
|
||||
objectionTitle: 'The objection, in full',
|
||||
plain: 'or, in plain terms —',
|
||||
citations: 'Citations',
|
||||
related: 'Related objections'
|
||||
}
|
||||
);
|
||||
|
||||
const acc = $derived(arch?.colorHex ?? '#5E81AC');
|
||||
const accSoft = $derived(arch ? hexToRgba(arch.colorHex, 0.14) : 'rgba(94,129,172,0.14)');
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const h = hex.replace('#', '');
|
||||
const r = parseInt(h.slice(0, 2), 16);
|
||||
const g = parseInt(h.slice(2, 4), 16);
|
||||
const b = parseInt(h.slice(4, 6), 16);
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
function selectArch(id: string) {
|
||||
userSelected = id;
|
||||
if (typeof window !== 'undefined') {
|
||||
history.replaceState(null, '', `#voice-${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith('#voice-')) {
|
||||
const id = hash.slice('#voice-'.length);
|
||||
if (archIds.includes(id)) {
|
||||
userSelected = id;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{arg.title} · {isLatin ? 'Apologia' : isGerman ? 'Apologetik' : 'Arguments'} · bocken.org</title>
|
||||
<meta name="description" content={arg.steel} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Spectral:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
|
||||
|
||||
<main class="detail">
|
||||
<a class="back-link" href="/{faithLang}/{slug}/contra">{labels.back}</a>
|
||||
|
||||
<div class="detail-eyebrow">
|
||||
{labels.eyebrowPrefix}
|
||||
{String(arg.n).padStart(2, '0')} · {arg.short}
|
||||
</div>
|
||||
<h1>{arg.title}</h1>
|
||||
|
||||
<section class="objection" aria-label="The objection">
|
||||
<h3>{labels.objectionTitle}</h3>
|
||||
<p class="steel">{arg.steel}</p>
|
||||
<blockquote class="quote"><q>{arg.quote}</q></blockquote>
|
||||
<div class="quote-by">— {arg.quoteBy}</div>
|
||||
<p class="pub"><span class="pub-prefix">{labels.plain}</span>{arg.pub}</p>
|
||||
</section>
|
||||
|
||||
<div class="tabs" role="tablist" aria-label="Answer voices">
|
||||
{#each archIds as id (id)}
|
||||
{@const a = ARCHETYPES[id]}
|
||||
{@const isActive = id === activeId}
|
||||
<a
|
||||
href="#voice-{id}"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
class="tab"
|
||||
class:active={isActive}
|
||||
style:border-bottom-color={isActive ? a.colorHex : 'transparent'}
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
selectArch(id);
|
||||
}}
|
||||
>
|
||||
<span class="glyph" aria-hidden="true" style="background:{a.color};">{a.glyph}</span>
|
||||
<span>{a.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if arch && counter}
|
||||
<section
|
||||
class="answer-panel arch-{activeId}"
|
||||
style:--acc={acc}
|
||||
style:--acc-soft={accSoft}
|
||||
style="border-left-color: {acc}; background-image: linear-gradient(to right, {accSoft}, transparent 320px);"
|
||||
id="voice-{activeId}"
|
||||
>
|
||||
<header class="answer-author">
|
||||
<span class="glyph lg" aria-hidden="true" style="background:{arch.color};"
|
||||
>{arch.glyph}</span
|
||||
>
|
||||
<div>
|
||||
<div class="who">{arch.name}</div>
|
||||
<div class="sub">{arch.sub}</div>
|
||||
{#if arch.era !== '—'}
|
||||
<div class="era">{arch.era}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2 class="answer-lede">{counter.lede}</h2>
|
||||
<div class="answer-body">
|
||||
{#each counter.body as p, i (i)}
|
||||
<p>{p}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if counter.cites.length > 0}
|
||||
<div class="cites">
|
||||
<span class="ct">{labels.citations}</span>
|
||||
{counter.cites.join(' · ')}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if arg.related.length > 0}
|
||||
<section class="related" aria-label={labels.related}>
|
||||
<h3>{labels.related}</h3>
|
||||
<div class="related-list">
|
||||
{#each arg.related as rid (rid)}
|
||||
{@const r = data.args.find((x) => x.id === rid)}
|
||||
{#if r}
|
||||
<a class="related-item" href="/{faithLang}/{slug}/contra/{r.id}">
|
||||
<span class="num">{String(r.n).padStart(2, '0')}</span>
|
||||
{r.title}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.detail {
|
||||
max-width: 820px;
|
||||
margin: 36px auto 0;
|
||||
padding: 0 24px 80px;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 24px;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
.back-link:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.back-link:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.detail-eyebrow {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: 14px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.detail h1 {
|
||||
font-size: clamp(1.8rem, 3.6vw, 2.6rem);
|
||||
line-height: 1.12;
|
||||
margin: 0 0 22px;
|
||||
letter-spacing: -0.01em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.objection {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 22px 24px;
|
||||
margin: 0 0 32px;
|
||||
}
|
||||
.objection h3 {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 12px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.objection .steel {
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 16px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.objection .quote {
|
||||
font-size: 1rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-secondary);
|
||||
border-left: 2px solid var(--color-border);
|
||||
padding: 4px 0 4px 14px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.objection .quote q::before {
|
||||
content: '\201C';
|
||||
}
|
||||
.objection .quote q::after {
|
||||
content: '\201D';
|
||||
}
|
||||
.objection .quote-by {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
padding-left: 14px;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
.objection .pub {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-style: italic;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
padding-top: 12px;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
.pub-prefix {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-style: normal;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 28px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.tab:visited {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.tab:hover,
|
||||
.tab:focus-visible {
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.tab.active,
|
||||
.tab.active:visited {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.glyph {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-pill);
|
||||
color: white;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.glyph.lg {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.answer-panel {
|
||||
--acc: var(--color-primary);
|
||||
--acc-soft: rgba(94, 129, 172, 0.12);
|
||||
border-left: 3px solid var(--acc);
|
||||
padding: 24px 26px;
|
||||
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.answer-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.answer-author .who {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.answer-author .sub {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.answer-author .era {
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.answer-lede {
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
max-width: 56ch;
|
||||
text-wrap: pretty;
|
||||
letter-spacing: -0.005em;
|
||||
text-align: left;
|
||||
}
|
||||
.answer-body {
|
||||
font-size: 1rem;
|
||||
line-height: 1.65;
|
||||
max-width: 60ch;
|
||||
}
|
||||
.answer-body p {
|
||||
margin: 0 0 14px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.answer-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cites {
|
||||
margin-top: 18px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
}
|
||||
.cites .ct {
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.68rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.arch-logician,
|
||||
.arch-scientist,
|
||||
.arch-pascal {
|
||||
font-family: 'IBM Plex Mono', ui-monospace, Menlo, Consolas, monospace;
|
||||
}
|
||||
.arch-aquinas,
|
||||
.arch-augustine,
|
||||
.arch-mystic {
|
||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
||||
}
|
||||
.arch-francis,
|
||||
.arch-lewis {
|
||||
font-family: 'Spectral', 'Lora', Georgia, serif;
|
||||
}
|
||||
.arch-chesterton {
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.arch-augustine .answer-lede,
|
||||
.arch-augustine .answer-body {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.related {
|
||||
margin: 38px 0 0;
|
||||
padding-top: 22px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.related h3 {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 14px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
text-align: left;
|
||||
}
|
||||
.related-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.related-item {
|
||||
display: block;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-secondary);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.35;
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
background var(--transition-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
.related-item:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
.related-item .num {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.detail h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.objection {
|
||||
padding: 18px 16px;
|
||||
}
|
||||
.answer-panel {
|
||||
padding: 18px 18px;
|
||||
}
|
||||
.tab {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { findArgumentLang, getArchetypes, getArguments } from '$lib/data/apologetik';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params, parent }) => {
|
||||
const { lang } = await parent();
|
||||
const [arg, archetypes, args] = await Promise.all([
|
||||
findArgumentLang(params.argId, lang),
|
||||
getArchetypes(lang),
|
||||
getArguments(lang)
|
||||
]);
|
||||
if (!arg) {
|
||||
error(404, 'Argument not found');
|
||||
}
|
||||
return { argument: arg, archetypes, args };
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import {
|
||||
getPosArguments,
|
||||
getPosLayers,
|
||||
getPosVoices,
|
||||
POS_ARGUMENTS as EN_POS_ARGUMENTS
|
||||
} from '$lib/data/apologetik';
|
||||
import { resolveScriptureForLang } from '$lib/server/scriptureLookup';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { lang } = await parent();
|
||||
const [voices, layers, args] = await Promise.all([
|
||||
getPosVoices(lang),
|
||||
getPosLayers(lang),
|
||||
getPosArguments(lang)
|
||||
]);
|
||||
|
||||
const lng: 'en' | 'de' = lang === 'de' ? 'de' : 'en';
|
||||
const argsWithScripture = args.map((a) => {
|
||||
const en = EN_POS_ARGUMENTS.find((x) => x.id === a.id);
|
||||
if (!en) return a;
|
||||
const resolved = resolveScriptureForLang(en.scripture.ref, lng);
|
||||
return {
|
||||
...a,
|
||||
scripture: resolved.text ? resolved : a.scripture
|
||||
};
|
||||
});
|
||||
|
||||
return { voices, layers, args: argsWithScripture };
|
||||
};
|
||||
@@ -0,0 +1,643 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { POS_LAYER_COLORS, type PosArgument } from '$lib/data/apologetik';
|
||||
import CaseTabs from '$lib/components/faith/CaseTabs.svelte';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const faithLang = $derived(data?.faithLang ?? 'faith');
|
||||
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
|
||||
const isLatin = $derived(data?.lang === 'la');
|
||||
const isGerman = $derived(data?.lang === 'de');
|
||||
|
||||
const POS_VOICES = $derived(data.voices);
|
||||
const POS_LAYERS = $derived(data.layers);
|
||||
const POS_ARGUMENTS = $derived(data.args);
|
||||
|
||||
let activeId = $state<string>('');
|
||||
|
||||
onMount(() => {
|
||||
const els = document.querySelectorAll<HTMLElement>('.pos-row');
|
||||
if (!els.length) return;
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||
if (visible[0]) {
|
||||
activeId = visible[0].target.id.replace(/^pos-/, '');
|
||||
}
|
||||
},
|
||||
{ rootMargin: '-64px 0px -60% 0px', threshold: 0 }
|
||||
);
|
||||
els.forEach((el) => io.observe(el));
|
||||
return () => io.disconnect();
|
||||
});
|
||||
|
||||
function jumpTo(e: MouseEvent, id: string) {
|
||||
const el = document.getElementById(`pos-${id}`);
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.replaceState(null, '', `#pos-${id}`);
|
||||
}
|
||||
|
||||
const labels = $derived(
|
||||
isLatin
|
||||
? {
|
||||
heading: 'Cur Christianitas vera est.',
|
||||
lede: 'Duodecim fila. Naturalia, theistica, christiana.',
|
||||
evidenceLabel: 'EVIDENTIA',
|
||||
articulatedBy: 'Articulatum a',
|
||||
strengthLabel: 'Pondus',
|
||||
cumulativeTitle: 'Cumulus argumentorum',
|
||||
cumulativeSub: 'Filum unum infirmum est. Tot fila, funis fortis.',
|
||||
layerLabels: { natural: 'Supernaturale', theism: 'Theismus', christianity: 'Christianitas' },
|
||||
convergeLabel: 'Christianitas',
|
||||
convergeSub: 'vera est.'
|
||||
}
|
||||
: isGerman
|
||||
? {
|
||||
heading: 'Warum das Christentum wahr ist.',
|
||||
lede: 'Zwölf Fäden. Vier zur übernatürlichen Wirklichkeit, vier zum einen Gott, vier zum besonderen Anspruch des Christentums.',
|
||||
evidenceLabel: 'BELEG',
|
||||
articulatedBy: 'Vorgetragen von',
|
||||
strengthLabel: 'Beweisgewicht',
|
||||
cumulativeTitle: 'Kumulativer Fall',
|
||||
cumulativeSub:
|
||||
'Kein einzelnes Argument zwingt. Zusammen aber laufen sie zusammen — ein Strang aus unabhängigen Fäden, jeder allein schwach, im Bündel stark.',
|
||||
layerLabels: { natural: 'Übernatürlich', theism: 'Theismus', christianity: 'Christentum' },
|
||||
convergeLabel: 'Christentum',
|
||||
convergeSub: 'ist wahr.'
|
||||
}
|
||||
: {
|
||||
heading: 'Why Christianity is true.',
|
||||
lede: 'Twelve threads. The first four argue that the supernatural is not an embarrassment but a stable fact of human experience. The next four argue that the supernatural is best described as one personal God. The last four argue that this God has spoken, in Israel and in Christ.',
|
||||
evidenceLabel: 'EVIDENCE',
|
||||
articulatedBy: 'Articulated by',
|
||||
strengthLabel: 'Evidential weight',
|
||||
cumulativeTitle: 'Cumulative case',
|
||||
cumulativeSub:
|
||||
'No single argument compels. Together they converge — a rope of independent threads, each weak alone, strong as a bundle. Hover any thread to read its claim.',
|
||||
layerLabels: {
|
||||
natural: 'Supernatural',
|
||||
theism: 'Theism',
|
||||
christianity: 'Christianity'
|
||||
},
|
||||
convergeLabel: 'Christianity',
|
||||
convergeSub: 'is true.'
|
||||
}
|
||||
);
|
||||
|
||||
function byLayer(lid: string): PosArgument[] {
|
||||
return POS_ARGUMENTS.filter((a) => a.layer === lid);
|
||||
}
|
||||
|
||||
const W = 700;
|
||||
const H = 240;
|
||||
const targetX = W - 110;
|
||||
const targetY = H / 2;
|
||||
const bands: Record<string, [number, number]> = {
|
||||
natural: [20, 80],
|
||||
theism: [90, 150],
|
||||
christianity: [160, 220]
|
||||
};
|
||||
|
||||
type Item = PosArgument & { y: number };
|
||||
const cumulativeItems = $derived.by<Item[]>(() =>
|
||||
POS_ARGUMENTS.map((a) => {
|
||||
const [y0, y1] = bands[a.layer];
|
||||
const inLayer = POS_ARGUMENTS.filter((x) => x.layer === a.layer);
|
||||
const idx = inLayer.findIndex((x) => x.id === a.id);
|
||||
const y = y0 + (y1 - y0) * ((idx + 0.5) / inLayer.length);
|
||||
return { ...a, y };
|
||||
})
|
||||
);
|
||||
|
||||
const tocItems = $derived(
|
||||
POS_ARGUMENTS.map((a) => {
|
||||
const layer = POS_LAYERS.find((l) => l.id === a.layer);
|
||||
return {
|
||||
id: a.id,
|
||||
n: a.n,
|
||||
short: a.title,
|
||||
title: a.title,
|
||||
href: `#pos-${a.id}`,
|
||||
group: labels.layerLabels[a.layer]
|
||||
};
|
||||
})
|
||||
);
|
||||
const tocLabel = $derived(
|
||||
isLatin ? 'Argumenta' : isGerman ? 'Belege' : 'Evidences'
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{labels.heading} · bocken.org</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A positive case for Christianity in twelve threads, organized in three layers: the supernatural is real, there is one God, Christianity is that revelation. Voices: Habermas, Polkinghorne, Newman, Hart, Lewis, Wright, Hahn, Plantinga, Eliade, the Perennialist."
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Spectral:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="positive">
|
||||
<ApologetikToc
|
||||
title={tocLabel}
|
||||
items={tocItems}
|
||||
{activeId}
|
||||
onItemClick={(e, id) => jumpTo(e, id)}
|
||||
/>
|
||||
|
||||
<CaseTabs {faithLang} active="pro" />
|
||||
|
||||
<section class="pos-head">
|
||||
<h1>{labels.heading}</h1>
|
||||
<p class="lede">{labels.lede}</p>
|
||||
</section>
|
||||
|
||||
<section class="cumulative" aria-label={labels.cumulativeTitle}>
|
||||
<div class="cumulative-title">{labels.cumulativeTitle}</div>
|
||||
<p class="cumulative-sub">{labels.cumulativeSub}</p>
|
||||
<svg
|
||||
class="cum-svg"
|
||||
viewBox="0 0 {W} {H}"
|
||||
role="img"
|
||||
aria-label={labels.cumulativeTitle}
|
||||
>
|
||||
{#each cumulativeItems as it (it.id)}
|
||||
{@const stroke = POS_LAYER_COLORS[it.layer]}
|
||||
{@const opacity = 0.25 + (it.strength / 5) * 0.55}
|
||||
{@const sw = 0.8 + it.strength * 0.6}
|
||||
<a href="/{faithLang}/{slug}/pro/{it.id}" aria-label={it.title}>
|
||||
<path
|
||||
d="M 8 {it.y} C {W * 0.45} {it.y}, {W * 0.55} {targetY}, {targetX} {targetY}"
|
||||
fill="none"
|
||||
stroke={stroke}
|
||||
stroke-width={sw}
|
||||
stroke-linecap="round"
|
||||
{opacity}
|
||||
>
|
||||
<title>{String(it.n).padStart(2, '0')} — {it.title}</title>
|
||||
</path>
|
||||
<circle cx="8" cy={it.y} r="3.5" fill={stroke} />
|
||||
<text
|
||||
x="18"
|
||||
y={it.y + 3.5}
|
||||
font-size="9"
|
||||
fill="var(--color-text-secondary)"
|
||||
font-family="ui-monospace, Menlo, monospace"
|
||||
>
|
||||
{String(it.n).padStart(2, '0')}
|
||||
</text>
|
||||
</a>
|
||||
{/each}
|
||||
<circle cx={targetX} cy={targetY} r="14" fill="var(--color-text-primary)" />
|
||||
<circle
|
||||
cx={targetX}
|
||||
cy={targetY}
|
||||
r="22"
|
||||
fill="none"
|
||||
stroke="var(--color-text-primary)"
|
||||
stroke-width="0.6"
|
||||
opacity="0.5"
|
||||
/>
|
||||
<circle
|
||||
cx={targetX}
|
||||
cy={targetY}
|
||||
r="34"
|
||||
fill="none"
|
||||
stroke="var(--color-text-primary)"
|
||||
stroke-width="0.4"
|
||||
opacity="0.25"
|
||||
/>
|
||||
<text
|
||||
x={targetX + 44}
|
||||
y={targetY - 4}
|
||||
font-size="13"
|
||||
font-weight="700"
|
||||
fill="var(--color-text-primary)">{labels.convergeLabel}</text
|
||||
>
|
||||
<text x={targetX + 44} y={targetY + 12} font-size="10" fill="var(--color-text-secondary)"
|
||||
>{labels.convergeSub}</text
|
||||
>
|
||||
</svg>
|
||||
<div class="cum-legend">
|
||||
<span class="cum-legend-item"
|
||||
><span class="dot" style="background:{POS_LAYER_COLORS.natural};"></span
|
||||
>{labels.layerLabels.natural}</span
|
||||
>
|
||||
<span class="cum-legend-item"
|
||||
><span class="dot" style="background:{POS_LAYER_COLORS.theism};"></span
|
||||
>{labels.layerLabels.theism}</span
|
||||
>
|
||||
<span class="cum-legend-item"
|
||||
><span class="dot" style="background:{POS_LAYER_COLORS.christianity};"></span
|
||||
>{labels.layerLabels.christianity}</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#each POS_LAYERS as layer (layer.id)}
|
||||
<section class="layer-section">
|
||||
<div class="layer-head">
|
||||
<span class="layer-num">{layer.sub}</span>
|
||||
</div>
|
||||
<h2 class="layer-title">{layer.title}</h2>
|
||||
|
||||
{#each byLayer(layer.id) as arg (arg.id)}
|
||||
<article class="pos-row" id="pos-{arg.id}">
|
||||
<a
|
||||
class="card-link"
|
||||
href="/{faithLang}/{slug}/pro/{arg.id}"
|
||||
aria-label={arg.title}
|
||||
></a>
|
||||
<div class="pos-num">
|
||||
{String(arg.n).padStart(2, '0')}
|
||||
<small>{labels.evidenceLabel}</small>
|
||||
</div>
|
||||
<div class="pos-body">
|
||||
<h3>{arg.title}</h3>
|
||||
<p class="pos-claim">{arg.claim}</p>
|
||||
{#if arg.note}
|
||||
<div class="pos-note">{arg.note}</div>
|
||||
{/if}
|
||||
<div class="strength-row">
|
||||
<span>{labels.strengthLabel}</span>
|
||||
<span class="strength-bar" aria-label="{arg.strength} of 5">
|
||||
{#each [1, 2, 3, 4, 5] as i (i)}
|
||||
<span class="pip" class:on={i <= arg.strength}></span>
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
<aside class="scripture">
|
||||
<p class="verse">"{arg.scripture.text}"</p>
|
||||
<div class="ref">{arg.scripture.ref}</div>
|
||||
</aside>
|
||||
<p class="pos-thesis">{arg.thesis}</p>
|
||||
|
||||
<div class="answer-rail">
|
||||
<span class="label">{labels.articulatedBy}</span>
|
||||
{#each Object.keys(arg.voices) as vid (vid)}
|
||||
{@const v = POS_VOICES[vid]}
|
||||
<a
|
||||
class="archetype-badge"
|
||||
href="/{faithLang}/{slug}/pro/{arg.id}#voice-{vid}"
|
||||
title="{v.name} — {v.sub}"
|
||||
>
|
||||
<span class="glyph" aria-hidden="true" style="background:{v.color};"
|
||||
>{v.glyph}</span
|
||||
>
|
||||
<span>{v.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.positive {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 80px;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.pos-head {
|
||||
max-width: 760px;
|
||||
margin: 36px auto 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.pos-head h1 {
|
||||
font-size: clamp(2rem, 4.4vw, 3.2rem);
|
||||
line-height: 1.08;
|
||||
font-weight: 700;
|
||||
margin: 0 0 18px;
|
||||
letter-spacing: -0.01em;
|
||||
text-align: left;
|
||||
}
|
||||
.pos-head .lede {
|
||||
font-size: 1.12rem;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 60ch;
|
||||
font-family: 'Spectral', 'Lora', Georgia, serif;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cumulative {
|
||||
max-width: 760px;
|
||||
margin: 38px auto 0;
|
||||
padding: 24px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
.cumulative-title {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 6px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.cumulative-sub {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0 0 18px;
|
||||
max-width: 56ch;
|
||||
}
|
||||
.cum-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.cum-svg a {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cum-svg a:hover path {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.cum-legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.cum-legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.cum-legend-item .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.layer-section {
|
||||
max-width: 760px;
|
||||
margin: 48px auto 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.layer-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.layer-num {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.layer-title {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin: 14px 0 4px;
|
||||
letter-spacing: -0.005em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pos-row {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr;
|
||||
gap: 22px;
|
||||
padding: 30px 18px;
|
||||
margin: 0 -18px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
align-items: start;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background var(--transition-fast);
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
.pos-row:hover {
|
||||
background: color-mix(in oklab, var(--color-bg-secondary) 60%, transparent);
|
||||
}
|
||||
.pos-row:has(.card-link:focus-visible) {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.pos-row:has(.card-link:hover) h3 {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.pos-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.card-link {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.card-link:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
.pos-row > .pos-num,
|
||||
.pos-row > .pos-body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pos-body h3 {
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
.archetype-badge {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pos-num {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
color: var(--color-text-tertiary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.pos-num small {
|
||||
display: block;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.14em;
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.pos-body h3 {
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.18;
|
||||
font-weight: 700;
|
||||
margin: 0 0 14px;
|
||||
letter-spacing: -0.005em;
|
||||
text-align: left;
|
||||
}
|
||||
.pos-claim {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 16px;
|
||||
color: var(--color-text-primary);
|
||||
max-width: 60ch;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.pos-thesis {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 60ch;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.pos-note {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-style: italic;
|
||||
margin: -10px 0 18px;
|
||||
max-width: 60ch;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-left: 2px solid var(--color-border);
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
}
|
||||
|
||||
.scripture {
|
||||
margin: 18px 0;
|
||||
padding: 16px 20px;
|
||||
border-left: 3px solid var(--color-text-primary);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
max-width: 56ch;
|
||||
}
|
||||
.scripture .verse {
|
||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
margin: 0 0 6px;
|
||||
color: var(--color-text-primary);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.scripture .ref {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.strength-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 4px 0 18px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-tertiary);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.strength-bar {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
}
|
||||
.strength-bar .pip {
|
||||
width: 18px;
|
||||
height: 6px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
.strength-bar .pip.on {
|
||||
background: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.answer-rail {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.answer-rail .label {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: 4px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.archetype-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px 5px 5px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-normal),
|
||||
background var(--transition-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
.archetype-badge:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--color-bg-elevated);
|
||||
}
|
||||
.archetype-badge:active,
|
||||
.archetype-badge:focus-visible {
|
||||
transform: scale(0.95);
|
||||
outline: none;
|
||||
}
|
||||
.archetype-badge .glyph {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-pill);
|
||||
color: white;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.pos-row {
|
||||
grid-template-columns: 40px 1fr;
|
||||
gap: 14px;
|
||||
padding: 24px 0;
|
||||
}
|
||||
.pos-num {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.pos-body h3 {
|
||||
font-size: 1.18rem;
|
||||
}
|
||||
.pos-head h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.layer-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import {
|
||||
findPositiveArgumentLang,
|
||||
getPosArguments,
|
||||
getPosLayers,
|
||||
getPosVoices,
|
||||
POS_ARGUMENTS as EN_POS_ARGUMENTS
|
||||
} from '$lib/data/apologetik';
|
||||
import { resolveScriptureForLang } from '$lib/server/scriptureLookup';
|
||||
|
||||
export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
const { lang } = await parent();
|
||||
const [arg, voices, layers, args] = await Promise.all([
|
||||
findPositiveArgumentLang(params.posArgId, lang),
|
||||
getPosVoices(lang),
|
||||
getPosLayers(lang),
|
||||
getPosArguments(lang)
|
||||
]);
|
||||
if (!arg) {
|
||||
error(404, 'Argument not found');
|
||||
}
|
||||
|
||||
const lng: 'en' | 'de' = lang === 'de' ? 'de' : 'en';
|
||||
const enArg = EN_POS_ARGUMENTS.find((x) => x.id === arg.id);
|
||||
const argument = enArg
|
||||
? (() => {
|
||||
const resolved = resolveScriptureForLang(enArg.scripture.ref, lng);
|
||||
return {
|
||||
...arg,
|
||||
scripture: resolved.text ? resolved : arg.scripture
|
||||
};
|
||||
})()
|
||||
: arg;
|
||||
|
||||
const argsWithScripture = args.map((a) => {
|
||||
const en = EN_POS_ARGUMENTS.find((x) => x.id === a.id);
|
||||
if (!en) return a;
|
||||
const resolved = resolveScriptureForLang(en.scripture.ref, lng);
|
||||
return { ...a, scripture: resolved.text ? resolved : a.scripture };
|
||||
});
|
||||
|
||||
return { argument, voices, layers, args: argsWithScripture };
|
||||
};
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
const faithLang = $derived(data?.faithLang ?? 'faith');
|
||||
const slug = $derived(faithLang === 'faith' ? 'apologetics' : 'apologetik');
|
||||
const isLatin = $derived(data?.lang === 'la');
|
||||
const isGerman = $derived(data?.lang === 'de');
|
||||
const arg = $derived(data.argument);
|
||||
const POS_VOICES = $derived(data.voices);
|
||||
const POS_LAYERS = $derived(data.layers);
|
||||
const POS_ARGUMENTS = $derived(data.args);
|
||||
const layer = $derived(POS_LAYERS.find((l) => l.id === arg.layer));
|
||||
|
||||
const layerLabels = $derived(
|
||||
isLatin
|
||||
? { natural: 'Supernaturale', theism: 'Theismus', christianity: 'Christianitas' }
|
||||
: isGerman
|
||||
? { natural: 'Übernatürlich', theism: 'Theismus', christianity: 'Christentum' }
|
||||
: { natural: 'Supernatural', theism: 'Theism', christianity: 'Christianity' }
|
||||
);
|
||||
const tocLabel = $derived(
|
||||
isLatin ? 'Argumenta' : isGerman ? 'Belege' : 'Evidences'
|
||||
);
|
||||
const tocItems = $derived(
|
||||
POS_ARGUMENTS.map((a) => ({
|
||||
id: a.id,
|
||||
n: a.n,
|
||||
short: a.title,
|
||||
title: a.title,
|
||||
href: `/${faithLang}/${slug}/pro/${a.id}`,
|
||||
group: layerLabels[a.layer]
|
||||
}))
|
||||
);
|
||||
|
||||
const voiceIds = $derived(Object.keys(arg.voices));
|
||||
let userSelected = $state<string | null>(null);
|
||||
const activeId = $derived(
|
||||
userSelected && voiceIds.includes(userSelected) ? userSelected : (voiceIds[0] ?? '')
|
||||
);
|
||||
const voice = $derived(POS_VOICES[activeId]);
|
||||
const counter = $derived(arg.voices[activeId]);
|
||||
|
||||
const labels = $derived(
|
||||
isLatin
|
||||
? {
|
||||
back: '← Ad omnia argumenta pro',
|
||||
eyebrow: 'Evidentia',
|
||||
claimTitle: 'Argumentum',
|
||||
strengthLabel: 'Pondus',
|
||||
citations: 'Citationes',
|
||||
related: 'Argumenta connexa'
|
||||
}
|
||||
: isGerman
|
||||
? {
|
||||
back: '← Alle positiven Argumente',
|
||||
eyebrow: 'Beleg',
|
||||
claimTitle: 'Der Anspruch',
|
||||
strengthLabel: 'Beweisgewicht',
|
||||
citations: 'Quellen',
|
||||
related: 'Verwandte Belege'
|
||||
}
|
||||
: {
|
||||
back: '← All positive arguments',
|
||||
eyebrow: 'Evidence',
|
||||
claimTitle: 'The claim',
|
||||
strengthLabel: 'Evidential weight',
|
||||
citations: 'Citations',
|
||||
related: 'Related evidences'
|
||||
}
|
||||
);
|
||||
|
||||
const acc = $derived(voice?.colorHex ?? '#5E81AC');
|
||||
const accSoft = $derived(voice ? hexToRgba(voice.colorHex, 0.14) : 'rgba(94,129,172,0.14)');
|
||||
|
||||
function hexToRgba(hex: string, alpha: number): string {
|
||||
const h = hex.replace('#', '');
|
||||
const r = parseInt(h.slice(0, 2), 16);
|
||||
const g = parseInt(h.slice(2, 4), 16);
|
||||
const b = parseInt(h.slice(4, 6), 16);
|
||||
return `rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
|
||||
function selectVoice(id: string) {
|
||||
userSelected = id;
|
||||
if (typeof window !== 'undefined') {
|
||||
history.replaceState(null, '', `#voice-${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith('#voice-')) {
|
||||
const id = hash.slice('#voice-'.length);
|
||||
if (voiceIds.includes(id)) {
|
||||
userSelected = id;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{arg.title} · {isLatin ? 'Argumenta pro' : isGerman ? 'Positives' : 'Positive case'} · bocken.org</title>
|
||||
<meta name="description" content={arg.claim} />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;0,700;1,400&family=Spectral:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<ApologetikToc title={tocLabel} items={tocItems} activeId={arg.id} />
|
||||
|
||||
<main class="detail">
|
||||
<a class="back-link" href="/{faithLang}/{slug}/pro">{labels.back}</a>
|
||||
|
||||
{#if layer}
|
||||
<div class="layer-tag">{layer.sub}</div>
|
||||
{/if}
|
||||
<div class="detail-eyebrow">{labels.eyebrow} {String(arg.n).padStart(2, '0')}</div>
|
||||
<h1>{arg.title}</h1>
|
||||
|
||||
<section class="claim-block">
|
||||
<h3>{labels.claimTitle}</h3>
|
||||
<p class="claim-text">{arg.claim}</p>
|
||||
<p class="thesis-text">{arg.thesis}</p>
|
||||
{#if arg.note}
|
||||
<div class="pos-note">{arg.note}</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<div class="strength-row">
|
||||
<span>{labels.strengthLabel}</span>
|
||||
<span class="strength-bar" aria-label="{arg.strength} of 5">
|
||||
{#each [1, 2, 3, 4, 5] as i (i)}
|
||||
<span class="pip" class:on={i <= arg.strength}></span>
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<aside class="scripture">
|
||||
<p class="verse">"{arg.scripture.text}"</p>
|
||||
<div class="ref">{arg.scripture.ref}</div>
|
||||
</aside>
|
||||
|
||||
<div class="tabs" role="tablist" aria-label="Voices">
|
||||
{#each voiceIds as id (id)}
|
||||
{@const v = POS_VOICES[id]}
|
||||
{@const isActive = id === activeId}
|
||||
<a
|
||||
href="#voice-{id}"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
class="tab"
|
||||
class:active={isActive}
|
||||
style:border-bottom-color={isActive ? v.colorHex : 'transparent'}
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
selectVoice(id);
|
||||
}}
|
||||
>
|
||||
<span class="glyph" aria-hidden="true" style="background:{v.color};">{v.glyph}</span>
|
||||
<span>{v.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if voice && counter}
|
||||
<section
|
||||
class="answer-panel"
|
||||
style:--acc={acc}
|
||||
style:--acc-soft={accSoft}
|
||||
style="border-left-color: {acc}; background-image: linear-gradient(to right, {accSoft}, transparent 320px);"
|
||||
id="voice-{activeId}"
|
||||
>
|
||||
<header class="answer-author">
|
||||
<span class="glyph lg" aria-hidden="true" style="background:{voice.color};"
|
||||
>{voice.glyph}</span
|
||||
>
|
||||
<div>
|
||||
<div class="who">{voice.name}</div>
|
||||
<div class="sub">{voice.sub}</div>
|
||||
{#if voice.era !== '—'}
|
||||
<div class="era">{voice.era}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<h2 class="answer-lede">{counter.lede}</h2>
|
||||
<div class="answer-body">
|
||||
{#each counter.body as p, i (i)}
|
||||
<p>{p}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if counter.cites.length > 0}
|
||||
<div class="cites">
|
||||
<span class="ct">{labels.citations}</span>
|
||||
{counter.cites.join(' · ')}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if arg.related.length > 0}
|
||||
<section class="related" aria-label={labels.related}>
|
||||
<h3>{labels.related}</h3>
|
||||
<div class="related-list">
|
||||
{#each arg.related as rid (rid)}
|
||||
{@const r = POS_ARGUMENTS.find((x) => x.id === rid)}
|
||||
{#if r}
|
||||
<a class="related-item" href="/{faithLang}/{slug}/pro/{r.id}">
|
||||
<span class="num">{String(r.n).padStart(2, '0')}</span>
|
||||
{r.title}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.detail {
|
||||
max-width: 820px;
|
||||
margin: 36px auto 0;
|
||||
padding: 0 24px 80px;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-bottom: 24px;
|
||||
transition:
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
.back-link:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.back-link:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.layer-tag {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
margin-bottom: 12px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.detail-eyebrow {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: 14px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.detail h1 {
|
||||
font-size: clamp(1.8rem, 3.6vw, 2.6rem);
|
||||
line-height: 1.12;
|
||||
margin: 0 0 22px;
|
||||
letter-spacing: -0.01em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.claim-block {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 22px 24px;
|
||||
margin: 0 0 28px;
|
||||
}
|
||||
.claim-block h3 {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 12px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
text-align: left;
|
||||
}
|
||||
.claim-text {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.55;
|
||||
margin: 0 0 14px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.thesis-text {
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.pos-note {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-style: italic;
|
||||
margin: 14px 0 0;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-left: 2px solid var(--color-border);
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
}
|
||||
|
||||
.strength-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 0 0 18px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-tertiary);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
.strength-bar {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
}
|
||||
.strength-bar .pip {
|
||||
width: 18px;
|
||||
height: 6px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
.strength-bar .pip.on {
|
||||
background: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.scripture {
|
||||
margin: 18px 0 28px;
|
||||
padding: 16px 20px;
|
||||
border-left: 3px solid var(--color-text-primary);
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
max-width: 60ch;
|
||||
}
|
||||
.scripture .verse {
|
||||
font-family: 'Cormorant Garamond', Georgia, serif;
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.4;
|
||||
font-style: italic;
|
||||
margin: 0 0 6px;
|
||||
color: var(--color-text-primary);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.scripture .ref {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
transition:
|
||||
color var(--transition-fast),
|
||||
border-color var(--transition-fast);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.tab:visited {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.tab:hover,
|
||||
.tab:focus-visible {
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.tab.active,
|
||||
.tab.active:visited {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.glyph {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-pill);
|
||||
color: white;
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.glyph.lg {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.answer-panel {
|
||||
--acc: var(--color-primary);
|
||||
--acc-soft: rgba(94, 129, 172, 0.12);
|
||||
border-left: 3px solid var(--acc);
|
||||
padding: 24px 26px;
|
||||
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.answer-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.answer-author .who {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.answer-author .sub {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.answer-author .era {
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.answer-lede {
|
||||
font-size: 1.18rem;
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
max-width: 56ch;
|
||||
text-wrap: pretty;
|
||||
letter-spacing: -0.005em;
|
||||
text-align: left;
|
||||
}
|
||||
.answer-body {
|
||||
font-size: 1rem;
|
||||
line-height: 1.65;
|
||||
max-width: 60ch;
|
||||
}
|
||||
.answer-body p {
|
||||
margin: 0 0 14px;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
.answer-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cites {
|
||||
margin-top: 18px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed var(--color-border);
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-tertiary);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
}
|
||||
.cites .ct {
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.68rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.related {
|
||||
margin: 38px 0 0;
|
||||
padding-top: 22px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.related h3 {
|
||||
font-size: 0.76rem;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-tertiary);
|
||||
margin: 0 0 14px;
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
text-align: left;
|
||||
}
|
||||
.related-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.related-item {
|
||||
display: block;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg-secondary);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.35;
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
background var(--transition-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
.related-item:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
.related-item .num {
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
||||
font-size: 0.76rem;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.detail h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.claim-block {
|
||||
padding: 18px 16px;
|
||||
}
|
||||
.answer-panel {
|
||||
padding: 18px 18px;
|
||||
}
|
||||
.tab {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import ArrowDown from '@lucide/svelte/icons/arrow-down';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import { page } from '$app/stores';
|
||||
import ApologetikToc from '$lib/components/faith/ApologetikToc.svelte';
|
||||
/** @type {number | string | null} */
|
||||
let expanded = $state(null);
|
||||
const isGerman = $derived($page.url.pathname.startsWith('/glaube'));
|
||||
@@ -12,6 +14,49 @@
|
||||
expanded = expanded === id ? null : id;
|
||||
}
|
||||
|
||||
const tocItems = [
|
||||
{ id: 'ursprung', short: 'Ursprung', href: '#ursprung' },
|
||||
{ id: 'warum', short: 'Warum die 10 Gebote?', href: '#warum' },
|
||||
{ id: 'biblischer-text', short: 'Biblischer Text', href: '#biblischer-text' },
|
||||
{ id: 'ueberlieferung', short: 'Katechetische Überlieferung', href: '#ueberlieferung' },
|
||||
{ id: 'erstes-gebot', short: 'Das Erste Gebot', href: '#erstes-gebot' },
|
||||
{ id: 'drei-pflichten', short: 'Drei Pflichten', href: '#drei-pflichten', group: 'Das Erste Gebot' },
|
||||
{ id: 'tugend-der-religion', short: 'Tugend der Religion', href: '#tugend-der-religion', group: 'Das Erste Gebot' },
|
||||
{ id: 'vier-akte', short: 'Vier Akte der religio', href: '#vier-akte', group: 'Das Erste Gebot' },
|
||||
{ id: 'warnung', short: 'Warnung', href: '#warnung', group: 'Das Erste Gebot' }
|
||||
];
|
||||
|
||||
let activeId = $state('');
|
||||
|
||||
onMount(() => {
|
||||
const els = tocItems
|
||||
.map((it) => document.getElementById(it.id))
|
||||
.filter((el) => el !== null);
|
||||
if (!els.length) return;
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((e) => e.isIntersecting)
|
||||
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||
if (visible[0]) {
|
||||
activeId = visible[0].target.id;
|
||||
}
|
||||
},
|
||||
{ rootMargin: '-64px 0px -60% 0px', threshold: 0 }
|
||||
);
|
||||
els.forEach((el) => io.observe(el));
|
||||
return () => io.disconnect();
|
||||
});
|
||||
|
||||
/** @param {MouseEvent} e @param {string} id */
|
||||
function jumpTo(e, id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
history.replaceState(null, '', `#${id}`);
|
||||
}
|
||||
|
||||
const gebote = [
|
||||
{ nr: 1, text: 'Du sollst keine anderen Götter neben mir haben.', active: true, tablet: 'god' },
|
||||
{ nr: 2, text: 'Du sollst den Namen Gottes nicht verunehren.', active: false, tablet: 'god' },
|
||||
@@ -39,23 +84,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<nav class="toc">
|
||||
<ul>
|
||||
<li><a href="#ursprung">Ursprung</a></li>
|
||||
<li><a href="#warum">Warum die 10 Gebote?</a></li>
|
||||
<li><a href="#biblischer-text">Biblischer Text</a></li>
|
||||
<li><a href="#ueberlieferung">Katechetische Überlieferung</a></li>
|
||||
<li>
|
||||
<a href="#erstes-gebot">Das Erste Gebot</a>
|
||||
<ul>
|
||||
<li><a href="#drei-pflichten">Drei Pflichten</a></li>
|
||||
<li><a href="#tugend-der-religion">Tugend der Religion</a></li>
|
||||
<li><a href="#vier-akte">Vier Akte der <i>religio</i></a></li>
|
||||
<li><a href="#warnung">Warnung</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<ApologetikToc title="Inhalt" items={tocItems} {activeId} onItemClick={jumpTo} />
|
||||
<div class="page">
|
||||
<header class="hero">
|
||||
<h1>Die Zehn Gebote Gottes</h1>
|
||||
@@ -319,41 +348,6 @@
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.toc {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.toc {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 4rem;
|
||||
left: max(1rem, calc((100vw - 700px) / 2 - 220px));
|
||||
width: 190px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.toc ul ul {
|
||||
padding-left: 0.75em;
|
||||
border-left: 1px solid var(--color-border);
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.toc li {
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.toc a {
|
||||
color: var(--color-text-tertiary);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.toc a:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
.page {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
|
||||
Reference in New Issue
Block a user