Complete frontend overhaul with WCAG 2.2 accessibility

Design System:
- Custom Hugo theme "swissfini" with editorial aesthetic
- CSS custom properties for comprehensive theming
- Light, Dark, and High Contrast themes
- Print-optimized styles

Accessibility Self-Service Controls:
- Font size adjustment (5 levels: 75%-150%)
- Theme toggle (Light/Dark/High Contrast/System)
- Dyslexia-friendly font (OpenDyslexic)
- Line spacing control (4 levels)
- Reduced motion toggle
- Reading width control (3 levels)
- Enhanced focus indicators
- All preferences persisted via localStorage

Templates & Components:
- Base layout with skip-links and accessibility panel
- Article template with drop caps and blockquotes
- Irony box and conclusion shortcodes
- Responsive header with mobile navigation

Content:
- Migrated SCION vs SD-WAN analysis from HTML
- Homepage teaser with paywall-style CTA

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 07:18:22 +00:00
parent 23d86a3a1a
commit bda1791fa5
28 changed files with 4667 additions and 2 deletions

View File

@@ -0,0 +1,194 @@
/* ============================================
Modern CSS Reset
Based on Josh Comeau's reset with accessibility enhancements
============================================ */
/* Box sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default margin and padding */
* {
margin: 0;
padding: 0;
}
/* Prevent font size inflation */
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
scroll-behavior: smooth;
}
/* Smooth scroll only when no preference */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Core body defaults */
body {
min-height: 100vh;
line-height: var(--line-height-body);
font-family: var(--font-body);
font-size: var(--font-size-base);
color: var(--color-text-primary);
background-color: var(--color-bg-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* Remove list styles on ul, ol with role="list" */
ul[role='list'],
ol[role='list'] {
list-style: none;
}
/* Set shorter line heights on headings and interactive elements */
h1, h2, h3, h4, h5, h6 {
line-height: var(--line-height-heading);
text-wrap: balance;
}
/* Cap line length for readability */
p, li, figcaption {
max-width: var(--reading-width);
text-wrap: pretty;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
color: var(--color-link);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.15em;
transition: var(--transition-colors);
}
a:not([class]):hover {
color: var(--color-link-hover);
}
a:not([class]):visited {
color: var(--color-link-visited);
}
/* Make images easier to work with */
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
/* Remove default button styles */
button {
background: none;
border: none;
cursor: pointer;
color: inherit;
}
/* Textarea without resize (unless specified) */
textarea:not([class]) {
resize: vertical;
}
/* Make sure textareas without a rows attribute are not tiny */
textarea:not([rows]) {
min-height: 10em;
}
/* Anything that has been anchored to should have extra scroll margin */
:target {
scroll-margin-block: 5ex;
}
/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Data attribute for user-controlled reduced motion */
[data-reduced-motion="true"] *,
[data-reduced-motion="true"] *::before,
[data-reduced-motion="true"] *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* Screen reader only utility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Focus visible for accessibility */
.sr-only:focus-visible {
position: static;
width: auto;
height: auto;
padding: 0;
margin: 0;
overflow: visible;
clip: auto;
white-space: normal;
}
/* Selection styling */
::selection {
background-color: var(--color-accent-cardinal-subtle);
color: var(--color-text-primary);
}
/* Focus outline styles */
:focus {
outline: none;
}
:focus-visible {
outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color);
outline-offset: var(--focus-outline-offset);
}
/* Enhanced focus mode */
[data-enhanced-focus="true"] :focus-visible {
outline-width: var(--focus-enhanced-width);
outline-offset: var(--focus-enhanced-offset);
box-shadow: 0 0 0 calc(var(--focus-enhanced-width) + 2px) var(--color-focus-ring);
}

View File

@@ -0,0 +1,391 @@
/* ============================================
Typography System
Editorial excellence for investigative satire
============================================ */
/* Google Fonts Import */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Literata:ital,opsz,wght@0,7..72,400;0,7..72,500;0,7..72,600;0,7..72,700;1,7..72,400;1,7..72,500&display=swap');
/* OpenDyslexic Font Face - loaded from CDN */
@font-face {
font-family: 'OpenDyslexic';
src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Italic.woff') format('woff');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* ========================================
HEADINGS
======================================== */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-body);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-tight);
margin-bottom: var(--space-4);
}
h1 {
font-size: var(--font-size-4xl);
line-height: 1.1;
letter-spacing: var(--letter-spacing-tighter);
margin-bottom: var(--space-6);
}
h2 {
font-size: var(--font-size-2xl);
line-height: 1.2;
margin-top: var(--space-12);
margin-bottom: var(--space-4);
padding-bottom: var(--space-2);
border-bottom: var(--border-width-thick) solid var(--color-accent-cardinal);
}
h3 {
font-size: var(--font-size-xl);
line-height: 1.3;
margin-top: var(--space-8);
color: var(--color-accent-navy);
}
h4 {
font-size: var(--font-size-lg);
line-height: 1.4;
margin-top: var(--space-6);
}
h5, h6 {
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: var(--letter-spacing-wide);
}
/* ========================================
BODY TEXT
======================================== */
p {
font-size: var(--font-size-md);
line-height: var(--line-height-body);
letter-spacing: var(--letter-spacing-body);
word-spacing: var(--word-spacing-body);
margin-bottom: var(--space-6);
color: var(--color-text-primary);
}
/* Lead paragraph */
.lead,
.article-body > p:first-of-type {
font-size: var(--font-size-lg);
line-height: 1.7;
color: var(--color-text-secondary);
}
/* Small text */
small,
.text-small {
font-size: var(--font-size-sm);
}
.text-xs {
font-size: var(--font-size-xs);
}
/* ========================================
DROP CAP - Vintage newspaper style
======================================== */
.drop-cap::first-letter,
.article-body > p:first-of-type::first-letter {
float: left;
font-size: 4.5em;
line-height: 0.8;
font-weight: var(--font-weight-bold);
color: var(--color-accent-cardinal);
margin-right: var(--space-3);
margin-top: 0.1em;
font-family: var(--font-body);
}
/* ========================================
LINKS
======================================== */
a {
color: var(--color-link);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
transition: var(--transition-colors);
}
a:hover {
color: var(--color-link-hover);
text-decoration-thickness: 2px;
}
a:focus-visible {
outline: var(--focus-outline-width) var(--focus-outline-style) var(--color-focus);
outline-offset: var(--focus-outline-offset);
border-radius: var(--radius-sm);
}
/* ========================================
LISTS
======================================== */
ul, ol {
margin-bottom: var(--space-6);
padding-left: var(--space-6);
}
li {
font-size: var(--font-size-md);
line-height: var(--line-height-body);
margin-bottom: var(--space-2);
}
li::marker {
color: var(--color-accent-cardinal);
}
/* Nested lists */
li > ul,
li > ol {
margin-top: var(--space-2);
margin-bottom: var(--space-2);
}
/* ========================================
BLOCKQUOTES - Editorial citations
======================================== */
blockquote {
position: relative;
margin: var(--space-8) 0;
padding: var(--space-6) var(--space-8);
background: linear-gradient(
135deg,
var(--color-accent-gold-subtle) 0%,
transparent 50%
);
border-left: var(--border-width-heavy) solid var(--color-accent-gold);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
font-style: italic;
}
blockquote::before {
content: '"';
position: absolute;
top: var(--space-2);
left: var(--space-3);
font-size: var(--font-size-4xl);
font-family: var(--font-serif);
color: var(--color-accent-gold);
opacity: 0.4;
line-height: 1;
}
blockquote p {
font-size: var(--font-size-md);
color: var(--color-text-secondary);
margin-bottom: var(--space-3);
}
blockquote p:last-of-type {
margin-bottom: 0;
}
blockquote cite,
blockquote .cite {
display: block;
margin-top: var(--space-4);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
color: var(--color-text-tertiary);
}
blockquote cite::before,
blockquote .cite::before {
content: '— ';
}
/* ========================================
CODE
======================================== */
code {
font-family: var(--font-mono);
font-size: 0.9em;
background-color: var(--color-bg-secondary);
padding: 0.15em 0.4em;
border-radius: var(--radius-sm);
color: var(--color-accent-cardinal);
}
pre {
margin: var(--space-6) 0;
padding: var(--space-6);
background-color: var(--color-accent-navy);
border-radius: var(--radius-md);
overflow-x: auto;
}
pre code {
background: none;
padding: 0;
color: var(--color-text-inverse);
font-size: var(--font-size-sm);
}
/* ========================================
HORIZONTAL RULE
======================================== */
hr {
border: none;
height: var(--border-width);
background: linear-gradient(
90deg,
transparent,
var(--color-border-strong) 20%,
var(--color-border-strong) 80%,
transparent
);
margin: var(--space-12) 0;
}
/* Decorative rule with diamond */
hr.ornament {
position: relative;
background: none;
height: var(--space-6);
}
hr.ornament::before {
content: '◆';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: var(--font-size-sm);
color: var(--color-accent-cardinal);
}
hr.ornament::after {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
var(--color-border-strong) 15%,
transparent 45%,
transparent 55%,
var(--color-border-strong) 85%,
transparent
);
}
/* ========================================
EMPHASIS & STRONG
======================================== */
strong, b {
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
em, i {
font-style: italic;
}
mark {
background-color: var(--color-accent-gold-subtle);
padding: 0.1em 0.3em;
border-radius: var(--radius-sm);
}
/* ========================================
DYSLEXIA MODE OVERRIDES
======================================== */
[data-dyslexia="true"] {
--font-body: var(--font-dyslexia);
--letter-spacing-body: var(--letter-spacing-dyslexia);
--word-spacing-body: var(--word-spacing-dyslexia);
}
[data-dyslexia="true"] h1,
[data-dyslexia="true"] h2,
[data-dyslexia="true"] h3,
[data-dyslexia="true"] h4,
[data-dyslexia="true"] h5,
[data-dyslexia="true"] h6 {
font-family: var(--font-dyslexia);
letter-spacing: var(--letter-spacing-dyslexia);
}
[data-dyslexia="true"] .drop-cap::first-letter,
[data-dyslexia="true"] .article-body > p:first-of-type::first-letter {
font-family: var(--font-dyslexia);
}
/* ========================================
LINE SPACING ADJUSTMENTS
======================================== */
[data-line-spacing="compact"] {
--line-height-body: var(--line-height-snug);
}
[data-line-spacing="normal"] {
--line-height-body: var(--line-height-normal);
}
[data-line-spacing="relaxed"] {
--line-height-body: var(--line-height-relaxed);
}
[data-line-spacing="loose"] {
--line-height-body: var(--line-height-loose);
}
/* ========================================
READING WIDTH ADJUSTMENTS
======================================== */
[data-reading-width="narrow"] {
--reading-width: var(--reading-width-narrow);
}
[data-reading-width="medium"] {
--reading-width: var(--reading-width-medium);
}
[data-reading-width="wide"] {
--reading-width: var(--reading-width-wide);
}

View File

@@ -0,0 +1,231 @@
/* ============================================
SwissFini.sh Design System
"Precision Satire" - Editorial Authority with Swiss Irony
============================================ */
:root {
/* ========================================
COLOR PALETTE - LIGHT THEME (DEFAULT)
Inspired by vintage investigative journalism
======================================== */
/* Paper & Background */
--color-bg-primary: #faf9f7;
--color-bg-secondary: #f5f3f0;
--color-bg-tertiary: #ebe8e4;
--color-bg-elevated: #ffffff;
/* Text Hierarchy */
--color-text-primary: #1a1a1a;
--color-text-secondary: #4a4a4a;
--color-text-tertiary: #6b6b6b;
--color-text-muted: #8a8a8a;
--color-text-inverse: #faf9f7;
/* Editorial Accents */
--color-accent-cardinal: #c41e3a;
--color-accent-cardinal-hover: #a31830;
--color-accent-cardinal-subtle: rgba(196, 30, 58, 0.1);
--color-accent-navy: #1e3a5f;
--color-accent-navy-hover: #152a45;
--color-accent-navy-subtle: rgba(30, 58, 95, 0.08);
--color-accent-gold: #c9a227;
--color-accent-gold-subtle: rgba(201, 162, 39, 0.15);
/* Semantic Colors */
--color-link: #1e3a5f;
--color-link-hover: #c41e3a;
--color-link-visited: #5a3d6b;
--color-success: #2d6a4f;
--color-warning: #b45309;
--color-error: #c41e3a;
/* Focus & Accessibility */
--color-focus: #0066cc;
--color-focus-ring: rgba(0, 102, 204, 0.35);
--color-focus-visible: #0052a3;
/* Borders & Dividers */
--color-border: #e5e2de;
--color-border-strong: #d0ccc6;
--color-divider: #ebe8e4;
/* ========================================
TYPOGRAPHY
======================================== */
/* Font Stacks */
--font-serif: 'Literata', 'Georgia', 'Times New Roman', Times, serif;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--font-dyslexia: 'OpenDyslexic', 'Comic Sans MS', 'Arial', sans-serif;
/* Active Font Family (swapped via JS) */
--font-body: var(--font-serif);
--font-ui: var(--font-sans);
/* Font Size Scale (multiplied by --font-size-scale) */
--font-size-scale: 1;
--font-size-2xs: calc(0.625rem * var(--font-size-scale)); /* 10px */
--font-size-xs: calc(0.75rem * var(--font-size-scale)); /* 12px */
--font-size-sm: calc(0.875rem * var(--font-size-scale)); /* 14px */
--font-size-base: calc(1rem * var(--font-size-scale)); /* 16px */
--font-size-md: calc(1.125rem * var(--font-size-scale)); /* 18px */
--font-size-lg: calc(1.25rem * var(--font-size-scale)); /* 20px */
--font-size-xl: calc(1.5rem * var(--font-size-scale)); /* 24px */
--font-size-2xl: calc(1.875rem * var(--font-size-scale)); /* 30px */
--font-size-3xl: calc(2.25rem * var(--font-size-scale)); /* 36px */
--font-size-4xl: calc(3rem * var(--font-size-scale)); /* 48px */
--font-size-5xl: calc(3.75rem * var(--font-size-scale)); /* 60px */
/* Font Weights */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-black: 900;
/* Line Heights (adjustable via accessibility) */
--line-height-tight: 1.25;
--line-height-snug: 1.375;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
--line-height-loose: 2;
--line-height-body: var(--line-height-relaxed);
--line-height-heading: var(--line-height-snug);
/* Letter Spacing */
--letter-spacing-tighter: -0.05em;
--letter-spacing-tight: -0.025em;
--letter-spacing-normal: 0;
--letter-spacing-wide: 0.025em;
--letter-spacing-wider: 0.05em;
--letter-spacing-widest: 0.1em;
/* For dyslexia mode */
--letter-spacing-dyslexia: 0.35ch;
--word-spacing-dyslexia: 1.225ch;
--letter-spacing-body: var(--letter-spacing-normal);
--word-spacing-body: normal;
/* ========================================
SPACING SCALE
======================================== */
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.25rem; /* 20px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-20: 5rem; /* 80px */
--space-24: 6rem; /* 96px */
--space-32: 8rem; /* 128px */
/* ========================================
LAYOUT
======================================== */
/* Reading Width (adjustable) */
--reading-width-narrow: 55ch;
--reading-width-medium: 70ch;
--reading-width-wide: 85ch;
--reading-width: var(--reading-width-medium);
/* Container */
--container-max: 1200px;
--container-padding: var(--space-6);
/* Article Width */
--article-width: min(var(--reading-width), 100%);
/* ========================================
BORDERS & EFFECTS
======================================== */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
--border-width: 1px;
--border-width-thick: 2px;
--border-width-heavy: 4px;
/* Shadows - Editorial depth */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
/* Elevated surface shadow */
--shadow-elevated:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 4px rgba(0, 0, 0, 0.04),
0 8px 16px rgba(0, 0, 0, 0.06);
/* ========================================
FOCUS INDICATORS (WCAG 2.2)
======================================== */
--focus-outline-width: 3px;
--focus-outline-offset: 2px;
--focus-outline-style: solid;
--focus-outline-color: var(--color-focus);
/* Enhanced focus (accessibility toggle) */
--focus-enhanced-width: 4px;
--focus-enhanced-offset: 4px;
/* ========================================
TRANSITIONS
======================================== */
--duration-instant: 0ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 350ms;
--duration-slower: 500ms;
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
--transition-colors: color var(--duration-fast) var(--ease-default),
background-color var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
--transition-transform: transform var(--duration-normal) var(--ease-out);
--transition-opacity: opacity var(--duration-normal) var(--ease-default);
--transition-shadow: box-shadow var(--duration-normal) var(--ease-default);
/* ========================================
Z-INDEX SCALE
======================================== */
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-popover: 600;
--z-tooltip: 700;
--z-accessibility-panel: 800;
--z-skip-link: 900;
--z-max: 9999;
}

View File

@@ -0,0 +1,400 @@
/* ============================================
Accessibility Panel
First-class citizen for user autonomy
============================================ */
/* ========================================
Toggle Button
======================================== */
.accessibility-toggle {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background-color: var(--color-bg-secondary);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-secondary);
cursor: pointer;
transition: var(--transition-colors), var(--transition-transform);
}
.accessibility-toggle:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
border-color: var(--color-border-strong);
}
.accessibility-toggle:focus-visible {
outline: var(--focus-outline-width) var(--focus-outline-style) var(--color-focus);
outline-offset: var(--focus-outline-offset);
}
.accessibility-toggle[aria-expanded="true"] {
background-color: var(--color-accent-navy);
color: var(--color-text-inverse);
border-color: var(--color-accent-navy);
}
.accessibility-toggle svg {
width: 20px;
height: 20px;
}
/* Pulse animation for discoverability (first visit) */
@keyframes pulse-ring {
0% {
box-shadow: 0 0 0 0 rgba(196, 30, 58, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(196, 30, 58, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(196, 30, 58, 0);
}
}
.accessibility-toggle.is-new {
animation: pulse-ring 2s ease-out infinite;
}
/* ========================================
Panel Container
======================================== */
.accessibility-panel {
position: fixed;
top: 0;
right: 0;
z-index: var(--z-accessibility-panel);
width: min(400px, 100vw);
height: 100vh;
background-color: var(--color-bg-elevated);
border-left: var(--border-width) solid var(--color-border);
box-shadow: var(--shadow-xl);
transform: translateX(100%);
transition: transform var(--duration-slow) var(--ease-out),
visibility var(--duration-slow);
visibility: hidden;
overflow-y: auto;
overscroll-behavior: contain;
}
.accessibility-panel.is-open {
transform: translateX(0);
visibility: visible;
}
/* Backdrop */
.accessibility-backdrop {
position: fixed;
inset: 0;
z-index: calc(var(--z-accessibility-panel) - 1);
background-color: rgba(0, 0, 0, 0.3);
opacity: 0;
visibility: hidden;
transition: opacity var(--duration-normal) var(--ease-default),
visibility var(--duration-normal);
}
.accessibility-backdrop.is-visible {
opacity: 1;
visibility: visible;
}
[data-theme="dark"] .accessibility-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
/* ========================================
Panel Header
======================================== */
.accessibility-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-5) var(--space-6);
border-bottom: var(--border-width) solid var(--color-border);
position: sticky;
top: 0;
background-color: var(--color-bg-elevated);
z-index: 1;
}
.accessibility-panel__header h2 {
font-family: var(--font-ui);
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin: 0;
border-bottom: none;
}
.accessibility-panel__close {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: none;
border: none;
border-radius: var(--radius-sm);
color: var(--color-text-tertiary);
cursor: pointer;
transition: var(--transition-colors);
}
.accessibility-panel__close:hover {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.accessibility-panel__close:focus-visible {
outline: var(--focus-outline-width) var(--focus-outline-style) var(--color-focus);
outline-offset: 2px;
}
.accessibility-panel__close svg {
width: 20px;
height: 20px;
}
/* ========================================
Panel Content
======================================== */
.accessibility-panel__content {
padding: var(--space-6);
}
/* ========================================
Control Groups
======================================== */
.accessibility-control {
margin-bottom: var(--space-6);
padding: 0;
border: none;
}
.accessibility-control legend {
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: var(--letter-spacing-wide);
margin-bottom: var(--space-3);
padding: 0;
}
.accessibility-control__buttons {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.accessibility-control__value {
min-width: 50px;
text-align: center;
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
padding: var(--space-2) var(--space-3);
background-color: var(--color-bg-tertiary);
border-radius: var(--radius-sm);
}
/* ========================================
Button Styles
======================================== */
.btn {
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
border: var(--border-width) solid var(--color-border);
background-color: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: pointer;
transition: var(--transition-colors);
}
.btn:hover {
background-color: var(--color-bg-tertiary);
border-color: var(--color-border-strong);
color: var(--color-text-primary);
}
.btn:focus-visible {
outline: var(--focus-outline-width) var(--focus-outline-style) var(--color-focus);
outline-offset: 2px;
}
/* Icon button (font size controls) */
.btn--icon {
width: 44px;
height: 44px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: var(--font-weight-bold);
}
.btn--icon:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Option buttons (segmented control) */
.btn--option {
flex: 1;
min-width: 0;
text-align: center;
padding: var(--space-3) var(--space-2);
}
.btn--option[aria-pressed="true"] {
background-color: var(--color-accent-navy);
border-color: var(--color-accent-navy);
color: var(--color-text-inverse);
}
/* Toggle button (switch style) */
.btn--toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: var(--space-3) var(--space-4);
text-align: left;
}
.btn--toggle .btn__label {
flex: 1;
}
.btn--toggle .btn__state {
position: relative;
width: 44px;
height: 24px;
background-color: var(--color-bg-tertiary);
border-radius: var(--radius-full);
transition: background-color var(--duration-fast) var(--ease-default);
}
.btn--toggle .btn__state::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background-color: var(--color-bg-elevated);
border-radius: var(--radius-full);
box-shadow: var(--shadow-sm);
transition: transform var(--duration-fast) var(--ease-default);
}
.btn--toggle[aria-pressed="true"] .btn__state {
background-color: var(--color-accent-navy);
}
.btn--toggle[aria-pressed="true"] .btn__state::after {
transform: translateX(20px);
}
/* Secondary button (reset) */
.btn--secondary {
background-color: transparent;
border-color: var(--color-accent-cardinal);
color: var(--color-accent-cardinal);
}
.btn--secondary:hover {
background-color: var(--color-accent-cardinal);
color: var(--color-text-inverse);
}
/* ========================================
Footer
======================================== */
.accessibility-control--footer {
margin-top: var(--space-8);
padding-top: var(--space-6);
border-top: var(--border-width) solid var(--color-border);
}
.accessibility-control--footer .btn {
width: 100%;
}
/* ========================================
Live Region (Screen Reader Announcements)
======================================== */
.accessibility-announcer {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ========================================
Reduced Motion Styles
======================================== */
@media (prefers-reduced-motion: reduce) {
.accessibility-panel {
transition: none;
}
.accessibility-backdrop {
transition: none;
}
.accessibility-toggle.is-new {
animation: none;
}
}
[data-reduced-motion="true"] .accessibility-panel {
transition: none;
}
[data-reduced-motion="true"] .accessibility-backdrop {
transition: none;
}
[data-reduced-motion="true"] .accessibility-toggle.is-new {
animation: none;
}
/* ========================================
Mobile Adjustments
======================================== */
@media (max-width: 480px) {
.accessibility-panel {
width: 100vw;
}
.btn--option {
font-size: var(--font-size-xs);
padding: var(--space-2) var(--space-1);
}
}

View File

@@ -0,0 +1,413 @@
/* ============================================
Article Styles
Long-form investigative journalism layout
============================================ */
.article {
max-width: var(--article-width);
margin: 0 auto;
padding: var(--space-8) var(--container-padding) var(--space-16);
}
/* ========================================
Article Header
======================================== */
.article-header {
margin-bottom: var(--space-10);
padding-bottom: var(--space-8);
border-bottom: var(--border-width) solid var(--color-border);
}
.article-category {
display: inline-block;
font-family: var(--font-ui);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: var(--letter-spacing-widest);
color: var(--color-accent-cardinal);
margin-bottom: var(--space-4);
}
.article-title {
font-size: clamp(var(--font-size-2xl), 5vw, var(--font-size-4xl));
line-height: 1.1;
margin-bottom: var(--space-4);
letter-spacing: var(--letter-spacing-tighter);
}
.article-subtitle {
font-size: var(--font-size-lg);
font-style: italic;
color: var(--color-text-secondary);
line-height: var(--line-height-relaxed);
margin-bottom: var(--space-6);
}
.article-meta {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
font-family: var(--font-ui);
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
}
.article-meta-item {
display: flex;
align-items: center;
gap: var(--space-2);
}
.article-meta-icon {
width: 16px;
height: 16px;
opacity: 0.6;
}
/* ========================================
Article Body
======================================== */
.article-body {
font-size: var(--font-size-md);
line-height: var(--line-height-body);
}
.article-body > * {
max-width: var(--reading-width);
}
.article-body > h2 {
margin-top: var(--space-16);
}
.article-body > h3 {
margin-top: var(--space-10);
}
/* ========================================
Tables - Comparison / Data Tables
======================================== */
.article-body table,
.comparison-table {
width: 100%;
max-width: 100%;
border-collapse: collapse;
margin: var(--space-8) 0;
font-family: var(--font-ui);
font-size: var(--font-size-sm);
background-color: var(--color-bg-elevated);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.article-body th,
.comparison-table th {
background-color: var(--color-accent-navy);
color: var(--color-text-inverse);
font-weight: var(--font-weight-semibold);
text-align: left;
padding: var(--space-4) var(--space-5);
border-bottom: var(--border-width-thick) solid var(--color-accent-navy-hover);
}
.article-body th:first-child,
.comparison-table th:first-child {
border-radius: var(--radius-md) 0 0 0;
}
.article-body th:last-child,
.comparison-table th:last-child {
border-radius: 0 var(--radius-md) 0 0;
}
.article-body td,
.comparison-table td {
padding: var(--space-4) var(--space-5);
border-bottom: var(--border-width) solid var(--color-border);
vertical-align: top;
}
.article-body tr:last-child td,
.comparison-table tr:last-child td {
border-bottom: none;
}
.article-body tr:hover,
.comparison-table tr:hover {
background-color: var(--color-bg-secondary);
}
/* Metric column (first column) */
.metric-col {
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
width: 30%;
}
/* SCION column - Cardinal red */
.scion-col {
color: var(--color-accent-cardinal);
}
/* SD-WAN column - Success green */
.sdwan-col {
color: var(--color-success);
}
/* ========================================
Irony Box - Official Warning (But Absurd)
======================================== */
.irony-box {
position: relative;
margin: var(--space-8) 0;
padding: var(--space-6) var(--space-6) var(--space-6) var(--space-8);
background-color: rgba(196, 30, 58, 0.05);
border-left: var(--border-width-heavy) solid var(--color-accent-cardinal);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.irony-box::before {
content: '⚠';
position: absolute;
top: var(--space-4);
left: calc(var(--space-8) * -1 - 1rem);
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-accent-cardinal);
color: var(--color-text-inverse);
font-size: var(--font-size-sm);
border-radius: var(--radius-full);
}
.irony-box strong {
color: var(--color-accent-cardinal);
font-weight: var(--font-weight-bold);
}
.irony-box p {
margin: 0;
color: var(--color-text-secondary);
}
/* Dark mode adjustment */
[data-theme="dark"] .irony-box {
background-color: rgba(232, 74, 100, 0.08);
}
[data-theme="high-contrast"] .irony-box {
background-color: rgba(255, 77, 109, 0.15);
border-left-width: 5px;
}
/* ========================================
Conclusion Box
======================================== */
.conclusion {
margin: var(--space-10) 0;
padding: var(--space-8);
background-color: var(--color-accent-navy);
color: var(--color-text-inverse);
border-radius: var(--radius-md);
}
.conclusion h3 {
color: var(--color-accent-gold);
margin-top: 0;
margin-bottom: var(--space-4);
border-bottom: none;
}
.conclusion p {
color: rgba(255, 255, 255, 0.9);
margin-bottom: var(--space-4);
}
.conclusion ul {
margin: var(--space-4) 0;
padding-left: var(--space-6);
}
.conclusion li {
color: rgba(255, 255, 255, 0.9);
margin-bottom: var(--space-2);
}
.conclusion li::marker {
color: var(--color-accent-gold);
}
/* ========================================
Sources Section
======================================== */
.sources {
margin-top: var(--space-12);
padding: var(--space-6);
background-color: var(--color-bg-secondary);
border-radius: var(--radius-md);
font-family: var(--font-ui);
font-size: var(--font-size-sm);
}
.sources h3 {
font-size: var(--font-size-base);
color: var(--color-text-secondary);
margin-top: 0;
margin-bottom: var(--space-4);
border-bottom: none;
}
.sources ul {
margin: 0;
padding-left: var(--space-5);
}
.sources li {
margin-bottom: var(--space-2);
font-size: var(--font-size-sm);
line-height: var(--line-height-normal);
}
.sources a {
color: var(--color-link);
word-break: break-word;
}
/* ========================================
Fade Overlay (Paywall Teaser)
======================================== */
.fade-overlay {
position: relative;
margin-top: calc(var(--space-16) * -1);
padding-top: var(--space-16);
background: linear-gradient(
to bottom,
transparent 0%,
var(--color-bg-primary) 40%,
var(--color-bg-primary) 100%
);
}
.paywall-box {
background: linear-gradient(
135deg,
var(--color-accent-navy) 0%,
#2d3a5f 100%
);
color: var(--color-text-inverse);
padding: var(--space-10);
border-radius: var(--radius-lg);
text-align: center;
box-shadow: var(--shadow-xl);
}
.paywall-box h2 {
font-family: var(--font-ui);
font-size: var(--font-size-xl);
color: white;
margin: 0 0 var(--space-4);
border-bottom: none;
}
.paywall-box > p {
font-family: var(--font-ui);
font-size: var(--font-size-base);
margin-bottom: var(--space-6);
opacity: 0.9;
max-width: none;
}
.teaser-list {
text-align: left;
max-width: 400px;
margin: var(--space-6) auto;
font-family: var(--font-ui);
font-size: var(--font-size-sm);
padding-left: var(--space-6);
}
.teaser-list li {
margin-bottom: var(--space-3);
opacity: 0.9;
color: white;
}
.teaser-list li::marker {
color: var(--color-accent-gold);
}
/* CTA Button */
.cta-button {
display: inline-block;
background-color: var(--color-accent-cardinal);
color: white;
text-decoration: none;
padding: var(--space-4) var(--space-8);
border-radius: var(--radius-sm);
font-family: var(--font-ui);
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
transition: background-color var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-out);
}
.cta-button:hover {
background-color: var(--color-accent-cardinal-hover);
color: white;
transform: translateY(-2px);
}
.cta-button:focus-visible {
outline: 3px solid var(--color-accent-gold);
outline-offset: 3px;
}
.free-label {
display: block;
margin-top: var(--space-4);
font-size: var(--font-size-sm);
opacity: 0.7;
font-family: var(--font-ui);
}
/* ========================================
Responsive
======================================== */
@media (max-width: 768px) {
.article {
padding: var(--space-6) var(--space-4) var(--space-12);
}
.article-title {
font-size: var(--font-size-2xl);
}
.article-body table {
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.irony-box {
padding-left: var(--space-6);
}
.irony-box::before {
position: static;
display: inline-flex;
margin-right: var(--space-2);
margin-bottom: var(--space-2);
}
}

View File

@@ -0,0 +1,153 @@
/* ============================================
Site Footer
Clean, informative, accessible
============================================ */
.site-footer {
background-color: var(--color-accent-navy);
color: var(--color-text-inverse);
margin-top: auto;
}
.footer-container {
max-width: var(--container-max);
margin: 0 auto;
padding: var(--space-12) var(--container-padding) var(--space-8);
}
/* ========================================
Footer Top
======================================== */
.footer-top {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: var(--space-10);
padding-bottom: var(--space-10);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-brand {
max-width: 320px;
}
.footer-logo {
display: flex;
align-items: center;
gap: var(--space-3);
text-decoration: none;
color: var(--color-text-inverse);
margin-bottom: var(--space-4);
}
.footer-logo .logo-mark {
background-color: var(--color-accent-cardinal);
}
.footer-logo .site-title {
font-size: var(--font-size-lg);
}
.footer-description {
font-family: var(--font-ui);
font-size: var(--font-size-sm);
color: rgba(255, 255, 255, 0.7);
line-height: var(--line-height-relaxed);
margin: 0;
}
/* Footer Sections */
.footer-section h3 {
font-family: var(--font-ui);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: var(--letter-spacing-widest);
color: var(--color-accent-gold);
margin-bottom: var(--space-4);
border-bottom: none;
}
.footer-links {
list-style: none;
margin: 0;
padding: 0;
}
.footer-links li {
margin-bottom: var(--space-2);
}
.footer-links a {
font-family: var(--font-ui);
font-size: var(--font-size-sm);
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
transition: var(--transition-colors);
}
.footer-links a:hover {
color: var(--color-text-inverse);
}
.footer-links a:focus-visible {
outline: 2px solid var(--color-accent-gold);
outline-offset: 2px;
}
/* ========================================
Footer Bottom
======================================== */
.footer-bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: var(--space-6);
font-family: var(--font-ui);
font-size: var(--font-size-xs);
color: rgba(255, 255, 255, 0.5);
}
.footer-copyright {
margin: 0;
}
.footer-legal {
display: flex;
gap: var(--space-4);
list-style: none;
margin: 0;
padding: 0;
}
.footer-legal a {
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
transition: var(--transition-colors);
}
.footer-legal a:hover {
color: rgba(255, 255, 255, 0.8);
}
/* ========================================
Responsive
======================================== */
@media (max-width: 768px) {
.footer-top {
grid-template-columns: 1fr;
gap: var(--space-8);
}
.footer-brand {
max-width: none;
}
.footer-bottom {
flex-direction: column;
gap: var(--space-4);
text-align: center;
}
}

View File

@@ -0,0 +1,258 @@
/* ============================================
Site Header
Authoritative yet approachable editorial header
============================================ */
.site-header {
position: sticky;
top: 0;
z-index: var(--z-sticky);
background-color: var(--color-bg-primary);
border-bottom: var(--border-width) solid var(--color-border);
transition: var(--transition-shadow), var(--transition-colors);
}
.site-header.scrolled {
box-shadow: var(--shadow-md);
}
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
max-width: var(--container-max);
margin: 0 auto;
padding: var(--space-4) var(--container-padding);
}
/* ========================================
Logo / Site Title
======================================== */
.site-logo {
display: flex;
align-items: center;
gap: var(--space-3);
text-decoration: none;
color: var(--color-text-primary);
}
.site-logo:hover {
color: var(--color-accent-cardinal);
}
.logo-mark {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-accent-cardinal);
color: var(--color-text-inverse);
font-family: var(--font-ui);
font-weight: var(--font-weight-black);
font-size: var(--font-size-lg);
border-radius: var(--radius-sm);
transition: var(--transition-transform);
}
.site-logo:hover .logo-mark {
transform: rotate(-3deg) scale(1.05);
}
.site-title {
font-family: var(--font-body);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
letter-spacing: var(--letter-spacing-tight);
line-height: 1;
}
.site-title .swiss {
color: var(--color-accent-cardinal);
}
.site-tagline {
font-family: var(--font-ui);
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
text-transform: uppercase;
letter-spacing: var(--letter-spacing-widest);
margin-top: var(--space-1);
}
/* ========================================
Navigation
======================================== */
.main-nav {
display: flex;
align-items: center;
gap: var(--space-8);
}
.nav-list {
display: flex;
gap: var(--space-6);
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
margin: 0;
}
.nav-link {
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
text-decoration: none;
padding: var(--space-2) var(--space-1);
position: relative;
transition: var(--transition-colors);
}
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: var(--color-accent-cardinal);
transform: scaleX(0);
transform-origin: right;
transition: transform var(--duration-normal) var(--ease-out);
}
.nav-link:hover {
color: var(--color-text-primary);
}
.nav-link:hover::after,
.nav-link.active::after {
transform: scaleX(1);
transform-origin: left;
}
.nav-link.active {
color: var(--color-accent-cardinal);
}
/* ========================================
Header Actions (Accessibility Toggle)
======================================== */
.header-actions {
display: flex;
align-items: center;
gap: var(--space-3);
}
/* ========================================
Mobile Navigation
======================================== */
.nav-toggle {
display: none;
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--color-text-primary);
}
.nav-toggle-icon {
width: 24px;
height: 2px;
background-color: currentColor;
position: relative;
transition: var(--transition-colors);
}
.nav-toggle-icon::before,
.nav-toggle-icon::after {
content: '';
position: absolute;
left: 0;
width: 100%;
height: 2px;
background-color: currentColor;
transition: transform var(--duration-normal) var(--ease-default);
}
.nav-toggle-icon::before {
top: -8px;
}
.nav-toggle-icon::after {
top: 8px;
}
/* Open state */
.nav-toggle[aria-expanded="true"] .nav-toggle-icon {
background-color: transparent;
}
.nav-toggle[aria-expanded="true"] .nav-toggle-icon::before {
transform: rotate(45deg) translate(5px, 6px);
}
.nav-toggle[aria-expanded="true"] .nav-toggle-icon::after {
transform: rotate(-45deg) translate(5px, -6px);
}
/* ========================================
Responsive
======================================== */
@media (max-width: 768px) {
.nav-toggle {
display: flex;
}
.main-nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
background-color: var(--color-bg-primary);
border-bottom: var(--border-width) solid var(--color-border);
padding: var(--space-4) var(--container-padding);
transform: translateY(-100%);
opacity: 0;
visibility: hidden;
transition: transform var(--duration-normal) var(--ease-out),
opacity var(--duration-normal) var(--ease-out),
visibility var(--duration-normal);
box-shadow: var(--shadow-lg);
}
.main-nav.is-open {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.nav-list {
flex-direction: column;
gap: var(--space-2);
width: 100%;
}
.nav-link {
display: block;
padding: var(--space-3) var(--space-4);
font-size: var(--font-size-base);
}
.site-tagline {
display: none;
}
}

