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

29
content/_index.md Normal file
View File

@@ -0,0 +1,29 @@
---
title: "SCION: The Emperor's New Internet?"
category: "Investigation"
subtitle: "After 16 years and millions in funding, Switzerland's \"revolutionary\" internet architecture still requires the same expensive private infrastructure it promised to replace."
source: "Analysis based on IETF documentation, official SCION specs, and ISP deployment data"
---
SCION (Scalability, Control, and Isolation on Next-generation Networks) represents peak Swiss tech nationalism — a "clean-slate" internet architecture from ETH Zurich that's been in development since 2009. The promise was revolutionary: path-aware networking that would give enterprises unprecedented control over their data's journey across the internet, with security properties that would make BGP hijacking and DDoS attacks things of the past.
The Swiss Secure Finance Network (SSFN), connecting over 300 financial institutions, is held up as proof that SCION has moved from academic theory to production reality. Swisscom, Sunrise, and SWITCH have all deployed SCION infrastructure. The SCION Association boasts of a "BGP-free" future.
But a closer look at the actual deployment architecture reveals an uncomfortable truth that the marketing materials carefully obscure. When you dig into the IETF documentation and the technical specifications, you find a revealing admission about what SCION actually requires to deliver on its security promises...
<div class="fade-overlay">
<div class="paywall-box">
<h2>Continue Reading</h2>
<p>Discover what the SCION marketing doesn't tell you:</p>
<ul class="teaser-list">
<li>Why production SCION can't run over public internet</li>
<li>The "dedicated infrastructure" requirement hidden in plain sight</li>
<li>How SSFN replaced MPLS with... more private links</li>
<li>SCION's missing encryption layer vs SD-WAN</li>
<li>SRv6: The IETF standard that makes SCION redundant</li>
<li>Axpo Systems: Sells SCION, deploys SRv6 for ASTRA highways</li>
</ul>
<a href="/articles/scion-vs-sdwan/" class="cta-button">Read Full Analysis</a>
<span class="free-label">No paywall. No subscription. Just facts.</span>
</div>
</div>

View File

@@ -0,0 +1,4 @@
---
title: "Articles"
description: "Investigative analysis of Swiss tech decisions and their real-world consequences."
---

View File

