slate-theme 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: slate-theme
3
+ Version: 0.1.0
4
+ Summary: A sleek, developer-focused, and open-source bilingual portfolio and blog site theme for Zensical.
5
+ Project-URL: Homepage, https://github.com/landerox/zensical-slate-theme
6
+ Project-URL: Repository, https://github.com/landerox/zensical-slate-theme
7
+ Project-URL: Issues, https://github.com/landerox/zensical-slate-theme/issues
8
+ Author-email: Fernando Landero <landerox@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bilingual,blog,developer,mkdocs,portfolio,slate,theme,zensical
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Documentation
19
+ Classifier: Topic :: Software Development :: Documentation
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: zensical>=0.0.45
22
+ Description-Content-Type: text/markdown
23
+
24
+ <!-- markdownlint-disable MD041 MD033 MD013 -->
25
+
26
+ <p align="center">
27
+ <img
28
+ src="https://raw.githubusercontent.com/landerox/zensical-slate-theme/main/zensical_slate_theme/assets/images/banner.svg"
29
+ width="460"
30
+ alt="zensical-slate-theme Banner"
31
+ />
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://github.com/landerox/zensical-slate-theme"><img src="https://img.shields.io/badge/GitHub-Repository-blue?logo=github&logoColor=white" alt="GitHub Repository" /></a>
36
+ <a href="https://pypi.org/project/slate-theme/"><img src="https://img.shields.io/pypi/v/slate-theme.svg?logo=pypi&logoColor=white" alt="PyPI Version" /></a>
37
+ <a href="https://pypi.org/project/slate-theme/"><img src="https://img.shields.io/pypi/pyversions/slate-theme.svg?logo=python&logoColor=white" alt="Python Support" /></a>
38
+ <a href="https://github.com/landerox/zensical-slate-theme/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" /></a>
39
+ </p>
40
+
41
+ ---
42
+
43
+ # Zensical Theme Slate
44
+
45
+ `slate-theme` is a sleek, developer-focused, and highly interactive bilingual portfolio and blog theme packaged for [Zensical](https://zensical.org) (the modern, Rust-powered static site generator built on top of MkDocs).
46
+
47
+ This package contains the core compiled design assets and templates so you can apply the polished **Slate Theme** directly to any existing Zensical website with zero-configuration setup.
48
+
49
+ <p align="center">
50
+ <img
51
+ src="https://raw.githubusercontent.com/landerox/zensical-slate-theme/main/content/en/assets/images/zensical-slate-theme-preview.webp"
52
+ width="800"
53
+ alt="zensical-slate-theme Live Preview"
54
+ />
55
+ </p>
56
+
57
+ ---
58
+
59
+ ## 🌟 Key Theme Features
60
+
61
+ * **Sleek Slate Aesthetics:** Deep graphite base (`#0b0f19`) and tailored indigo/slate accents for a premium technical look.
62
+ * **Interactive Neural Background:** Canvas particle system connecting nodes with lines that respond smoothly to mouse movements.
63
+ * **Circular Page Ripple:** A gorgeous ripple transition triggered when toggling light/dark modes (fully respects `prefers-reduced-motion`).
64
+ * **3D Glare Tilt Cards:** Project and experience cards that tilt dynamically in response to mouse coordinates with a glassmorphism reflection overlay.
65
+ * **Zero-Configuration Asset Injection:** Automated asset injection via layout overrides, loading styles and scripts natively without configuration boilerplate.
66
+
67
+ ---
68
+
69
+ ## 🚀 Installation & Usage
70
+
71
+ You can apply the Slate theme to your Zensical project in two simple steps:
72
+
73
+ ### 1. Install the Package
74
+
75
+ Add the package to your project's virtual environment using `uv` (recommended):
76
+
77
+ ```bash
78
+ uv add slate-theme
79
+ ```
80
+
81
+ Or using standard `pip`:
82
+
83
+ ```bash
84
+ pip install slate-theme
85
+ ```
86
+
87
+ ### 2. Configure Your Theme
88
+
89
+ Update your `zensical.toml` (and `zensical.es.toml` if your site is bilingual) to load the packaged theme:
90
+
91
+ ```toml
92
+ [project.theme]
93
+ name = "slate"
94
+ ```
95
+
96
+ > [!NOTE]
97
+ > **Zero-Configuration Setup:** You do NOT need to declare `extra_css` or `extra_javascript` configurations in your site configuration file to load the theme's core stylesheets and scripts. The package handles this natively via layout template inheritance.
98
+
99
+ ---
100
+
101
+ ## 🎨 Starter Template
102
+
103
+ If you are starting a new website from scratch, this repository also serves as a complete **Bilingual Starter Pack**.
104
+
105
+ Instead of configuring everything manually, you can use the pre-configured starter layout, multi-language structure (English and Spanish), and automated deployment workflows.
106
+
107
+ Visit the **[Official GitHub Repository](https://github.com/landerox/zensical-slate-theme)** for cloning instructions, manual setup steps, and the full bilingual customization guide.
108
+
109
+ ---
110
+
111
+ ## 📄 License
112
+
113
+ To maintain a clear boundary between the open-source engine and the theme's core assets or template content, this repository operates under a dual-license model:
114
+
115
+ * **Source Code** (`pyproject.toml`, `.github/`, scripts, tooling configurations) is released under the [MIT License](https://github.com/landerox/zensical-slate-theme/blob/main/LICENSE).
116
+ * **Starter Template Content** (Markdown files under `content/`, default demo images, and placeholder prose) is released under the [Creative Commons Attribution 4.0 International License (CC-BY-4.0)](https://github.com/landerox/zensical-slate-theme/blob/main/LICENSE-CONTENT).
@@ -0,0 +1,15 @@
1
+ zensical_slate_theme/__init__.py,sha256=H4EVFtLNadR7hPJvCV24AZf1R6Abz1x1K_-V6FPrDnU,137
2
+ zensical_slate_theme/main.html,sha256=ejSxRFwlG8Ij4jyb9BWfg69FJpAhOFAUp0ntC5GjLtM,273
3
+ zensical_slate_theme/mkdocs_theme.yml,sha256=cukiYGtZqiDJoK8rWEOteUyRLot7ZKNQXNvYsPeJf7M,122
4
+ zensical_slate_theme/assets/images/banner.svg,sha256=wQ9y4JYUn8GY8o7F1da6AtumC-sTjD7HSjYI-j1PdCU,1327
5
+ zensical_slate_theme/assets/images/favicon.svg,sha256=glgvdXwMr7Sq7wCfCRtENXLJImhVwJLEHFt1N7TFK_A,726
6
+ zensical_slate_theme/assets/images/logo.svg,sha256=glgvdXwMr7Sq7wCfCRtENXLJImhVwJLEHFt1N7TFK_A,726
7
+ zensical_slate_theme/assets/images/profile.svg,sha256=bm7uYEAgGjRc444hjGC2nBUWBTSdeMlbI4VvLSDWH7U,1140
8
+ zensical_slate_theme/assets/images/zensical-slate-theme-preview.webp,sha256=4cEChckDJTmp3FLIbBZVdUyZQVOrTWqQZkiTMnOP7-w,78034
9
+ zensical_slate_theme/assets/javascripts/extra.js,sha256=OfBde76-aYZjn7zY5lytkfPnonSK7aqlvzJEYoNSK9w,10913
10
+ zensical_slate_theme/assets/stylesheets/extra.css,sha256=xSEA5Nk1p0g8Ui9SCmHLJ4vw6oX3AxnY_PnIuLCgE-c,28305
11
+ slate_theme-0.1.0.dist-info/METADATA,sha256=15ny4mtqmkl6DVihe84OnuNTrMKmNgxQUMGK_EETkVM,5340
12
+ slate_theme-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
13
+ slate_theme-0.1.0.dist-info/entry_points.txt,sha256=TJw7P2c4So33AsnKYhSu9aYK97e9da4ZdRvSxSHf3u0,45
14
+ slate_theme-0.1.0.dist-info/licenses/LICENSE,sha256=wHQWxu03TdX4QFaCPqOffYggBfvkT7C3U7x02Ya8I4g,1084
15
+ slate_theme-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [mkdocs.themes]
2
+ slate = zensical_slate_theme
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 {Your Name or Organization}
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ """Zensical Theme Slate.
2
+
3
+ A sleek, developer-focused, and open-source personal portfolio and blog site theme.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,32 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 100" width="480" height="100">
2
+ <defs>
3
+ <style>
4
+ .logo-text {
5
+ fill: #f8fafc;
6
+ }
7
+ </style>
8
+ <linearGradient id="slateGrad" x1="0%" y1="0%" x2="100%" y2="100%">
9
+ <stop offset="0%" stop-color="#82b0ff" />
10
+ <stop offset="100%" stop-color="#4f46e5" />
11
+ </linearGradient>
12
+ <linearGradient id="darkGrad" x1="0%" y1="0%" x2="100%" y2="100%">
13
+ <stop offset="0%" stop-color="#94a3b8" />
14
+ <stop offset="100%" stop-color="#334155" />
15
+ </linearGradient>
16
+ <linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
17
+ <stop offset="0%" stop-color="#0b0f19" />
18
+ <stop offset="100%" stop-color="#1e1e2f" />
19
+ </linearGradient>
20
+ </defs>
21
+ <!-- Background Card -->
22
+ <rect width="480" height="100" rx="12" fill="url(#bgGrad)" />
23
+ <!-- Logo Symbol -->
24
+ <g transform="translate(10, 10) scale(0.8)">
25
+ <!-- Slab 1: Slate/Grey -->
26
+ <polygon points="20,70 40,20 52,20 32,70" fill="url(#darkGrad)" />
27
+ <!-- Slab 2: Glowing Blue-Indigo -->
28
+ <polygon points="42,80 62,30 74,30 54,80" fill="url(#slateGrad)" />
29
+ </g>
30
+ <!-- Text -->
31
+ <text x="95" y="62" class="logo-text" font-family="Outfit, Inter, system-ui, -apple-system, sans-serif" font-size="32" font-weight="700">zensical-slate-theme</text>
32
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <defs>
3
+ <linearGradient id="slateGrad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#82b0ff" />
5
+ <stop offset="100%" stop-color="#4f46e5" />
6
+ </linearGradient>
7
+ <linearGradient id="darkGrad" x1="0%" y1="0%" x2="100%" y2="100%">
8
+ <stop offset="0%" stop-color="#94a3b8" />
9
+ <stop offset="100%" stop-color="#334155" />
10
+ </linearGradient>
11
+ </defs>
12
+ <g>
13
+ <!-- Slab 1: Slate/Grey -->
14
+ <polygon points="20,70 40,20 52,20 32,70" fill="url(#darkGrad)" />
15
+ <!-- Slab 2: Glowing Blue-Indigo -->
16
+ <polygon points="42,80 62,30 74,30 54,80" fill="url(#slateGrad)" />
17
+ </g>
18
+ </svg>
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <defs>
3
+ <linearGradient id="slateGrad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#82b0ff" />
5
+ <stop offset="100%" stop-color="#4f46e5" />
6
+ </linearGradient>
7
+ <linearGradient id="darkGrad" x1="0%" y1="0%" x2="100%" y2="100%">
8
+ <stop offset="0%" stop-color="#94a3b8" />
9
+ <stop offset="100%" stop-color="#334155" />
10
+ </linearGradient>
11
+ </defs>
12
+ <g>
13
+ <!-- Slab 1: Slate/Grey -->
14
+ <polygon points="20,70 40,20 52,20 32,70" fill="url(#darkGrad)" />
15
+ <!-- Slab 2: Glowing Blue-Indigo -->
16
+ <polygon points="42,80 62,30 74,30 54,80" fill="url(#slateGrad)" />
17
+ </g>
18
+ </svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
2
+ <defs>
3
+ <!-- Background Circle Gradient -->
4
+ <linearGradient id="avatarBg" x1="0%" y1="0%" x2="100%" y2="100%">
5
+ <stop offset="0%" stop-color="#1e293b" />
6
+ <stop offset="100%" stop-color="#0f172a" />
7
+ </linearGradient>
8
+ <!-- Glow effect for silhouette -->
9
+ <linearGradient id="avatarGlow" x1="0%" y1="0%" x2="100%" y2="0%">
10
+ <stop offset="0%" stop-color="#82b0ff" stop-opacity="0.8" />
11
+ <stop offset="100%" stop-color="#4f46e5" stop-opacity="0.8" />
12
+ </linearGradient>
13
+ </defs>
14
+
15
+ <!-- Circle background with border -->
16
+ <circle cx="50" cy="50" r="48" fill="url(#avatarBg)" stroke="#334155" stroke-width="2" />
17
+
18
+ <!-- Stylized head and shoulders silhouette -->
19
+ <circle cx="50" cy="40" r="16" fill="url(#avatarGlow)" />
20
+ <path d="M18,80 C18,65 30,58 50,58 C70,58 82,65 82,80 Z" fill="url(#avatarGlow)" />
21
+
22
+ <!-- Sleek glowing accent line/ring to make it look premium -->
23
+ <circle cx="50" cy="50" r="44" fill="none" stroke="#4f46e5" stroke-width="1.5" stroke-dasharray="20 10 5 10" opacity="0.5" />
24
+ </svg>
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Custom JavaScript for Zensical Theme Slate (EN)
3
+ * Implements View Transitions API for theme switches and other client-side interactions.
4
+ */
5
+ document.addEventListener("DOMContentLoaded", () => {
6
+ // Theme Toggle Circular Transition using View Transitions API
7
+ document.addEventListener("click", (e) => {
8
+ // Find if the clicked element or its parent is a palette toggle label
9
+ const label = e.target.closest('label[for^="__palette_"]');
10
+ if (!label) return;
11
+
12
+ // Check if the browser supports the View Transitions API
13
+ if (!document.startViewTransition) return;
14
+
15
+ // Check if the user prefers reduced motion
16
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
17
+ if (prefersReducedMotion) return;
18
+
19
+ // Prevent default click behavior so we can animate it manually
20
+ e.preventDefault();
21
+
22
+ // Get click coordinates
23
+ const x = e.clientX;
24
+ const y = e.clientY;
25
+
26
+ // Set custom properties on the root element
27
+ document.documentElement.style.setProperty("--click-x", `${x}px`);
28
+ document.documentElement.style.setProperty("--click-y", `${y}px`);
29
+
30
+ // Calculate the maximum radius to cover the entire viewport
31
+ const maxRadius = Math.hypot(
32
+ Math.max(x, window.innerWidth - x),
33
+ Math.max(y, window.innerHeight - y)
34
+ );
35
+ document.documentElement.style.setProperty("--clip-radius", `${maxRadius}px`);
36
+
37
+ // Get the radio input associated with the label
38
+ const targetInputId = label.getAttribute("for");
39
+ const targetInput = document.getElementById(targetInputId);
40
+
41
+ if (targetInput) {
42
+ // Start the view transition and click the input inside it
43
+ document.startViewTransition(() => {
44
+ targetInput.click();
45
+ });
46
+ }
47
+ });
48
+ });
49
+
50
+ /* ---------------------------------------------------------- */
51
+ /* Interactive Neural Network Background Code */
52
+ /* ---------------------------------------------------------- */
53
+ (() => {
54
+ let canvas = null;
55
+ let ctx = null;
56
+ let particles = [];
57
+ let animationFrameId = null;
58
+ const mouse = { x: null, y: null, radius: 200 };
59
+ let width = 0;
60
+ let height = 0;
61
+ let isInitialized = false;
62
+
63
+ class Particle {
64
+ constructor() {
65
+ this.x = Math.random() * width;
66
+ this.y = Math.random() * height;
67
+ this.vx = (Math.random() - 0.5) * 0.4;
68
+ this.vy = (Math.random() - 0.5) * 0.4;
69
+ this.radius = Math.random() * 3.0 + 2.5;
70
+ }
71
+
72
+ update() {
73
+ this.x += this.vx;
74
+ this.y += this.vy;
75
+
76
+ // Robust boundary collisions using absolute direction forces
77
+ if (this.x < 0) {
78
+ this.x = 0;
79
+ this.vx = Math.abs(this.vx);
80
+ } else if (this.x > width) {
81
+ this.x = width;
82
+ this.vx = -Math.abs(this.vx);
83
+ }
84
+
85
+ if (this.y < 0) {
86
+ this.y = 0;
87
+ this.vy = Math.abs(this.vy);
88
+ } else if (this.y > height) {
89
+ this.y = height;
90
+ this.vy = -Math.abs(this.vy);
91
+ }
92
+ }
93
+
94
+ draw() {
95
+ ctx.beginPath();
96
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
97
+ ctx.fill();
98
+ }
99
+ }
100
+
101
+ function initParticles() {
102
+ particles = [];
103
+ const count = Math.min(115, Math.floor((width * height) / 15000));
104
+ for (let i = 0; i < count; i++) {
105
+ particles.push(new Particle());
106
+ }
107
+ }
108
+
109
+ function resizeCanvas() {
110
+ if (!canvas) return;
111
+ const dpr = window.devicePixelRatio || 1;
112
+ width = window.innerWidth;
113
+ height = window.innerHeight;
114
+ // Setting canvas width/height resets the 2D context drawing state.
115
+ // Assign immediately before scale() to prevent transform compounding.
116
+ canvas.width = width * dpr;
117
+ canvas.height = height * dpr;
118
+ canvas.style.width = `${width}px`;
119
+ canvas.style.height = `${height}px`;
120
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
121
+ initParticles();
122
+ }
123
+
124
+ function setupNeuralBackground() {
125
+ // Disable entirely if user prefers reduced motion
126
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
127
+ if (prefersReducedMotion) return;
128
+
129
+ canvas = document.getElementById("neural-background");
130
+ if (!canvas) {
131
+ canvas = document.createElement("canvas");
132
+ canvas.id = "neural-background";
133
+ document.body.appendChild(canvas);
134
+ }
135
+ ctx = canvas.getContext("2d");
136
+
137
+ resizeCanvas();
138
+
139
+ if (!isInitialized) {
140
+ isInitialized = true;
141
+
142
+ window.addEventListener("resize", resizeCanvas);
143
+
144
+ window.addEventListener("mousemove", (e) => {
145
+ mouse.x = e.clientX;
146
+ mouse.y = e.clientY;
147
+ });
148
+
149
+ window.addEventListener("mouseleave", () => {
150
+ mouse.x = null;
151
+ mouse.y = null;
152
+ });
153
+
154
+ document.addEventListener("visibilitychange", () => {
155
+ if (document.visibilityState === "visible") {
156
+ if (!animationFrameId) {
157
+ animate();
158
+ }
159
+ } else {
160
+ if (animationFrameId) {
161
+ cancelAnimationFrame(animationFrameId);
162
+ animationFrameId = null;
163
+ }
164
+ }
165
+ });
166
+
167
+ animate();
168
+ }
169
+ }
170
+
171
+ function animate() {
172
+ if (document.visibilityState === "hidden") {
173
+ animationFrameId = null;
174
+ return;
175
+ }
176
+
177
+ if (!ctx) return;
178
+ ctx.clearRect(0, 0, width, height);
179
+
180
+ const isDarkMode = document.documentElement.getAttribute("data-md-color-scheme") === "slate";
181
+
182
+ // Section-aware color: vary accent by current page path
183
+ const path = window.location.pathname;
184
+ let particleHue = isDarkMode ? "130, 176, 255" : "79, 70, 229";
185
+ if (path.includes("/radar/ai") || path.includes("/services/production-ai")) {
186
+ particleHue = isDarkMode ? "168, 130, 255" : "124, 58, 237";
187
+ } else if (path.includes("/radar/data") || path.includes("/services/data")) {
188
+ particleHue = isDarkMode ? "130, 220, 180" : "16, 130, 90";
189
+ } else if (path.includes("/radar/devops") || path.includes("/services/cloud")) {
190
+ particleHue = isDarkMode ? "130, 200, 255" : "37, 99, 235";
191
+ }
192
+
193
+ const particleColor = `rgba(${particleHue}, ${isDarkMode ? "0.45" : "0.35"})`;
194
+ const connectionDist = 220;
195
+
196
+ ctx.fillStyle = particleColor;
197
+
198
+ for (let i = 0; i < particles.length; i++) {
199
+ particles[i].update();
200
+ particles[i].draw();
201
+ }
202
+
203
+ ctx.lineWidth = 0.9;
204
+ for (let i = 0; i < particles.length; i++) {
205
+ const p1 = particles[i];
206
+ for (let j = i + 1; j < particles.length; j++) {
207
+ const p2 = particles[j];
208
+ const dx = p1.x - p2.x;
209
+ const dy = p1.y - p2.y;
210
+ const dist = Math.hypot(dx, dy);
211
+
212
+ if (dist < connectionDist) {
213
+ const alpha = (1 - dist / connectionDist) * 0.55;
214
+ ctx.strokeStyle = `rgba(${particleHue}, ${(alpha * (isDarkMode ? 0.75 : 0.85)).toFixed(3)})`;
215
+ ctx.beginPath();
216
+ ctx.moveTo(p1.x, p1.y);
217
+ ctx.lineTo(p2.x, p2.y);
218
+ ctx.stroke();
219
+ }
220
+ }
221
+
222
+ if (mouse.x !== null && mouse.y !== null) {
223
+ const dx = p1.x - mouse.x;
224
+ const dy = p1.y - mouse.y;
225
+ const dist = Math.hypot(dx, dy);
226
+ if (dist < mouse.radius) {
227
+ const alpha = (1 - dist / mouse.radius) * 0.45;
228
+ ctx.strokeStyle = `rgba(${particleHue}, ${(alpha * (isDarkMode ? 0.7 : 0.9)).toFixed(3)})`;
229
+ ctx.beginPath();
230
+ ctx.moveTo(p1.x, p1.y);
231
+ ctx.lineTo(mouse.x, mouse.y);
232
+ ctx.stroke();
233
+ }
234
+ }
235
+ }
236
+
237
+ animationFrameId = requestAnimationFrame(animate);
238
+ }
239
+
240
+ /**
241
+ * 3D interactive card effect with cursor-tracking glare highlight
242
+ */
243
+ function setupCard3DEffect() {
244
+ const cards = document.querySelectorAll(".grid.cards > ul > li, .grid > .card");
245
+ if (cards.length === 0) return;
246
+
247
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
248
+ if (prefersReducedMotion) return;
249
+
250
+ cards.forEach((card) => {
251
+ // Avoid double listener attachments on instant navigation
252
+ if (card.dataset.tiltInitialized) return;
253
+ card.dataset.tiltInitialized = "true";
254
+
255
+ card.addEventListener("mousemove", (e) => {
256
+ const rect = card.getBoundingClientRect();
257
+ const x = e.clientX - rect.left;
258
+ const y = e.clientY - rect.top;
259
+
260
+ const px = (x / rect.width) - 0.5;
261
+ const py = (y / rect.height) - 0.5;
262
+
263
+ const maxRotation = 3;
264
+ const rx = -py * maxRotation;
265
+ const ry = px * maxRotation;
266
+
267
+ card.style.setProperty("--rx", `${rx}deg`);
268
+ card.style.setProperty("--ry", `${ry}deg`);
269
+ card.style.setProperty("--ty", `-3px`);
270
+ card.style.setProperty("--mouse-x", `${x}px`);
271
+ card.style.setProperty("--mouse-y", `${y}px`);
272
+ });
273
+
274
+ card.addEventListener("mouseleave", () => {
275
+ card.style.setProperty("--rx", "0deg");
276
+ card.style.setProperty("--ry", "0deg");
277
+ card.style.setProperty("--ty", "0px");
278
+ card.style.setProperty("--mouse-x", "-999px");
279
+ card.style.setProperty("--mouse-y", "-999px");
280
+ });
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Force repository links in header to open in a new tab
286
+ */
287
+ function setupRepoLinkTarget() {
288
+ const repoLinks = document.querySelectorAll(".md-header__source, .md-source");
289
+ repoLinks.forEach((link) => {
290
+ if (link.tagName === "A") {
291
+ link.setAttribute("target", "_blank");
292
+ link.setAttribute("rel", "noopener noreferrer");
293
+ } else {
294
+ const anchors = link.querySelectorAll("a");
295
+ anchors.forEach((a) => {
296
+ a.setAttribute("target", "_blank");
297
+ a.setAttribute("rel", "noopener noreferrer");
298
+ });
299
+ }
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Append theme and template details to the generator notice in the footer
305
+ */
306
+ function setupFooterAttribution() {
307
+ const copyright = document.querySelector(".md-copyright");
308
+ if (!copyright) return;
309
+
310
+ // Avoid duplicate appends on instant navigation reload
311
+ if (copyright.dataset.themeLinkAdded) return;
312
+ copyright.dataset.themeLinkAdded = "true";
313
+
314
+ const isEs = document.documentElement.lang === "es";
315
+ const zensicalLink = copyright.querySelector("a[href*='zensical.org']");
316
+ if (zensicalLink) {
317
+ const span = document.createElement("span");
318
+ if (isEs) {
319
+ span.innerHTML = ' usando <a href="https://github.com/landerox/zensical-slate-theme" target="_blank" rel="noopener">Slate Theme</a>';
320
+ } else {
321
+ span.innerHTML = ' using <a href="https://github.com/landerox/zensical-slate-theme" target="_blank" rel="noopener">Slate Theme</a>';
322
+ }
323
+ zensicalLink.after(span);
324
+ }
325
+ }
326
+
327
+ if (typeof document$ !== "undefined") {
328
+ document$.subscribe(() => {
329
+ setupNeuralBackground();
330
+ setupCard3DEffect();
331
+ setupRepoLinkTarget();
332
+ setupFooterAttribution();
333
+ });
334
+ } else {
335
+ document.addEventListener("DOMContentLoaded", () => {
336
+ setupNeuralBackground();
337
+ setupCard3DEffect();
338
+ setupRepoLinkTarget();
339
+ setupFooterAttribution();
340
+ });
341
+ }
342
+ })();
343
+