View File

@@ -0,0 +1,191 @@
/* ============================================
SwissFini.sh Main Stylesheet
"Precision Satire" Design System
============================================ */
/*
Build Order:
1. Variables (design tokens)
2. Reset (normalize defaults)
3. Typography (base text styles)
4. Themes (color variants)
5. Components (UI elements)
6. Utilities (helper classes)
*/
/* ========================================
Base Layer
======================================== */
@import "base/_variables.css";
@import "base/_reset.css";
@import "base/_typography.css";
/* ========================================
Theme Variants
======================================== */
@import "themes/_dark.css";
@import "themes/_high-contrast.css";
/* ========================================
Components
======================================== */
@import "components/_header.css";
@import "components/_footer.css";
@import "components/_article.css";
@import "components/_accessibility-panel.css";
/* ========================================
Utilities
======================================== */
@import "utilities/_focus.css";
@import "utilities/_print.css";
/* ========================================
Layout Helpers
======================================== */
.container {
width: 100%;
max-width: var(--container-max);
margin-left: auto;
margin-right: auto;
padding-left: var(--container-padding);
padding-right: var(--container-padding);
}
.main-content {
flex: 1;
width: 100%;
}
/* Page wrapper for sticky footer */
.page-wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ========================================
Skip Links (Accessibility)
======================================== */
.skip-links {
position: absolute;
top: 0;
left: 0;
z-index: var(--z-skip-link);
}
.skip-link {
position: absolute;
top: -100%;
left: var(--space-4);
padding: var(--space-3) var(--space-6);
background-color: var(--color-accent-navy);
color: var(--color-text-inverse);
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
text-decoration: none;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
transition: top var(--duration-fast) var(--ease-out);
}
.skip-link:focus {
top: var(--space-4);
outline: 3px solid var(--color-accent-gold);
outline-offset: 2px;
}
/* ========================================
Font Size Scale (Accessibility Control)
======================================== */
[data-font-size="1"] { --font-size-scale: 0.75; }
[data-font-size="2"] { --font-size-scale: 0.875; }
[data-font-size="3"] { --font-size-scale: 1; }
[data-font-size="4"] { --font-size-scale: 1.25; }
[data-font-size="5"] { --font-size-scale: 1.5; }
/* ========================================
Motion Preferences
======================================== */
/* Respect system preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* User toggle override */
[data-reduced-motion="true"] *,
[data-reduced-motion="true"] *::before,
[data-reduced-motion="true"] *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* ========================================
Utility Classes
======================================== */
/* Text alignment */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
/* Display */
.hidden { display: none !important; }
.block { display: block; }
.inline-block { display: inline-block; }
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.grid { display: grid; }
/* Flexbox */
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.flex-wrap { flex-wrap: wrap; }
.gap-2 { gap: var(--space-2); }
.gap-4 { gap: var(--space-4); }
.gap-6 { gap: var(--space-6); }
/* Margin */
.mt-4 { margin-top: var(--space-4); }
.mt-8 { margin-top: var(--space-8); }
.mb-4 { margin-bottom: var(--space-4); }
.mb-8 { margin-bottom: var(--space-8); }
.mx-auto { margin-left: auto; margin-right: auto; }
/* Padding */
.p-4 { padding: var(--space-4); }
.p-6 { padding: var(--space-6); }
.p-8 { padding: var(--space-8); }
.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); }
.py-8 { padding-top: var(--space-8); padding-bottom: var(--space-8); }
.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); }
.px-6 { padding-left: var(--space-6); padding-right: var(--space-6); }
/* Width */
.w-full { width: 100%; }
.max-w-prose { max-width: var(--reading-width); }
/* Colors */
.text-cardinal { color: var(--color-accent-cardinal); }
.text-navy { color: var(--color-accent-navy); }
.text-muted { color: var(--color-text-muted); }
.bg-elevated { background-color: var(--color-bg-elevated); }