@@ -0,0 +1,296 @@
---
title: "SCION vs SD-WAN: The Infrastructure Reality"
subtitle: "What actually runs under the hood of Switzerland's \"next-generation internet\""
category: "Investigation"
date: 2025-01-15
tags: ["SCION", "SD-WAN", "SRv6", "Swiss Tech", "Infrastructure"]
---
## Market Reality Check
| Metric | SCION | SD-WAN |
|--------|-------|--------|
| Market size | Unmeasured (Swiss niche) | $6-9 billion (2024) |
| Active vendors | 1 (Anapaya) + open source | 70+ vendors |
| Enterprise customers | ~300 (SSFN) | 40,000+ (Fortinet alone) |
| Development timeline | 16 years (since 2009) | ~10 years |
| Gartner Magic Quadrant | Not evaluated | Full quadrant, 6 leaders |
| Pricing transparency | "Book a demo" | Published pricing |
## The Underlay: What Actually Carries SCION Traffic?
### SCION Transport Layer
SCION packets are encapsulated in **UDP/IPv4 or UDP/IPv6** between SCION nodes:
> "SCION is using a UDP/IP underlay to transport SCION packets between SCION nodes. These UDP/IP packets are only valid between two SCION nodes and change after every SCION hop."
>
> — IETF Draft: draft-dekater-scion-dataplane
### The Dirty Secret: Dedicated Infrastructure Required
Here's the critical point that marketing materials gloss over:
> "When it comes to inter-domain communication, **an overlay deployment on top of today's Internet is not desirable**, as SCION would inherit issues from its weak underlay. Thus, **inter-AS SCION links are usually deployed in parallel to existing links**, in order to preserve its security properties."
>
> — IETF SCION Overview & Official Documentation
{{< irony >}}
Production SCION deployments require dedicated/parallel physical infrastructure between ISPs — just like the expensive MPLS VPNs that SD-WAN was designed to replace.
{{< /irony >}}
### SSFN: Replaced MPLS With... More Private Infrastructure
The Swiss Secure Finance Network is touted as SCION's flagship deployment. What it actually did:
> "SSFN replacing multiple existing MPLS networks"
>
> — SIX Group & Swisscom
SCION didn't eliminate expensive private infrastructure — it replaced one private network (MPLS) with another (dedicated SCION links between Swisscom, Sunrise, and SWITCH).
## Encryption: The Missing Layer
Unlike SD-WAN's mandatory IPSec encryption, SCION does **not encrypt payload by default**:
- **SPAO** (SCION Packet Authenticator Option) — authenticates packets using DRKey
- **Path validation** via cryptographic signatures
- **No mandatory payload encryption** — applications must handle this themselves
> "This option is primarily intended to be used in conjunction with DRKey which provides shared secrets without explicit key exchange... analogous to IPSec"
>
> — SCION SPAO Documentation
Note the word "analogous" — it's authentication, not encryption.
## Infrastructure Comparison
| Aspect | SCION (Production) | SD-WAN |
|--------|-------------------|--------|
| Inter-site transport | UDP/IP over **dedicated parallel links** | IPSec tunnels over public internet + optional MPLS |
| Payload encryption | Optional (app layer) | Mandatory IPSec (AES-256) |
| Can use public internet? | Not recommended for production | Yes (primary use case) |
| Private infrastructure needed? | Required for security guarantees | Optional (MPLS for premium) |
| Intra-AS transport | Existing IP/MPLS | Existing IP/MPLS |
| Path control | Full end-to-end | First hop only |
## The SCIONLab Admission
The research network that runs over public internet explicitly states:
> "The security, availability, and performance properties of SCION are **not fully realized**"
>
> — SCIONLab Documentation
## The Elephant in the Room: SRv6
While ETH Zurich spent 16 years building a clean-slate internet replacement, the IETF quietly standardized **Segment Routing over IPv6 (SRv6)** — which delivers end-to-end path control over the existing internet.
### What is SRv6?
SRv6 (RFC 8986) encodes routing instructions directly in the IPv6 header using a Segment Routing Header (SRH). The critical difference from SCION:
> "A transit node is a node along the path of the SRv6 packet. **The transit node does not inspect the SRH.** The destination address of the IPv6 packet does not correspond to the transit node."
>
> — Cisco SRv6 Configuration Guide
{{< irony >}}
Any standard IPv6 router in the middle of the path just forwards SRv6 packets normally — no upgrade required. Only the endpoints need SRv6 capability. It works transparently over the existing internet.
{{< /irony >}}
### SRv6 + SD-WAN = End-to-End Path Control
Modern SD-WAN platforms integrate with SRv6 to provide the path control that SCION claims as its unique advantage:
> "This integration allows SD-WAN policies to leverage SRv6 paths to meet specific application requirements, such as low latency or high reliability. Unified visibility across SD-WAN overlays and SRv6 underlays simplifies troubleshooting."
>
> — Cisco SD-WAN for Critical Networks
### Production Deployment Scale
While SCION serves ~300 Swiss financial institutions, SRv6 is deployed at global scale:
- **85,000+ Cisco routers** deployed with SRv6 (2025)
- **Reliance Jio** — 600 million mobile customers, 100 million homes
- **Rakuten Mobile** — largest SRv6 uSID migration in Japan
- **SoftBank Japan** — production SRv6 with network slicing
- **Bell Canada** — simplified data center operations
- **vivo Brazil** — multi-vendor SRv6 on live network
- **Swisscom** — yes, the same Swisscom promoting SCION
### Multi-Vendor, Standards-Based
Unlike SCION's single commercial vendor (Anapaya), SRv6 has full ecosystem support:
- **Cisco, Juniper, Nokia, Huawei** — all major vendors
- **IETF standardized** — RFC 8986, not a draft or research project
- **SONiC integration** — open source switch OS (Alibaba, Microsoft, Nvidia)
- **Interoperability tested** — EANTC multi-vendor validation
### The Compression Advantage: uSID
SRv6 micro-segments (uSID) compress up to 6 segment instructions into a single 128-bit IPv6 address, minimizing overhead while maintaining full path programmability.
## Case Study: Axpo Systems & ASTRA
The contradictions of Swiss SCION promotion are perfectly illustrated by **Axpo Systems AG**.
### Who is Axpo Systems?
- Subsidiary of Axpo Group, headquartered in Lupfig, ~140 employees
- Self-described as "The neural system of system-relevant Switzerland runs through us"
- Operates critical OT (Operational Technology) networks for Swiss infrastructure
### Their SCION Involvement
Axpo Systems is deeply invested in SCION:
- **March 2024:** Joined SCION Association as newest member
- **January 2025:** Launched "first OT Security Operations Center with SCION connectivity" with Anapaya
- Markets SCION as "the safest routing protocol for the Internet of the future"
- Sells "Secure WAN Service" based on SCION for enterprise customers
> "SCION combines the flexibility and accessibility of the public Internet with the security and reliability of a private MPLS network."
>
> — Axpo Systems marketing
### What They Actually Use for Critical Infrastructure
In November 2023, Axpo Systems won the contract to design, build, and operate **ASTRA's IP-Netz BSA** — the backbone network connecting Switzerland's national highway infrastructure (traffic management, safety systems, tunnel controls).
**Contract value:** CHF 1,514,100
The IP-Netz BSA is a dedicated network separate from Axpo's own aXbone infrastructure. It spans all of Switzerland, connecting ASTRA's regional units (Gebietseinheiten) with redundant fiber optic infrastructure routed along national road corridors.
### The Technology Choice: SRv6
When Axpo Systems designed and rolled out the ASTRA BSA network — critical infrastructure for Swiss highway safety — **they chose SRv6 (Segment Routing over IPv6)**.
Not SCION. Not the "revolutionary Swiss technology" they actively promote. They deployed the IETF-standard SRv6 for Switzerland's highway backbone.
{{< irony title="The Ultimate Hypocrisy" >}}
Axpo Systems — a SCION Association member since March 2024, promoter of SCION as "the safest routing protocol for the Internet of the future" — chose SRv6 over SCION when building critical Swiss infrastructure. If SCION were truly superior, why didn't they use it for ASTRA's highway network?
{{< /irony >}}
### Meanwhile, Their Own Backbone...
Axpo Systems' internal production infrastructure (the **aXbone** network serving their own customers) runs on traditional MPLS:
> "The crisis-proof and highly available **MPLS-based data network** of Axpo Systems is characterised by redundant line routing and comprehensive network monitoring."
>
> — Axpo Systems, aXbone Infrastructure
### The Three-Way Contradiction
| Network | Technology | Status |
|---------|------------|--------|
| ASTRA BSA (highways) | **SRv6** | Production — designed by Axpo Systems |
| aXbone (Axpo's backbone) | **MPLS** | Production — Axpo's own infrastructure |
| SCION | **SCION** | Marketing — what they sell to others |
When it matters — when Swiss highway safety depends on it — Axpo Systems deploys SRv6. When it's their own money — they run MPLS. When it's customer money — they sell SCION.
## The axboneNG Evolution: What's Actually Being Built
Axpo Systems is replacing the current aXbone with **axboneNG** — a next-generation backbone. The technology choice is revealing:
### axboneNG Platform
| Component | Technology | Purpose |
|-----------|------------|---------|
| Hardware | **Ribbon Neptune 1800 + NPT-1250** | Metro aggregation & access routing |
| Legacy OT services | **MPLS-TP** | TDM-based operational technology |
| Modern services | **FlexE + FlexAlgo + SR-MPLS** | Network slicing, traffic engineering |
The Ribbon Neptune platform supports IP/MPLS, MPLS-TP, Segment Routing, FlexE, and EVPN — all **industry-standard technologies**. Not SCION.
### SCION as an Overlay Service
Where does SCION fit in axboneNG? As a **service carried on top** of the real backbone:
- **SSUN ISD76 backbone:** Dedicated L3 VPN for Swiss Secure Utility Network core-to-core inter-AS links
- **SwissIX SCION VLAN:** Dedicated DWDM links from Axpo servers to SwissIX SCION peering — *parallel to* their regular internet exchange connectivity
{{< irony title="The Architecture Tells the Truth" >}}
SCION doesn't replace the backbone — it rides on top of it. Axpo Systems is building axboneNG on SR-MPLS and FlexE (industry standards), then carrying SCION as just another VPN service. The "revolutionary internet replacement" is an overlay on conventional infrastructure.
{{< /irony >}}
### Swiss Secure Utility Network (SSUN)
The SSUN, launched August 2025, is the SCION network for Swiss energy utilities. Key details:
- Partners: VSE, Anapaya, Axpo Systems, Cyberlink, Litecom, Sunrise, Swisscom
- ISD76 — the isolation domain for Swiss utilities
- By 2030, connection becomes "gradually mandatory" for utility market partners
But look at how SSUN is actually delivered: as a **dedicated L3 VPN** on Axpo's SR-MPLS backbone, with **dedicated DWDM links** to SwissIX for SCION peering. The underlying transport is conventional technology.
### SwissIX SCION Peering
SwissIX offers a dedicated SCION VLAN — the first IXP in the world to do so. But note the infrastructure:
- SCION runs as a **separate VLAN** alongside regular internet peering
- Participants need **dedicated ports** or spare capacity on existing ports
- Pricing: CHF 200-350/month per port
- Traffic must stay below 80% of paid port capacity
SCION at SwissIX isn't replacing internet peering — it's an **additional overlay service** requiring separate infrastructure and fees.
## The Ultimate Irony
| Capability | SCION | SD-WAN + SRv6 |
|------------|-------|---------------|
| End-to-end path control | Yes | Yes |
| Works over public internet | No (security degraded) | Yes (encrypted) |
| Transit router upgrade needed | Yes (SCION routers) | No (standard IPv6) |
| Dedicated inter-ISP links | Required for production | Not required |
| IETF standard | Draft stage | RFC 8986 (2021) |
| Vendor support | 1 (Anapaya) | All major vendors |
| Production scale | ~300 customers | Billions of endpoints |
{{< conclusion >}}
SCION's marketing claims "virtual connections just as secure as leased lines" — but achieving this requires deploying on **parallel dedicated infrastructure**, not the public internet.
Meanwhile, **SRv6 delivers the same end-to-end path control** that SCION touts as revolutionary — but it works transparently over any IPv6 network, is an IETF standard (not a draft), and is already deployed at billion-user scale.
The supposed SCION advantages are rendered a costly exercise in academic empire-building:
- **Path control?** SRv6 does it over standard IPv6.
- **No BGP dependency?** SRv6 source routing bypasses BGP path selection.
- **Multi-path?** SD-WAN + SRv6 provides it with encryption included.
**SD-WAN + SRv6:** Encrypts everything, works over public internet, end-to-end path control, IETF standard, all major vendors.
**SCION:** No encryption, requires dedicated links, single vendor, 16 years in development, still a draft.
{{< /conclusion >}}
<div class="sources">
### Sources
- [IETF: SCION Data Plane Draft](https://datatracker.ietf.org/doc/draft-dekater-scion-dataplane/)
- [IETF: SCION Overview](https://www.ietf.org/archive/id/draft-dekater-panrg-scion-overview-03.html)
- [SCION Packet Authenticator Option](https://docs.scion.org/en/latest/protocols/authenticator-option.html)
- [DRKey Infrastructure](https://docs.scion.org/en/latest/cryptography/drkey.html)
- [SIX: Secure Swiss Finance Network](https://www.six-group.com/en/products-services/banking-services/ssfn.html)
- [Swisscom: SCION & SSFN](https://www.swisscom.ch/en/business/enterprise/themen/security/resilienz-cyberattacken-scion.html)
- [Anapaya: SCION & SD-WAN](https://www.anapaya.net/blog/the-full-picture-scion-sd-wan)
- [SCIONLab Research Network](https://www.scionlab.org/)
- [RFC 8986: SRv6 Network Programming](https://datatracker.ietf.org/doc/rfc8986/)
- [Cisco: SRv6 Configuration Guide](https://www.cisco.com/c/en/us/td/docs/routers/asr9000/software/asr9k-r6-6/segment-routing/configuration/guide/b-segment-routing-cg-asr9000-66x.html)
- [Cisco: SD-WAN for Critical Networks](https://www.cisco.com/c/en/us/solutions/enterprise/design-zone-branch-wan/sd-wan-for-critical-networks-infrastructure-wp.html)
- [Segment Routing News: SRv6 Deployments](https://www.segment-routing.net/srv6-news)
- [Cisco: The Case for SRv6 (2025)](https://news-blogs.cisco.com/apjc/2025/01/22/the-case-for-srv6-simplifying-networks-for-a-complex-future/)
- [Anapaya: SCION vs Segment Routing](https://www.anapaya.net/blog/scion-vs.-segment-routing)
- [SCION Association: Axpo Systems Membership](https://www.scion.org/welcome-to-axpo-systems-the-newest-member-of-the-scion-association/)
- [Anapaya: Axpo Systems OT SOC](https://www.anapaya.net/news/the-first-ot-security-operation-center-with-scion-connectivity-is-launched-by-axpo-systems)
- [Axpo Systems: SCION Marketing](https://www.axpo.com/ch/en/energy/digital-solutions/cyber-security-connectivity/ot-innovation/scion.html)
- [Axpo Systems: aXbone MPLS](https://www.axpo.com/ch/en/energy/digital-solutions/cyber-security-connectivity/ot-networks/ip-mpls.html)
- [IT-Beschaffung: ASTRA Contracts](https://www.it-beschaffung.ch/list/it/a/2326/all/bundesamt-fuer-strassen-astra)
- [ASTRA 13040: IP-Netz BSA](https://www.astra.admin.ch/dam/astra/de/dokumente/standards_fuer_nationalstrassen/astra%2013040%20ipnetzbsa.pdf.download.pdf/astra_13040d.pdf)
- [Ribbon: Neptune NPT 1800](https://ribboncommunications.com/products/service-provider-products/ip-routing/access-aggregation-routers/npt-1800)
- [Anapaya: Secure Swiss Utility Network](https://www.anapaya.net/secure-swiss-utility-network-by-anapaya)
- [SwissIX: SCION Peering](https://www.swissix.ch/services/scion-peering-mesh/)
- [VSE: SSUN for National Security](https://www.strom.ch/en/perspective/protecting-utility-ecosystem-foundation-national-security)
</div>

View File

@@ -1,3 +1,40 @@
baseURL = 'https://example.org/' baseURL = 'https://swissfini.sh/'
languageCode = 'en-us' languageCode = 'en-us'
title = 'My New Hugo Site' title = 'SwissFini.sh'
theme = 'swissfini'
[params]
description = "Investigative analysis of Swiss tech nationalism"
author = "SwissFini Editorial"
tagline = "When Swiss-made becomes Swiss-made-up"
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
[markup.highlight]
style = 'dracula'
lineNos = false
lineNumbersInTable = true
[outputs]
home = ["HTML", "RSS"]
section = ["HTML", "RSS"]
[taxonomies]
tag = "tags"
category = "categories"
[menu]
[[menu.main]]
name = "Home"
url = "/"
weight = 1
[[menu.main]]
name = "Articles"
url = "/articles/"
weight = 2
[[menu.main]]
name = "About"
url = "/about/"
weight = 3

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>