diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..691fed6 --- /dev/null +++ b/content/_index.md @@ -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... + +
+
+

Continue Reading

+

Discover what the SCION marketing doesn't tell you:

+ + Read Full Analysis + No paywall. No subscription. Just facts. +
+
diff --git a/content/articles/_index.md b/content/articles/_index.md new file mode 100644 index 0000000..2eba020 --- /dev/null +++ b/content/articles/_index.md @@ -0,0 +1,4 @@ +--- +title: "Articles" +description: "Investigative analysis of Swiss tech decisions and their real-world consequences." +--- diff --git a/content/articles/scion-vs-sdwan.md b/content/articles/scion-vs-sdwan.md new file mode 100644 index 0000000..d15c104 --- /dev/null +++ b/content/articles/scion-vs-sdwan.md @@ -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 >}} + +
+ +### 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) + +
diff --git a/hugo.toml b/hugo.toml index 7e568b8..08c46b9 100644 --- a/hugo.toml +++ b/hugo.toml @@ -1,3 +1,40 @@ -baseURL = 'https://example.org/' +baseURL = 'https://swissfini.sh/' 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 diff --git a/themes/swissfini/assets/css/base/_reset.css b/themes/swissfini/assets/css/base/_reset.css new file mode 100644 index 0000000..eedfdd7 --- /dev/null +++ b/themes/swissfini/assets/css/base/_reset.css @@ -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); +} diff --git a/themes/swissfini/assets/css/base/_typography.css b/themes/swissfini/assets/css/base/_typography.css new file mode 100644 index 0000000..9f448d8 --- /dev/null +++ b/themes/swissfini/assets/css/base/_typography.css @@ -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); +} diff --git a/themes/swissfini/assets/css/base/_variables.css b/themes/swissfini/assets/css/base/_variables.css new file mode 100644 index 0000000..35e6fda --- /dev/null +++ b/themes/swissfini/assets/css/base/_variables.css @@ -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; +} diff --git a/themes/swissfini/assets/css/components/_accessibility-panel.css b/themes/swissfini/assets/css/components/_accessibility-panel.css new file mode 100644 index 0000000..fdd99ef --- /dev/null +++ b/themes/swissfini/assets/css/components/_accessibility-panel.css @@ -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); + } +} diff --git a/themes/swissfini/assets/css/components/_article.css b/themes/swissfini/assets/css/components/_article.css new file mode 100644 index 0000000..e55870b --- /dev/null +++ b/themes/swissfini/assets/css/components/_article.css @@ -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); + } +} diff --git a/themes/swissfini/assets/css/components/_footer.css b/themes/swissfini/assets/css/components/_footer.css new file mode 100644 index 0000000..353725f --- /dev/null +++ b/themes/swissfini/assets/css/components/_footer.css @@ -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; + } +} diff --git a/themes/swissfini/assets/css/components/_header.css b/themes/swissfini/assets/css/components/_header.css new file mode 100644 index 0000000..0b905b8 --- /dev/null +++ b/themes/swissfini/assets/css/components/_header.css @@ -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; + } +} diff --git a/themes/swissfini/assets/css/main.css b/themes/swissfini/assets/css/main.css new file mode 100644 index 0000000..faa4f4c --- /dev/null +++ b/themes/swissfini/assets/css/main.css @@ -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); } diff --git a/themes/swissfini/assets/css/themes/_dark.css b/themes/swissfini/assets/css/themes/_dark.css new file mode 100644 index 0000000..7399084 --- /dev/null +++ b/themes/swissfini/assets/css/themes/_dark.css @@ -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; +} diff --git a/themes/swissfini/assets/css/themes/_high-contrast.css b/themes/swissfini/assets/css/themes/_high-contrast.css new file mode 100644 index 0000000..b900923 --- /dev/null +++ b/themes/swissfini/assets/css/themes/_high-contrast.css @@ -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); +} diff --git a/themes/swissfini/assets/css/utilities/_focus.css b/themes/swissfini/assets/css/utilities/_focus.css new file mode 100644 index 0000000..8d2cb40 --- /dev/null +++ b/themes/swissfini/assets/css/utilities/_focus.css @@ -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); +} diff --git a/themes/swissfini/assets/css/utilities/_print.css b/themes/swissfini/assets/css/utilities/_print.css new file mode 100644 index 0000000..c00cbdc --- /dev/null +++ b/themes/swissfini/assets/css/utilities/_print.css @@ -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; + } +} diff --git a/themes/swissfini/assets/js/accessibility.js b/themes/swissfini/assets/js/accessibility.js new file mode 100644 index 0000000..0e7874c --- /dev/null +++ b/themes/swissfini/assets/js/accessibility.js @@ -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(); +})(); diff --git a/themes/swissfini/layouts/_default/baseof.html b/themes/swissfini/layouts/_default/baseof.html new file mode 100644 index 0000000..a427d87 --- /dev/null +++ b/themes/swissfini/layouts/_default/baseof.html @@ -0,0 +1,27 @@ + + + + {{- partial "head.html" . -}} + + +
+ {{- partial "skip-links.html" . -}} + {{- partial "header.html" . -}} + +
+ {{- block "main" . }}{{- end }} +
+ + {{- partial "footer.html" . -}} +
+ + {{- partial "accessibility-panel.html" . -}} + + {{/* Backdrop for panel */}} + + + {{/* Load JavaScript */}} + {{ $js := resources.Get "js/accessibility.js" | minify | fingerprint }} + + + diff --git a/themes/swissfini/layouts/_default/list.html b/themes/swissfini/layouts/_default/list.html new file mode 100644 index 0000000..ef29613 --- /dev/null +++ b/themes/swissfini/layouts/_default/list.html @@ -0,0 +1,99 @@ +{{ define "main" }} +
+
+