View File

@@ -0,0 +1,116 @@
/* ============================================
Dark Theme
"Classified Documents" - Reading in the shadows
============================================ */
[data-theme="dark"] {
/* Paper & Background - Deep charcoal */
--color-bg-primary: #0d0d0f;
--color-bg-secondary: #151518;
--color-bg-tertiary: #1c1c20;
--color-bg-elevated: #222226;
/* Text - Soft whites for reduced eye strain */
--color-text-primary: #e8e6e3;
--color-text-secondary: #a8a5a0;
--color-text-tertiary: #7a7772;
--color-text-muted: #5a5855;
--color-text-inverse: #0d0d0f;
/* Accents - Brightened for dark mode */
--color-accent-cardinal: #e84a64;
--color-accent-cardinal-hover: #ff5d78;
--color-accent-cardinal-subtle: rgba(232, 74, 100, 0.15);
--color-accent-navy: #5a9fd4;
--color-accent-navy-hover: #7ab4e0;
--color-accent-navy-subtle: rgba(90, 159, 212, 0.12);
--color-accent-gold: #e6c84a;
--color-accent-gold-subtle: rgba(230, 200, 74, 0.12);
/* Links */
--color-link: #5a9fd4;
--color-link-hover: #e84a64;
--color-link-visited: #b088c0;
/* Semantic */
--color-success: #4ade80;
--color-warning: #fbbf24;
--color-error: #e84a64;
/* Focus */
--color-focus: #60a5fa;
--color-focus-ring: rgba(96, 165, 250, 0.3);
--color-focus-visible: #93c5fd;
/* Borders */
--color-border: #2a2a2f;
--color-border-strong: #3a3a40;
--color-divider: #252528;
/* Shadows - Deeper for dark mode */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.35), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
--shadow-elevated:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.3),
0 8px 16px rgba(0, 0, 0, 0.35);
}
/* Dark mode specific adjustments */
[data-theme="dark"] code {
background-color: var(--color-bg-tertiary);
}
[data-theme="dark"] pre {
background-color: #0a0a0c;
border: 1px solid var(--color-border);
}
[data-theme="dark"] pre code {
color: #e8e6e3;
}
[data-theme="dark"] blockquote {
background: linear-gradient(
135deg,
rgba(230, 200, 74, 0.08) 0%,
transparent 50%
);
}
[data-theme="dark"] hr.ornament::after {
background: linear-gradient(
90deg,
transparent,
var(--color-border) 15%,
transparent 45%,
transparent 55%,
var(--color-border) 85%,
transparent
);
}
[data-theme="dark"] ::selection {
background-color: rgba(232, 74, 100, 0.3);
}
/* Subtle vignette effect for reading focus */
[data-theme="dark"] body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
background: radial-gradient(
ellipse at center,
transparent 0%,
transparent 60%,
rgba(0, 0, 0, 0.15) 100%
);
z-index: -1;
}

View File

@@ -0,0 +1,141 @@
/* ============================================
High Contrast Theme
Maximum accessibility - WCAG AAA compliant
============================================ */
[data-theme="high-contrast"] {
/* Pure black & white base */
--color-bg-primary: #000000;
--color-bg-secondary: #0a0a0a;
--color-bg-tertiary: #141414;
--color-bg-elevated: #1a1a1a;
/* Pure white text */
--color-text-primary: #ffffff;
--color-text-secondary: #ffffff;
--color-text-tertiary: #e0e0e0;
--color-text-muted: #c0c0c0;
--color-text-inverse: #000000;
/* High visibility accents */
--color-accent-cardinal: #ff4d6d;
--color-accent-cardinal-hover: #ff7a93;
--color-accent-cardinal-subtle: rgba(255, 77, 109, 0.2);
--color-accent-navy: #00e5ff;
--color-accent-navy-hover: #4df0ff;
--color-accent-navy-subtle: rgba(0, 229, 255, 0.15);
--color-accent-gold: #ffff00;
--color-accent-gold-subtle: rgba(255, 255, 0, 0.15);
/* Links - Bright cyan for visibility */
--color-link: #00e5ff;
--color-link-hover: #ffff00;
--color-link-visited: #ff80ff;
/* Semantic - High visibility */
--color-success: #00ff7f;
--color-warning: #ffff00;
--color-error: #ff4d6d;
/* Focus - Bright yellow for maximum visibility */
--color-focus: #ffff00;
--color-focus-ring: rgba(255, 255, 0, 0.4);
--color-focus-visible: #ffff00;
/* Borders - High contrast */
--color-border: #ffffff;
--color-border-strong: #ffffff;
--color-divider: #444444;
/* Stronger borders */
--border-width: 2px;
--border-width-thick: 3px;
--border-width-heavy: 5px;
/* Focus indicators - Extra visible */
--focus-outline-width: 4px;
--focus-outline-offset: 4px;
}
/* High contrast specific overrides */
[data-theme="high-contrast"] a {
text-decoration-thickness: 2px;
}
[data-theme="high-contrast"] a:hover {
text-decoration-thickness: 3px;
}
[data-theme="high-contrast"] h2 {
border-bottom-color: var(--color-accent-cardinal);
border-bottom-width: 3px;
}
[data-theme="high-contrast"] blockquote {
background: rgba(255, 255, 0, 0.1);
border-left-color: var(--color-accent-gold);
border-left-width: 5px;
}
[data-theme="high-contrast"] blockquote::before {
color: var(--color-accent-gold);
opacity: 0.8;
}
[data-theme="high-contrast"] code {
background-color: #1a1a1a;
border: 1px solid var(--color-border);
}
[data-theme="high-contrast"] pre {
background-color: #0a0a0a;
border: 2px solid var(--color-border);
}
[data-theme="high-contrast"] hr {
background: var(--color-border);
height: 2px;
}
[data-theme="high-contrast"] ::selection {
background-color: var(--color-accent-gold);
color: #000000;
}
/* All interactive elements get high contrast treatment */
[data-theme="high-contrast"] button,
[data-theme="high-contrast"] [role="button"],
[data-theme="high-contrast"] input,
[data-theme="high-contrast"] select,
[data-theme="high-contrast"] textarea {
border: 2px solid var(--color-border);
}
[data-theme="high-contrast"] button:hover,
[data-theme="high-contrast"] [role="button"]:hover {
background-color: var(--color-accent-gold);
color: #000000;
}
/* Table enhancements */
[data-theme="high-contrast"] th {
background-color: var(--color-accent-navy);
color: #000000;
}
[data-theme="high-contrast"] td {
border: 1px solid var(--color-border);
}
/* Drop cap high contrast */
[data-theme="high-contrast"] .drop-cap::first-letter,
[data-theme="high-contrast"] .article-body > p:first-of-type::first-letter {
color: var(--color-accent-cardinal);
}
/* Remove subtle gradients that might reduce contrast */
[data-theme="high-contrast"] blockquote {
background: rgba(255, 255, 0, 0.1);
}