{{ .Title }}

+ {{ with .Description }} +

{{ . }}

+ {{ end }} +
+ + {{ if .Pages }} +
+ {{ range .Pages }} + + {{ end }} +
+ {{ else }} +

No articles yet.

+ {{ end }} +
+ + +{{ end }} diff --git a/themes/swissfini/layouts/_default/single.html b/themes/swissfini/layouts/_default/single.html new file mode 100644 index 0000000..54b5b65 --- /dev/null +++ b/themes/swissfini/layouts/_default/single.html @@ -0,0 +1,69 @@ +{{ define "main" }} +
+ {{/* Article Header */}} +
+ {{ with .Params.category }} + + {{ end }} + +

{{ .Title }}

+ + {{ with .Params.subtitle }} +

{{ . }}

+ {{ end }} + + +
+ + {{/* Article Body */}} +
+ {{ .Content }} +
+ + {{/* Tags */}} + {{ with .Params.tags }} + + {{ end }} +
+{{ end }} diff --git a/themes/swissfini/layouts/index.html b/themes/swissfini/layouts/index.html new file mode 100644 index 0000000..1ed85d1 --- /dev/null +++ b/themes/swissfini/layouts/index.html @@ -0,0 +1,36 @@ +{{ define "main" }} +
+ {{/* Article Header */}} +
+ {{ with .Params.category }} + + {{ end }} + +

{{ .Title }}

+ + {{ with .Params.subtitle }} +

{{ . }}

+ {{ end }} + + +
+ + {{/* Article Body */}} +
+ {{ .Content }} +
+
+{{ end }} diff --git a/themes/swissfini/layouts/partials/accessibility-panel.html b/themes/swissfini/layouts/partials/accessibility-panel.html new file mode 100644 index 0000000..2fcd46c --- /dev/null +++ b/themes/swissfini/layouts/partials/accessibility-panel.html @@ -0,0 +1,160 @@ +{{/* Accessibility Settings Panel */}} + diff --git a/themes/swissfini/layouts/partials/footer.html b/themes/swissfini/layouts/partials/footer.html new file mode 100644 index 0000000..cc7019b --- /dev/null +++ b/themes/swissfini/layouts/partials/footer.html @@ -0,0 +1,55 @@ + + + diff --git a/themes/swissfini/layouts/partials/head.html b/themes/swissfini/layouts/partials/head.html new file mode 100644 index 0000000..2f27796 --- /dev/null +++ b/themes/swissfini/layouts/partials/head.html @@ -0,0 +1,116 @@ + + + + +{{/* Title */}} +{{ if .IsHome }}{{ .Site.Title }} - {{ .Site.Params.tagline }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }} + +{{/* Meta Description */}} + + +{{/* Author */}} + + +{{/* Theme Color - Updated by JS based on preferences */}} + + +{{/* Canonical URL */}} + + +{{/* Open Graph / Social */}} + + + + + +{{ with .Site.Params.ogImage }}{{ end }} + +{{/* Twitter Card */}} + + + + +{{/* RSS */}} +{{ range .AlternativeOutputFormats -}} + {{ printf `` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }} +{{ end -}} + +{{/* Preconnect to Google Fonts */}} + + + +{{/* 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 }} + + +{{/* Favicon */}} + + + + +{{/* Critical inline script to prevent theme flash */}} + diff --git a/themes/swissfini/layouts/partials/header.html b/themes/swissfini/layouts/partials/header.html new file mode 100644 index 0000000..46fee37 --- /dev/null +++ b/themes/swissfini/layouts/partials/header.html @@ -0,0 +1,63 @@ + diff --git a/themes/swissfini/layouts/partials/skip-links.html b/themes/swissfini/layouts/partials/skip-links.html new file mode 100644 index 0000000..dbf3633 --- /dev/null +++ b/themes/swissfini/layouts/partials/skip-links.html @@ -0,0 +1,3 @@ + diff --git a/themes/swissfini/layouts/shortcodes/conclusion.html b/themes/swissfini/layouts/shortcodes/conclusion.html new file mode 100644 index 0000000..068f181 --- /dev/null +++ b/themes/swissfini/layouts/shortcodes/conclusion.html @@ -0,0 +1,8 @@ +{{/* Conclusion Box Shortcode +Usage: {{< conclusion >}}Content{{< /conclusion >}} + {{< conclusion title="The Bottom Line" >}}Content{{< /conclusion >}} +*/}} +
+

{{ .Get "title" | default "The Bottom Line" }}

+ {{ .Inner | markdownify }} +
diff --git a/themes/swissfini/layouts/shortcodes/irony.html b/themes/swissfini/layouts/shortcodes/irony.html new file mode 100644 index 0000000..25c5e39 --- /dev/null +++ b/themes/swissfini/layouts/shortcodes/irony.html @@ -0,0 +1,12 @@ +{{/* Irony Box Shortcode +Usage: {{< irony >}}Content here{{< /irony >}} + {{< irony title="Custom Title" >}}Content{{< /irony >}} +*/}} +
+ {{ with .Get "title" }} + {{ . }}: + {{ else }} + Translation: + {{ end }} + {{ .Inner | markdownify }} +