View File

@@ -0,0 +1,123 @@
/* ============================================
Focus Indicators - WCAG 2.2 Compliant
Visible, consistent, customizable
============================================ */
/* ========================================
Default Focus Styles
======================================== */
/* Remove default outline, we'll add our own */
:focus {
outline: none;
}
/* Focus visible for keyboard navigation */
:focus-visible {
outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color);
outline-offset: var(--focus-outline-offset);
}
/* Skip focus ring for mouse clicks */
:focus:not(:focus-visible) {
outline: none;
}
/* ========================================
Enhanced Focus Mode
======================================== */
[data-enhanced-focus="true"] :focus-visible {
outline-width: var(--focus-enhanced-width);
outline-offset: var(--focus-enhanced-offset);
box-shadow: 0 0 0 calc(var(--focus-enhanced-width) + 3px) var(--color-focus-ring);
}
/* Extra visible for interactive elements */
[data-enhanced-focus="true"] button:focus-visible,
[data-enhanced-focus="true"] a:focus-visible,
[data-enhanced-focus="true"] input:focus-visible,
[data-enhanced-focus="true"] select:focus-visible,
[data-enhanced-focus="true"] textarea:focus-visible,
[data-enhanced-focus="true"] [role="button"]:focus-visible,
[data-enhanced-focus="true"] [tabindex]:focus-visible {
position: relative;
z-index: 1;
}
/* ========================================
Component-Specific Focus Styles
======================================== */
/* Buttons */
button:focus-visible,
.btn:focus-visible,
[role="button"]:focus-visible {
outline-offset: 3px;
}
/* Links */
a:focus-visible {
border-radius: var(--radius-sm);
}
/* Form inputs */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline-offset: 0;
box-shadow: 0 0 0 3px var(--color-focus-ring);
}
/* Cards and containers */
article:focus-visible,
[role="article"]:focus-visible,
.card:focus-visible {
outline-offset: 4px;
}
/* ========================================
Skip Link Focus
======================================== */
.skip-link:focus {
position: fixed;
top: var(--space-4);
left: var(--space-4);
z-index: var(--z-skip-link);
padding: var(--space-3) var(--space-6);
background-color: var(--color-accent-navy);
color: var(--color-text-inverse);
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
text-decoration: none;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
outline: 3px solid var(--color-accent-gold);
outline-offset: 2px;
}
/* ========================================
Focus Within (for composite widgets)
======================================== */
[data-enhanced-focus="true"] fieldset:focus-within {
outline: 2px dashed var(--color-focus);
outline-offset: 4px;
border-radius: var(--radius-md);
}
/* ========================================
High Contrast Mode Focus
======================================== */
[data-theme="high-contrast"] :focus-visible {
outline-color: var(--color-focus);
outline-width: 4px;
}
[data-theme="high-contrast"][data-enhanced-focus="true"] :focus-visible {
outline-width: 5px;
box-shadow: 0 0 0 8px var(--color-focus-ring);
}

View File

@@ -0,0 +1,380 @@
/* ============================================
Print Styles
Clean, readable printed output
============================================ */
@media print {
/* ========================================
Reset for Print
======================================== */
*,
*::before,
*::after {
background: transparent !important;
color: #000 !important;
box-shadow: none !important;
text-shadow: none !important;
}
/* ========================================
Page Setup
======================================== */
@page {
margin: 2cm;
size: A4;
}
html {
font-size: 12pt;
}
body {
font-family: Georgia, 'Times New Roman', Times, serif;
line-height: 1.5;
max-width: none;
padding: 0;
margin: 0;
}
/* ========================================
Typography for Print
======================================== */
h1 {
font-size: 24pt;
margin-bottom: 12pt;
page-break-after: avoid;
}
h2 {
font-size: 18pt;
margin-top: 24pt;
margin-bottom: 12pt;
border-bottom: 1pt solid #000;
page-break-after: avoid;
}
h3 {
font-size: 14pt;
margin-top: 18pt;
margin-bottom: 8pt;
page-break-after: avoid;
}
h4, h5, h6 {
font-size: 12pt;
page-break-after: avoid;
}
p {
font-size: 11pt;
line-height: 1.6;
margin-bottom: 10pt;
orphans: 3;
widows: 3;
}
/* Drop cap for first paragraph */
.article-body > p:first-of-type::first-letter {
font-size: 36pt;
float: left;
line-height: 1;
margin-right: 6pt;
margin-top: 2pt;
}
/* ========================================
Links
======================================== */
a {
text-decoration: underline;
}
/* Show URLs after external links */
a[href^="http"]::after,
a[href^="https"]::after {
content: " (" attr(href) ")";
font-size: 9pt;
font-style: italic;
word-break: break-all;
}
/* Don't show URL for internal links */
a[href^="/"]::after,
a[href^="#"]::after {
content: none;
}
/* ========================================
Hide Non-Essential Elements
======================================== */
.site-header,
.site-footer,
.main-nav,
.accessibility-toggle,
.accessibility-panel,
.accessibility-backdrop,
.skip-links,
.nav-toggle,
.cta-button,
.paywall-box,
.fade-overlay,
.header-actions,
nav,
aside,
[role="complementary"],
[aria-hidden="true"] {
display: none !important;
}
/* ========================================
Article Styling
======================================== */
.article {
max-width: none;
padding: 0;
}
.article-header {
border-bottom: 1pt solid #000;
padding-bottom: 12pt;
margin-bottom: 18pt;
}
.article-category {
font-size: 10pt;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1pt;
}
.article-title {
font-size: 28pt;
line-height: 1.1;
}
.article-subtitle {
font-size: 14pt;
font-style: italic;
}
.article-meta {
font-size: 10pt;
border-top: 0.5pt solid #666;
padding-top: 8pt;
margin-top: 12pt;
}
/* ========================================
Tables
======================================== */
table {
border-collapse: collapse;
width: 100%;
margin: 12pt 0;
font-size: 10pt;
page-break-inside: avoid;
}
th {
background-color: #ddd !important;
font-weight: bold;
text-align: left;
padding: 6pt 8pt;
border: 0.5pt solid #000;
}
td {
padding: 6pt 8pt;
border: 0.5pt solid #000;
vertical-align: top;
}
tr:nth-child(even) {
background-color: #f5f5f5 !important;
}
/* ========================================
Blockquotes
======================================== */
blockquote {
margin: 12pt 0;
padding: 8pt 12pt;
border-left: 3pt solid #000;
font-style: italic;
}
blockquote::before {
content: none;
}
blockquote cite {
display: block;
margin-top: 6pt;
font-size: 10pt;
font-style: normal;
}
/* ========================================
Irony Box
======================================== */
.irony-box {
margin: 12pt 0;
padding: 10pt;
border: 2pt solid #000;
background-color: #f9f9f9 !important;
}
.irony-box::before {
content: "Note: ";
font-weight: bold;
}
/* ========================================
Conclusion
======================================== */
.conclusion {
margin: 18pt 0;
padding: 12pt;
border: 1pt solid #000;
background-color: #eee !important;
}
.conclusion h3 {
border-bottom: none;
}
/* ========================================
Sources
======================================== */
.sources {
margin-top: 24pt;
padding-top: 12pt;
border-top: 1pt solid #000;
}
.sources h3 {
font-size: 12pt;
margin-bottom: 8pt;
border-bottom: none;
}
.sources ul {
font-size: 9pt;
line-height: 1.4;
}
.sources a::after {
content: " [" attr(href) "]";
font-size: 8pt;
}
/* ========================================
Code
======================================== */
code {
font-family: 'Courier New', Courier, monospace;
font-size: 10pt;
border: 0.5pt solid #ccc;
padding: 1pt 3pt;
}
pre {
font-family: 'Courier New', Courier, monospace;
font-size: 9pt;
border: 0.5pt solid #000;
padding: 8pt;
white-space: pre-wrap;
word-wrap: break-word;
page-break-inside: avoid;
}
pre code {
border: none;
padding: 0;
}
/* ========================================
Page Breaks
======================================== */
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
}
p, blockquote, table, ul, ol {
page-break-inside: avoid;
}
img, figure {
page-break-inside: avoid;
max-width: 100% !important;
}
/* ========================================
Images
======================================== */
img {
max-width: 100%;
height: auto;
}
figure {
margin: 12pt 0;
text-align: center;
}
figcaption {
font-size: 10pt;
font-style: italic;
margin-top: 6pt;
}
/* ========================================
Lists
======================================== */
ul, ol {
margin: 10pt 0;
padding-left: 20pt;
}
li {
font-size: 11pt;
margin-bottom: 4pt;
}
/* ========================================
Print Header/Footer
======================================== */
.print-header {
display: block;
text-align: center;
font-size: 10pt;
margin-bottom: 18pt;
padding-bottom: 12pt;
border-bottom: 0.5pt solid #000;
}
.print-footer {
display: block;
text-align: center;
font-size: 9pt;
margin-top: 24pt;
padding-top: 12pt;
border-top: 0.5pt solid #000;
}
}

View File

@@ -0,0 +1,660 @@
/**
* SwissFini.sh Accessibility Controller
* WCAG 2.2 compliant self-service accessibility controls
*
* Features:
* - Font size adjustment (5 levels: 75%-150%)
* - Theme toggle (light/dark/high-contrast/system)
* - Dyslexia-friendly font toggle
* - Line spacing control (4 levels)
* - Reduced motion toggle
* - Reading width control (3 levels)
* - Enhanced focus indicators toggle
* - All preferences persisted in localStorage
*/
(function () {
'use strict';
// ============================================
// Configuration
// ============================================
const STORAGE_KEY = 'swissfini_accessibility';
const FIRST_VISIT_KEY = 'swissfini_first_visit';
const DEFAULTS = {
fontSize: 3, // 1-5 scale, 3 = 100%
theme: 'system', // 'light', 'dark', 'high-contrast', 'system'
dyslexiaFont: false,
lineSpacing: 'normal', // 'compact', 'normal', 'relaxed', 'loose'
reducedMotion: 'system', // 'true', 'false', 'system'
readingWidth: 'medium', // 'narrow', 'medium', 'wide'
enhancedFocus: false
};
const FONT_SIZES = {
1: 0.75, // 75%
2: 0.875, // 87.5%
3: 1, // 100%
4: 1.25, // 125%
5: 1.5 // 150%
};
const FONT_SIZE_LABELS = {
1: '75%',
2: '88%',
3: '100%',
4: '125%',
5: '150%'
};
// ============================================
// State Management
// ============================================
let state = { ...DEFAULTS };
function loadPreferences() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
state = { ...DEFAULTS, ...parsed };
}
} catch (e) {
console.warn('Could not load accessibility preferences:', e);
}
return state;
}
function savePreferences() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch (e) {
console.warn('Could not save accessibility preferences:', e);
}
}
function isFirstVisit() {
try {
return !localStorage.getItem(FIRST_VISIT_KEY);
} catch (e) {
return true;
}
}
function markVisited() {
try {
localStorage.setItem(FIRST_VISIT_KEY, 'true');
} catch (e) {
// Silent fail
}
}
// ============================================
// DOM Manipulation
// ============================================
const html = document.documentElement;
function applyFontSize(level) {
html.setAttribute('data-font-size', level);
// Update label if exists
const label = document.getElementById('font-size-label');
if (label) {
label.textContent = FONT_SIZE_LABELS[level] || '100%';
}
}
function applyTheme(theme) {
// Remove existing theme
html.removeAttribute('data-theme');
let effectiveTheme = theme;
if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
effectiveTheme = prefersDark ? 'dark' : 'light';
}
if (effectiveTheme !== 'light') {
html.setAttribute('data-theme', effectiveTheme);
}
// Update meta theme-color
const metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) {
const colors = {
light: '#faf9f7',
dark: '#0d0d0f',
'high-contrast': '#000000'
};
metaTheme.setAttribute('content', colors[effectiveTheme] || colors.light);
}
}
function applyDyslexiaFont(enabled) {
html.setAttribute('data-dyslexia', enabled.toString());
}
function applyLineSpacing(spacing) {
html.setAttribute('data-line-spacing', spacing);
}
function applyReducedMotion(preference) {
if (preference === 'system') {
html.removeAttribute('data-reduced-motion');
} else {
html.setAttribute('data-reduced-motion', preference);
}
}
function applyReadingWidth(width) {
html.setAttribute('data-reading-width', width);
}
function applyEnhancedFocus(enabled) {
html.setAttribute('data-enhanced-focus', enabled.toString());
}
function applyAllPreferences() {
applyFontSize(state.fontSize);
applyTheme(state.theme);
applyDyslexiaFont(state.dyslexiaFont);
applyLineSpacing(state.lineSpacing);
applyReducedMotion(state.reducedMotion);
applyReadingWidth(state.readingWidth);
applyEnhancedFocus(state.enhancedFocus);
}
// ============================================
// Event Handlers
// ============================================
function handleFontSizeChange(direction) {
const newSize = Math.max(1, Math.min(5, state.fontSize + direction));
if (newSize !== state.fontSize) {
state.fontSize = newSize;
applyFontSize(newSize);
savePreferences();
updateControlStates();
announceChange(`Font size changed to ${FONT_SIZE_LABELS[newSize]}`);
}
}
function handleThemeChange(theme) {
state.theme = theme;
applyTheme(theme);
savePreferences();
updateControlStates();
const themeNames = {
light: 'light mode',
dark: 'dark mode',
'high-contrast': 'high contrast mode',
system: 'system preference'
};
announceChange(`Theme changed to ${themeNames[theme]}`);
}
function handleDyslexiaToggle() {
state.dyslexiaFont = !state.dyslexiaFont;
applyDyslexiaFont(state.dyslexiaFont);
savePreferences();
updateControlStates();
announceChange(`Dyslexia-friendly font ${state.dyslexiaFont ? 'enabled' : 'disabled'}`);
}
function handleLineSpacingChange(spacing) {
state.lineSpacing = spacing;
applyLineSpacing(spacing);
savePreferences();
updateControlStates();
announceChange(`Line spacing set to ${spacing}`);
}
function handleReducedMotionChange(preference) {
state.reducedMotion = preference;
applyReducedMotion(preference);
savePreferences();
updateControlStates();
const labels = {
true: 'enabled',
false: 'disabled',
system: 'set to system preference'
};
announceChange(`Reduced motion ${labels[preference]}`);
}
function handleReadingWidthChange(width) {
state.readingWidth = width;
applyReadingWidth(width);
savePreferences();
updateControlStates();
announceChange(`Reading width set to ${width}`);
}
function handleEnhancedFocusToggle() {
state.enhancedFocus = !state.enhancedFocus;
applyEnhancedFocus(state.enhancedFocus);
savePreferences();
updateControlStates();
announceChange(`Enhanced focus indicators ${state.enhancedFocus ? 'enabled' : 'disabled'}`);
}
function handleResetPreferences() {
state = { ...DEFAULTS };
applyAllPreferences();
savePreferences();
updateControlStates();
announceChange('All accessibility preferences reset to defaults');
}
// ============================================
// Screen Reader Announcements
// ============================================
let announcer = null;
function createAnnouncer() {
announcer = document.createElement('div');
announcer.id = 'accessibility-announcer';
announcer.setAttribute('role', 'status');
announcer.setAttribute('aria-live', 'polite');
announcer.setAttribute('aria-atomic', 'true');
announcer.className = 'accessibility-announcer';
document.body.appendChild(announcer);
}
function announceChange(message) {
if (!announcer) createAnnouncer();
// Clear and re-announce for screen readers
announcer.textContent = '';
requestAnimationFrame(() => {
announcer.textContent = message;
});
}
// ============================================
// Panel Toggle & Focus Management
// ============================================
let lastFocusedElement = null;
function initPanelToggle() {
const toggleBtn = document.getElementById('accessibility-toggle');
const panel = document.getElementById('accessibility-panel');
const closeBtn = document.getElementById('accessibility-close');
const backdrop = document.getElementById('accessibility-backdrop');
if (!toggleBtn || !panel) return;
function openPanel() {
lastFocusedElement = document.activeElement;
panel.classList.add('is-open');
panel.setAttribute('aria-hidden', 'false');
toggleBtn.setAttribute('aria-expanded', 'true');
if (backdrop) {
backdrop.classList.add('is-visible');
}
// Focus first focusable element
const firstFocusable = panel.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
setTimeout(() => firstFocusable.focus(), 50);
}
// Add event listeners
document.addEventListener('keydown', handlePanelKeydown);
}
function closePanel() {
panel.classList.remove('is-open');
panel.setAttribute('aria-hidden', 'true');
toggleBtn.setAttribute('aria-expanded', 'false');
if (backdrop) {
backdrop.classList.remove('is-visible');
}
// Restore focus
if (lastFocusedElement) {
lastFocusedElement.focus();
}
// Remove event listeners
document.removeEventListener('keydown', handlePanelKeydown);
}
function handlePanelKeydown(e) {
// Close on Escape
if (e.key === 'Escape') {
closePanel();
return;
}
// Focus trapping
if (e.key === 'Tab') {
const focusables = panel.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
if (focusables.length === 0) return;
const firstFocusable = focusables[0];
const lastFocusable = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
// Toggle button click
toggleBtn.addEventListener('click', () => {
const isOpen = panel.classList.contains('is-open');
if (isOpen) {
closePanel();
} else {
openPanel();
}
});
// Close button click
if (closeBtn) {
closeBtn.addEventListener('click', closePanel);
}
// Backdrop click
if (backdrop) {
backdrop.addEventListener('click', closePanel);
}
// Remove first-visit animation after interaction
toggleBtn.addEventListener('click', () => {
toggleBtn.classList.remove('is-new');
markVisited();
}, { once: true });
}
// ============================================
// Control State Synchronization
// ============================================
function updateControlStates() {
// Font size buttons
const decreaseBtn = document.getElementById('font-size-decrease');
const increaseBtn = document.getElementById('font-size-increase');
const fontLabel = document.getElementById('font-size-label');
if (decreaseBtn) decreaseBtn.disabled = state.fontSize <= 1;
if (increaseBtn) increaseBtn.disabled = state.fontSize >= 5;
if (fontLabel) fontLabel.textContent = FONT_SIZE_LABELS[state.fontSize];
// Theme buttons
document.querySelectorAll('[data-theme-option]').forEach(btn => {
const isActive = btn.dataset.themeOption === state.theme;
btn.setAttribute('aria-pressed', isActive);
});
// Dyslexia toggle
const dyslexiaBtn = document.getElementById('dyslexia-toggle');
if (dyslexiaBtn) {
dyslexiaBtn.setAttribute('aria-pressed', state.dyslexiaFont);
}
// Line spacing buttons
document.querySelectorAll('[data-spacing-option]').forEach(btn => {
const isActive = btn.dataset.spacingOption === state.lineSpacing;
btn.setAttribute('aria-pressed', isActive);
});
// Reduced motion buttons
document.querySelectorAll('[data-motion-option]').forEach(btn => {
const isActive = btn.dataset.motionOption === state.reducedMotion;
btn.setAttribute('aria-pressed', isActive);
});
// Reading width buttons
document.querySelectorAll('[data-width-option]').forEach(btn => {
const isActive = btn.dataset.widthOption === state.readingWidth;
btn.setAttribute('aria-pressed', isActive);
});
// Enhanced focus toggle
const focusBtn = document.getElementById('enhanced-focus-toggle');
if (focusBtn) {
focusBtn.setAttribute('aria-pressed', state.enhancedFocus);
}
}
// ============================================
// Event Binding
// ============================================
function bindEvents() {
// Font size
const decreaseBtn = document.getElementById('font-size-decrease');
const increaseBtn = document.getElementById('font-size-increase');
if (decreaseBtn) {
decreaseBtn.addEventListener('click', () => handleFontSizeChange(-1));
}
if (increaseBtn) {
increaseBtn.addEventListener('click', () => handleFontSizeChange(1));
}
// Theme
document.querySelectorAll('[data-theme-option]').forEach(btn => {
btn.addEventListener('click', () => handleThemeChange(btn.dataset.themeOption));
});
// Dyslexia
const dyslexiaBtn = document.getElementById('dyslexia-toggle');
if (dyslexiaBtn) {
dyslexiaBtn.addEventListener('click', handleDyslexiaToggle);
}
// Line spacing
document.querySelectorAll('[data-spacing-option]').forEach(btn => {
btn.addEventListener('click', () => handleLineSpacingChange(btn.dataset.spacingOption));
});
// Reduced motion
document.querySelectorAll('[data-motion-option]').forEach(btn => {
btn.addEventListener('click', () => handleReducedMotionChange(btn.dataset.motionOption));
});
// Reading width
document.querySelectorAll('[data-width-option]').forEach(btn => {
btn.addEventListener('click', () => handleReadingWidthChange(btn.dataset.widthOption));
});
// Enhanced focus
const focusBtn = document.getElementById('enhanced-focus-toggle');
if (focusBtn) {
focusBtn.addEventListener('click', handleEnhancedFocusToggle);
}
// Reset
const resetBtn = document.getElementById('accessibility-reset');
if (resetBtn) {
resetBtn.addEventListener('click', handleResetPreferences);
}
// System preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (state.theme === 'system') {
applyTheme('system');
}
});
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', () => {
if (state.reducedMotion === 'system') {
applyReducedMotion('system');
}
});
}
// ============================================
// Header Scroll Effect
// ============================================
function initHeaderScroll() {
const header = document.querySelector('.site-header');
if (!header) return;
let lastScroll = 0;
const scrollThreshold = 10;
function handleScroll() {
const currentScroll = window.scrollY;
if (currentScroll > scrollThreshold) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
lastScroll = currentScroll;
}
window.addEventListener('scroll', handleScroll, { passive: true });
}
// ============================================
// Mobile Navigation
// ============================================
function initMobileNav() {
const toggle = document.querySelector('.nav-toggle');
const nav = document.querySelector('.main-nav');
if (!toggle || !nav) return;
toggle.addEventListener('click', () => {
const isOpen = nav.classList.contains('is-open');
nav.classList.toggle('is-open');
toggle.setAttribute('aria-expanded', !isOpen);
});
// Close on click outside
document.addEventListener('click', (e) => {
if (!nav.contains(e.target) && !toggle.contains(e.target)) {
nav.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
}
});
// Close on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && nav.classList.contains('is-open')) {
nav.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
toggle.focus();
}
});
}
// ============================================
// Initialization
// ============================================
function init() {
loadPreferences();
applyAllPreferences();
// Wait for DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onDOMReady);
} else {
onDOMReady();
}
}
function onDOMReady() {
bindEvents();
initPanelToggle();
initHeaderScroll();
initMobileNav();
updateControlStates();
// Show first-visit animation
if (isFirstVisit()) {
const toggleBtn = document.getElementById('accessibility-toggle');
if (toggleBtn) {
toggleBtn.classList.add('is-new');
}
}
}
// ============================================
// Critical Styles (Prevent Flash)
// ============================================
(function applyCriticalStyles() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const prefs = JSON.parse(stored);
// Apply theme immediately
if (prefs.theme && prefs.theme !== 'light') {
if (prefs.theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
html.setAttribute('data-theme', 'dark');
}
} else {
html.setAttribute('data-theme', prefs.theme);
}
}
// Apply font scale
if (prefs.fontSize && FONT_SIZES[prefs.fontSize]) {
html.setAttribute('data-font-size', prefs.fontSize);
}
// Apply dyslexia font
if (prefs.dyslexiaFont) {
html.setAttribute('data-dyslexia', 'true');
}
// Apply other critical preferences
if (prefs.lineSpacing) {
html.setAttribute('data-line-spacing', prefs.lineSpacing);
}
if (prefs.readingWidth) {
html.setAttribute('data-reading-width', prefs.readingWidth);
}
if (prefs.enhancedFocus) {
html.setAttribute('data-enhanced-focus', 'true');
}
if (prefs.reducedMotion && prefs.reducedMotion !== 'system') {
html.setAttribute('data-reduced-motion', prefs.reducedMotion);
}
}
} catch (e) {
// Silent fail
}
})();
// Start
init();
})();

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang | default "en" }}">
<head>
{{- partial "head.html" . -}}
</head>
<body>
<div class="page-wrapper">
{{- partial "skip-links.html" . -}}
{{- partial "header.html" . -}}
<main id="main-content" class="main-content" role="main">
{{- block "main" . }}{{- end }}
</main>
{{- partial "footer.html" . -}}
</div>
{{- partial "accessibility-panel.html" . -}}
{{/* Backdrop for panel */}}
<div id="accessibility-backdrop" class="accessibility-backdrop" aria-hidden="true"></div>
{{/* Load JavaScript */}}
{{ $js := resources.Get "js/accessibility.js" | minify | fingerprint }}
<script src="{{ $js.RelPermalink }}" defer></script>
</body>
</html>

View File

@@ -0,0 +1,99 @@
{{ define "main" }}
<div class="container py-8">
<header class="mb-8">
<h1 class="article-title">{{ .Title }}</h1>
{{ with .Description }}
<p class="lead">{{ . }}</p>
{{ end }}
</header>
{{ if .Pages }}
<div class="article-list">
{{ range .Pages }}
<article class="article-card">
<a href="{{ .Permalink }}" class="article-card__link">
{{ with .Params.category }}
<span class="article-category">{{ . }}</span>
{{ end }}
<h2 class="article-card__title">{{ .Title }}</h2>
{{ with .Params.subtitle }}
<p class="article-card__excerpt">{{ . }}</p>
{{ else }}
{{ with .Summary }}
<p class="article-card__excerpt">{{ . | plainify | truncate 160 }}</p>
{{ end }}
{{ end }}
<div class="article-card__meta">
{{ if .Date }}
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "Jan 2, 2006" }}
</time>
{{ end }}
{{ if .ReadingTime }}
<span>{{ .ReadingTime }} min read</span>
{{ end }}
</div>
</a>
</article>
{{ end }}
</div>
{{ else }}
<p class="text-muted">No articles yet.</p>
{{ end }}
</div>
<style>
.article-list {
display: grid;
gap: var(--space-6);
}
.article-card {
background-color: var(--color-bg-elevated);
border-radius: var(--radius-md);
border: var(--border-width) solid var(--color-border);
transition: var(--transition-shadow), var(--transition-transform);
}
.article-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.article-card__link {
display: block;
padding: var(--space-6);
text-decoration: none;
color: inherit;
}
.article-card__title {
font-size: var(--font-size-xl);
margin: var(--space-2) 0 var(--space-3);
color: var(--color-text-primary);
border-bottom: none;
}
.article-card:hover .article-card__title {
color: var(--color-accent-cardinal);
}
.article-card__excerpt {
color: var(--color-text-secondary);
font-size: var(--font-size-base);
line-height: var(--line-height-relaxed);
margin-bottom: var(--space-4);
}
.article-card__meta {
display: flex;
gap: var(--space-4);
font-family: var(--font-ui);
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
}
</style>
{{ end }}

View File

@@ -0,0 +1,69 @@
{{ define "main" }}
<article class="article" itemscope itemtype="https://schema.org/Article">
{{/* Article Header */}}
<header class="article-header">
{{ with .Params.category }}
<span class="article-category" itemprop="articleSection">{{ . }}</span>
{{ end }}
<h1 class="article-title" itemprop="headline">{{ .Title }}</h1>
{{ with .Params.subtitle }}
<p class="article-subtitle" itemprop="description">{{ . }}</p>
{{ end }}
<div class="article-meta">
{{ if .Date }}
<span class="article-meta-item">
<svg class="article-meta-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
<time datetime="{{ .Date.Format "2006-01-02" }}" itemprop="datePublished">
{{ .Date.Format "January 2, 2006" }}
</time>
</span>
{{ end }}
{{ with .Params.source }}
<span class="article-meta-item">
<svg class="article-meta-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<path d="M14 2v6h6"></path>
</svg>
{{ . }}
</span>
{{ end }}
{{ if .ReadingTime }}
<span class="article-meta-item">
<svg class="article-meta-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
{{ .ReadingTime }} min read
</span>
{{ end }}
</div>
</header>
{{/* Article Body */}}
<div class="article-body" itemprop="articleBody">
{{ .Content }}
</div>
{{/* Tags */}}
{{ with .Params.tags }}
<footer class="article-footer">
<div class="article-tags">
<span class="article-tags-label">Tags:</span>
{{ range . }}
<a href="{{ "tags/" | relURL }}{{ . | urlize }}/" class="article-tag">{{ . }}</a>
{{ end }}
</div>
</footer>
{{ end }}
</article>
{{ end }}

View File

@@ -0,0 +1,36 @@
{{ define "main" }}
<article class="article">
{{/* Article Header */}}
<header class="article-header">
{{ with .Params.category }}
<span class="article-category">{{ . }}</span>
{{ end }}
<h1 class="article-title">{{ .Title }}</h1>
{{ with .Params.subtitle }}
<p class="article-subtitle">{{ . }}</p>
{{ end }}
<div class="article-meta">
{{ with .Params.source }}
<span class="article-meta-item">
<svg class="article-meta-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<path d="M14 2v6h6"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
<path d="M10 9H8"></path>
</svg>
{{ . }}
</span>
{{ end }}
</div>
</header>
{{/* Article Body */}}
<div class="article-body">
{{ .Content }}
</div>
</article>
{{ end }}

View File

@@ -0,0 +1,160 @@
{{/* Accessibility Settings Panel */}}
<aside
id="accessibility-panel"
class="accessibility-panel"
aria-hidden="true"
aria-labelledby="accessibility-panel-title"
role="dialog"
aria-modal="true"
>
{{/* Panel Header */}}
<div class="accessibility-panel__header">
<h2 id="accessibility-panel-title">Accessibility</h2>
<button
id="accessibility-close"
class="accessibility-panel__close"
aria-label="Close accessibility settings"
>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18"></path>
<path d="M6 6l12 12"></path>
</svg>
</button>
</div>
{{/* Panel Content */}}
<div class="accessibility-panel__content">
{{/* Font Size */}}
<fieldset class="accessibility-control">
<legend>Text Size</legend>
<div class="accessibility-control__buttons">
<button
id="font-size-decrease"
class="btn btn--icon"
aria-label="Decrease text size"
>
<span aria-hidden="true">A</span>
</button>
<span
id="font-size-label"
class="accessibility-control__value"
aria-live="polite"
>100%</span>
<button
id="font-size-increase"
class="btn btn--icon"
aria-label="Increase text size"
>
<span aria-hidden="true">A+</span>
</button>
</div>
</fieldset>
{{/* Color Theme */}}
<fieldset class="accessibility-control">
<legend>Color Theme</legend>
<div class="accessibility-control__buttons" role="group" aria-label="Select color theme">
<button data-theme-option="light" class="btn btn--option" aria-pressed="false">
Light
</button>
<button data-theme-option="dark" class="btn btn--option" aria-pressed="false">
Dark
</button>
<button data-theme-option="high-contrast" class="btn btn--option" aria-pressed="false">
High Contrast
</button>
<button data-theme-option="system" class="btn btn--option" aria-pressed="true">
System
</button>
</div>
</fieldset>
{{/* Dyslexia Font */}}
<fieldset class="accessibility-control">
<legend>Reading Support</legend>
<button
id="dyslexia-toggle"
class="btn btn--toggle"
aria-pressed="false"
role="switch"
>
<span class="btn__label">Dyslexia-friendly font</span>
<span class="btn__state" aria-hidden="true"></span>
</button>
</fieldset>
{{/* Line Spacing */}}
<fieldset class="accessibility-control">
<legend>Line Spacing</legend>
<div class="accessibility-control__buttons" role="group" aria-label="Select line spacing">
<button data-spacing-option="compact" class="btn btn--option" aria-pressed="false">
Compact
</button>
<button data-spacing-option="normal" class="btn btn--option" aria-pressed="true">
Normal
</button>
<button data-spacing-option="relaxed" class="btn btn--option" aria-pressed="false">
Relaxed
</button>
<button data-spacing-option="loose" class="btn btn--option" aria-pressed="false">
Loose
</button>
</div>
</fieldset>
{{/* Reduced Motion */}}
<fieldset class="accessibility-control">
<legend>Motion</legend>
<div class="accessibility-control__buttons" role="group" aria-label="Select motion preference">
<button data-motion-option="true" class="btn btn--option" aria-pressed="false">
Reduce
</button>
<button data-motion-option="false" class="btn btn--option" aria-pressed="false">
Allow
</button>
<button data-motion-option="system" class="btn btn--option" aria-pressed="true">
System
</button>
</div>
</fieldset>
{{/* Reading Width */}}
<fieldset class="accessibility-control">
<legend>Reading Width</legend>
<div class="accessibility-control__buttons" role="group" aria-label="Select reading width">
<button data-width-option="narrow" class="btn btn--option" aria-pressed="false">
Narrow
</button>
<button data-width-option="medium" class="btn btn--option" aria-pressed="true">
Medium
</button>
<button data-width-option="wide" class="btn btn--option" aria-pressed="false">
Wide
</button>
</div>
</fieldset>
{{/* Enhanced Focus */}}
<fieldset class="accessibility-control">
<legend>Keyboard Navigation</legend>
<button
id="enhanced-focus-toggle"
class="btn btn--toggle"
aria-pressed="false"
role="switch"
>
<span class="btn__label">Enhanced focus indicators</span>
<span class="btn__state" aria-hidden="true"></span>
</button>
</fieldset>
{{/* Reset Button */}}
<div class="accessibility-control accessibility-control--footer">
<button id="accessibility-reset" class="btn btn--secondary">
Reset to Defaults
</button>
</div>
</div>
</aside>

View File

@@ -0,0 +1,55 @@
<footer class="site-footer" role="contentinfo">
<div class="footer-container">
{{/* Footer Top */}}
<div class="footer-top">
{{/* Brand */}}
<div class="footer-brand">
<a href="{{ "/" | relURL }}" class="footer-logo" aria-label="{{ .Site.Title }} - Home">
<span class="logo-mark" aria-hidden="true">SF</span>
<span class="site-title"><span class="swiss">Swiss</span>Fini.sh</span>
</a>
<p class="footer-description">
{{ .Site.Params.description }}
</p>
</div>
{{/* Navigation */}}
<div class="footer-section">
<h3>Navigation</h3>
<ul class="footer-links" role="list">
{{ range .Site.Menus.main }}
<li><a href="{{ .URL | relURL }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</div>
{{/* Resources */}}
<div class="footer-section">
<h3>Resources</h3>
<ul class="footer-links" role="list">
<li><a href="{{ "index.xml" | relURL }}">RSS Feed</a></li>
<li><a href="#" id="footer-accessibility-link">Accessibility</a></li>
</ul>
</div>
</div>
{{/* Footer Bottom */}}
<div class="footer-bottom">
<p class="footer-copyright">
&copy; {{ now.Year }} {{ .Site.Title }}. All facts may be satirical.
</p>
<ul class="footer-legal" role="list">
<li><a href="/privacy/">Privacy</a></li>
<li><a href="/terms/">Terms</a></li>
</ul>
</div>
</div>
</footer>
<script>
// Footer accessibility link opens panel
document.getElementById('footer-accessibility-link')?.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('accessibility-toggle')?.click();
});
</script>

View File

@@ -0,0 +1,116 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{{/* Title */}}
<title>{{ if .IsHome }}{{ .Site.Title }} - {{ .Site.Params.tagline }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
{{/* Meta Description */}}
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Summary }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}{{ end }}">
{{/* Author */}}
<meta name="author" content="{{ .Site.Params.author }}">
{{/* Theme Color - Updated by JS based on preferences */}}
<meta name="theme-color" content="#faf9f7">
{{/* Canonical URL */}}
<link rel="canonical" href="{{ .Permalink }}">
{{/* Open Graph / Social */}}
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:title" content="{{ .Title }}">
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Summary }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:site_name" content="{{ .Site.Title }}">
{{ with .Site.Params.ogImage }}<meta property="og:image" content="{{ . | absURL }}">{{ end }}
{{/* Twitter Card */}}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .Title }}">
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Summary }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}{{ end }}">
{{/* RSS */}}
{{ range .AlternativeOutputFormats -}}
{{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
{{ end -}}
{{/* Preconnect to Google Fonts */}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
{{/* Stylesheets - Concatenate all CSS files */}}
{{ $variables := resources.Get "css/base/_variables.css" }}
{{ $reset := resources.Get "css/base/_reset.css" }}
{{ $typography := resources.Get "css/base/_typography.css" }}
{{ $dark := resources.Get "css/themes/_dark.css" }}
{{ $highContrast := resources.Get "css/themes/_high-contrast.css" }}
{{ $header := resources.Get "css/components/_header.css" }}
{{ $footer := resources.Get "css/components/_footer.css" }}
{{ $article := resources.Get "css/components/_article.css" }}
{{ $accessibilityPanel := resources.Get "css/components/_accessibility-panel.css" }}
{{ $focus := resources.Get "css/utilities/_focus.css" }}
{{ $print := resources.Get "css/utilities/_print.css" }}
{{ $main := resources.Get "css/main.css" }}
{{ $allCSS := slice $variables $reset $typography $dark $highContrast $header $footer $article $accessibilityPanel $focus $print $main | resources.Concat "css/bundle.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $allCSS.RelPermalink }}" integrity="{{ $allCSS.Data.Integrity }}">
{{/* Favicon */}}
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
{{/* Critical inline script to prevent theme flash */}}
<script>
(function() {
try {
var stored = localStorage.getItem('swissfini_accessibility');
if (stored) {
var prefs = JSON.parse(stored);
var html = document.documentElement;
// Theme
if (prefs.theme && prefs.theme !== 'light') {
if (prefs.theme === 'system') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
html.setAttribute('data-theme', 'dark');
}
} else {
html.setAttribute('data-theme', prefs.theme);
}
}
// Font size
if (prefs.fontSize) {
html.setAttribute('data-font-size', prefs.fontSize);
}
// Dyslexia
if (prefs.dyslexiaFont) {
html.setAttribute('data-dyslexia', 'true');
}
// Line spacing
if (prefs.lineSpacing) {
html.setAttribute('data-line-spacing', prefs.lineSpacing);
}
// Reading width
if (prefs.readingWidth) {
html.setAttribute('data-reading-width', prefs.readingWidth);
}
// Enhanced focus
if (prefs.enhancedFocus) {
html.setAttribute('data-enhanced-focus', 'true');
}
// Reduced motion
if (prefs.reducedMotion && prefs.reducedMotion !== 'system') {
html.setAttribute('data-reduced-motion', prefs.reducedMotion);
}
}
} catch (e) {}
})();
</script>

View File

@@ -0,0 +1,63 @@
<header class="site-header" role="banner">
<div class="header-container">
{{/* Logo & Site Title */}}
<a href="{{ "/" | relURL }}" class="site-logo" aria-label="{{ .Site.Title }} - Home">
<span class="logo-mark" aria-hidden="true">SF</span>
<div>
<span class="site-title"><span class="swiss">Swiss</span>Fini.sh</span>
<span class="site-tagline">{{ .Site.Params.tagline }}</span>
</div>
</a>
{{/* Mobile Nav Toggle */}}
<button
class="nav-toggle"
aria-expanded="false"
aria-controls="main-navigation"
aria-label="Toggle navigation menu"
>
<span class="nav-toggle-icon" aria-hidden="true"></span>
</button>
{{/* Main Navigation */}}
<nav id="main-navigation" class="main-nav" aria-label="Main navigation">
<ul class="nav-list" role="list">
{{ range .Site.Menus.main }}
<li class="nav-item">
<a
href="{{ .URL | relURL }}"
class="nav-link{{ if $.IsMenuCurrent "main" . }} active{{ end }}"
{{ if $.IsMenuCurrent "main" . }}aria-current="page"{{ end }}
>
{{ .Name }}
</a>
</li>
{{ end }}
</ul>
</nav>
{{/* Header Actions */}}
<div class="header-actions">
{{/* Accessibility Toggle */}}
<button
id="accessibility-toggle"
class="accessibility-toggle"
aria-expanded="false"
aria-controls="accessibility-panel"
aria-label="Open accessibility settings"
>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 1v2"></path>
<path d="M12 21v2"></path>
<path d="M4.22 4.22l1.42 1.42"></path>
<path d="M18.36 18.36l1.42 1.42"></path>
<path d="M1 12h2"></path>
<path d="M21 12h2"></path>
<path d="M4.22 19.78l1.42-1.42"></path>
<path d="M18.36 5.64l1.42-1.42"></path>
</svg>
</button>
</div>
</div>
</header>

View File

@@ -0,0 +1,3 @@
<nav class="skip-links" aria-label="Skip links">
<a href="#main-content" class="skip-link">Skip to main content</a>
</nav>

View File

@@ -0,0 +1,8 @@
{{/* Conclusion Box Shortcode
Usage: {{< conclusion >}}Content{{< /conclusion >}}
{{< conclusion title="The Bottom Line" >}}Content{{< /conclusion >}}
*/}}
<div class="conclusion" role="note" aria-label="{{ .Get "title" | default "Conclusion" }}">
<h3>{{ .Get "title" | default "The Bottom Line" }}</h3>
{{ .Inner | markdownify }}
</div>

View File

@@ -0,0 +1,12 @@
{{/* Irony Box Shortcode
Usage: {{< irony >}}Content here{{< /irony >}}
{{< irony title="Custom Title" >}}Content{{< /irony >}}
*/}}
<div class="irony-box" role="note" aria-label="{{ .Get "title" | default "Editorial Note" }}">
{{ with .Get "title" }}
<strong>{{ . }}:</strong>
{{ else }}
<strong>Translation:</strong>
{{ end }}
{{ .Inner | markdownify }}
</div>