@networkpro/web 1.14.3 → 1.15.1

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.
Files changed (38) hide show
  1. package/.node-version +1 -1
  2. package/.nvmrc +1 -1
  3. package/CHANGELOG.md +87 -2
  4. package/README.md +6 -4
  5. package/_redirects +4 -2
  6. package/netlify.toml +0 -5
  7. package/package.json +14 -15
  8. package/scripts/testRedirects.js +84 -0
  9. package/src/app.html +1 -1
  10. package/src/hooks.server.js +20 -1
  11. package/src/lib/components/RedirectPage.svelte +2 -34
  12. package/src/lib/components/layout/HeaderDefault.svelte +8 -3
  13. package/src/lib/components/layout/HeaderHome.svelte +8 -3
  14. package/src/lib/pages/AboutContent.svelte +1 -1
  15. package/src/lib/pages/LicenseContent.svelte +1 -2
  16. package/src/lib/pages/PrivacyDashboard.svelte +0 -1
  17. package/src/lib/styles/css/default.css +22 -0
  18. package/src/lib/styles/global.min.css +1 -1
  19. package/src/lib/utils/redirect.js +52 -0
  20. package/src/lib/utils/utm.js +1 -0
  21. package/src/routes/consultation/+page.svelte +16 -2
  22. package/src/routes/contact/+page.svelte +2 -4
  23. package/src/routes/links/+page.svelte +2 -4
  24. package/src/routes/posts/+page.svelte +2 -4
  25. package/src/routes/privacy-rights/+page.svelte +2 -4
  26. package/static/sitemap.xml +4 -4
  27. package/tests/unit/{unregisterServiceWorker.test.js → client/lib/unregisterServiceWorker.test.js} +2 -2
  28. package/tests/unit/client/lib/utils/redirect.test.js +80 -0
  29. package/tests/unit/{utm.test.js → client/lib/utils/utm.test.js} +1 -1
  30. package/tests/unit/{routes → client/routes}/page.svelte.test.js +2 -2
  31. package/tests/unit/{checkEnv.test.js → server/checkEnv.test.js} +2 -2
  32. package/tests/unit/{checkVersions.test.js → server/checkVersions.test.js} +2 -2
  33. package/tests/{internal → unit/server/internal}/auditCoverage.test.js +13 -6
  34. package/tests/unit/{lib → server/lib}/utils/purify.test.js +2 -2
  35. package/vitest.config.client.js +1 -4
  36. package/vitest.config.server.js +1 -1
  37. package/netlify/edge-functions/csp-report.js +0 -160
  38. package/tests/unit/csp-report.test.js +0 -81
package/.node-version CHANGED
@@ -1 +1 @@
1
- 24.3.0
1
+ 24.4.0
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 24.3.0
1
+ 24.4.0
package/CHANGELOG.md CHANGED
@@ -22,6 +22,89 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
22
22
 
23
23
  ---
24
24
 
25
+ ## [1.15.1] - 2025-07-12
26
+
27
+ ### Added
28
+
29
+ - Added `Report-To` header in `src/hooks.server.js` to support modern CSP reporting
30
+
31
+ ### Changed
32
+
33
+ - Bumped project version to `1.15.1`
34
+ - Updated CSP report URL in `src/hooks.server.js` to use external endpoint
35
+ - Updated `sitemap.xml` to reflect latest site structure
36
+ - Updated `.node-version` and `.nvmrc` to `v24.4.0`
37
+ - Cleaned up `netlify.toml`:
38
+ - Removed `[[edge_functions]]` block
39
+ - Confirmed `ENV_MODE` remains for internal tooling
40
+ - Updated to latest versions:
41
+ - `@eslint/js` to `^9.31.0`
42
+ - `@playwright/test` to `^1.54.1`
43
+ - `@sveltejs/kit` to `2.22.5`
44
+ - `@sveltejs/vite-plugin-svelte` to `6.0.0`
45
+ - `eslint` to `^9.31.0`
46
+ - `eslint-plugin-jsdoc` to `^51.3.4`
47
+ - `playwright` to `^1.54.1`
48
+ - `posthog-js` to `^1.257.0`
49
+ - `stylelint` to `^16.21.1`
50
+ - `svelte` to `5.35.6`
51
+ - `vite` to `7.0.4`
52
+
53
+ ### Removed
54
+
55
+ - Deleted `/netlify/edge-functions/` and `csp-report.js` (CSP report handling is now in its own project)
56
+ - Removed `tests/unit/server/csp-report.test.js` from project, as CSP endpoint has been relocated
57
+
58
+ ---
59
+
60
+ ## [1.15.0] - 2025-07-01
61
+
62
+ ### Added
63
+
64
+ - `redirect.js` utility to handle browser-aware redirects with fallback logic for Firefox.
65
+ - Unit test for `redirect.js` under `tests/unit/client/lib/utils/redirect.test.js`.
66
+ - `/consultation` redirect route to `utm.js` UTM-tracking logic.
67
+ - Redirect from `/foss` to `/foss-spotlight` in `_redirects`.
68
+ - `scripts/testRedirect.js` to verify Netlify/SvelteKit trailing slash redirect behavior.
69
+ - `test:redirects` script in package.json to trigger `scripts/testRedirects.js`
70
+ - Logic to suppress `rel="noopener noreferrer"` on internal redirects in `HeaderHome.svelte` and `HeaderDefault.svelte`
71
+ - `redirect` flag to navigation metadata to distinguish internal redirect behavior in `HeaderHome.svelte` and `HeaderDefault.svelte`
72
+
73
+ ### Changed
74
+
75
+ - Bumped version to **v1.15.0**
76
+ - Restructured unit tests:
77
+ - Moved `purify.test.js` to `tests/unit/client/lib/utils/`
78
+ - Moved `utm.test.js` to `tests/unit/client/lib/utils/`
79
+ - Moved `unregisterServiceWorker.test.js` to `tests/unit/client/lib/utils/`
80
+ - Moved `page.svelte.test.js` to `tests/unit/client/routes/`
81
+ - Moved `checkEnv.test.js`, `checkVersions.test.js`, and `csp-report.test.js` to `tests/unit/server/`
82
+ - Moved `auditCoverage.test.js` to `tests/unit/server/internal/`
83
+ - Refactored `_redirects` file:
84
+ - Removed trailing slashes to match SvelteKit/Netlify conventions.
85
+ - Updated `RedirectPage.svelte` to use `redirectWithBrowserAwareness()` for better cross-browser redirect behavior.
86
+ - Refactored all relevant `+page.svelte` files to remove local redirect timeouts and centralize logic in `RedirectPage`.
87
+ - Updated `vitest.config.client.js` and `vitest.config.server.js` to:
88
+ - Reflect new directory structure
89
+ - Properly assign `jsdom` for client-side tests and `node` for server-side tests
90
+ - `/consultation`, `/contact`, `/links`, `/posts`, and `/privacy-rights` `+page.svelte` files updated to capture UTM parameters in a privacy-preserving manner.
91
+ - Moved inline styles from `RedirectPage.svelte` to `src/lib/styles/css/default.css`, including `@keyframes spin` animation used by `.loading-spinner`
92
+ - Removed unnecessary `rel` attribute from internal links in `AboutContent.svelte`, `LicenseContent.svelte`, and `PrivacyDashboard.svelte`
93
+ - Updated project README with revised directory structure reflecting separate client/server test folders.
94
+ - Upgraded dependencies:
95
+ - `globals` `^16.2.0` → `^16.3.0`
96
+
97
+ ### Fixed
98
+
99
+ - Firefox-specific issue where delayed `window.location.replace()` triggered a new tab instead of redirecting in the same window — now handled by bypassing the delay in Firefox.
100
+ - Prevented server-context tests from breaking due to `window` usage by scoping them to client-only environments.
101
+
102
+ ## Removed
103
+
104
+ - `head:flatten` and `head:validate` scripts in package.json, as the `_headers` file has been deprecated
105
+
106
+ ---
107
+
25
108
  ## [1.14.3] - 2025-06-30
26
109
 
27
110
  ### Added
@@ -32,6 +115,7 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
32
115
 
33
116
  ### Changed
34
117
 
118
+ - Bumped version to **v1.14.3**
35
119
  - Refactored redirect logic in multiple pages to integrate UTM-aware analytics
36
120
  - All redirect pages now consistently open in a new browser tab using `<a>` fallback
37
121
  - Enhanced `utm.js` logic to support campaign identification for `/contact`, `/links`, `/posts`, and `/privacy-rights`
@@ -566,8 +650,9 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
566
650
 
567
651
  <!-- Link references -->
568
652
 
569
- [Unreleased]: https://github.com/netwk-pro/netwk-pro.github.io/compare/v1.14.3...HEAD
570
- [1.14.3]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.3
653
+ [Unreleased]: https://github.com/netwk-pro/netwk-pro.github.io/compare/v1.15.1...HEAD
654
+ [1.15.1]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.15.1
655
+ [1.15.0]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.15.0
571
656
  [1.14.2]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.2
572
657
  [1.14.1]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.1
573
658
  [1.14.0]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.0
package/README.md CHANGED
@@ -92,10 +92,12 @@ This project follows the principles of [Keep a Changelog](https://keepachangelog
92
92
  │ ├── robots.txt # SEO: allow/disallow crawlers
93
93
  │ └── sitemap.xml # SEO: full site map
94
94
  ├── tests/
95
- │ ├── e2e/ # Playwright end-to-end tests
96
- ├── internal/ # Internal audit/test helpers
97
- │ └── auditCoverage.test.js # Warns about untested source modules
98
- └── unit/ # Vitest unit tests
95
+ │ ├── e2e/ # Playwright end-to-end tests
96
+ └── unit/ # Vitest unit tests
97
+ ├── client/ # Client-side (jsdom) unit tests
98
+ ├── server/ # Server-side (node) unit tests
99
+ │ │ └── internal/ # Internal audit/test helpers
100
+ │ │ └── auditCoverage.test.js # Warns about untested source modules
99
101
  ├── _redirects # Netlify redirect rules
100
102
  ├── CHANGELOG.md # Chronological record of notable project changes
101
103
  ├── netlify.toml # Netlify configuration
package/_redirects CHANGED
@@ -3,13 +3,15 @@ https://www.netwk.pro/* https://netwk.pro/:splat 301
3
3
 
4
4
  # Redirect /privacy-policy (and variants) to /privacy
5
5
  /privacy-policy /privacy 301
6
- /privacy-policy/ /privacy 301
7
6
  /privacy-policy/* /privacy/:splat 301
8
7
 
9
8
  # Redirect /legal (and variants) to /license
10
9
  /legal /license 301
11
- /legal/ /license 301
12
10
  /legal/* /license/:splat 301
13
11
 
12
+ # Redirect /foss (and variants) to /foss-spotlight
13
+ /foss /foss-spotlight 301
14
+ /foss/* /foss-spotlight/:splat 301
15
+
14
16
  # Redirect for Netlify function proxy
15
17
  /api/* /.netlify/functions/:splat 200
package/netlify.toml CHANGED
@@ -2,7 +2,6 @@
2
2
  command = "npm run build"
3
3
 
4
4
  [build.environment]
5
- NODE_VERSION = "22"
6
5
  ENV_MODE = "prod"
7
6
 
8
7
  [dev]
@@ -10,10 +9,6 @@
10
9
  targetPort = 5173
11
10
  port = 8888
12
11
 
13
- [[edge_functions]]
14
- path = "/api/csp-report"
15
- function = "csp-report"
16
-
17
12
  [[plugins]]
18
13
  package = "netlify-plugin-submit-sitemap"
19
14
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@networkpro/web",
3
3
  "private": false,
4
- "version": "1.14.3",
4
+ "version": "1.15.1",
5
5
  "description": "Locking Down Networks, Unlocking Confidence™ | Security, Networking, Privacy — Network Pro Strategies",
6
6
  "keywords": [
7
7
  "advisory",
@@ -60,6 +60,7 @@
60
60
  "test:watch": "vitest --config vitest.config.client.js --watch",
61
61
  "test:coverage": "npm run test:client -- --run --coverage && npm run test:server -- --run --coverage",
62
62
  "test:e2e": "npx playwright test --retries=1",
63
+ "test:redirects": "node scripts/testRedirects.js",
63
64
  "coverage:client": "node scripts/openReport.js client",
64
65
  "coverage:server": "node scripts/openReport.js server",
65
66
  "coverage:open": "npm run coverage:client && npm run coverage:server",
@@ -74,44 +75,42 @@
74
75
  "lhci": "lhci",
75
76
  "lhci:run": "lhci autorun --config=.lighthouserc.cjs",
76
77
  "audit:coverage": "vitest run tests/internal/auditCoverage.test.js",
77
- "head:flatten": "node scripts/flattenHeaders.js",
78
- "head:validate": "node scripts/validateHeaders.js",
79
78
  "postinstall": "npm run check:node"
80
79
  },
81
80
  "dependencies": {
82
81
  "dompurify": "^3.2.6",
83
- "posthog-js": "^1.256.0",
82
+ "posthog-js": "^1.257.0",
84
83
  "semver": "^7.7.2",
85
- "svelte": "5.34.9"
84
+ "svelte": "5.35.6"
86
85
  },
87
86
  "devDependencies": {
88
87
  "@eslint/compat": "^1.3.1",
89
- "@eslint/js": "^9.30.0",
88
+ "@eslint/js": "^9.31.0",
90
89
  "@lhci/cli": "^0.15.1",
91
- "@playwright/test": "^1.53.2",
90
+ "@playwright/test": "^1.54.1",
92
91
  "@sveltejs/adapter-netlify": "^5.0.2",
93
- "@sveltejs/kit": "2.22.2",
94
- "@sveltejs/vite-plugin-svelte": "5.1.0",
92
+ "@sveltejs/kit": "2.22.5",
93
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
95
94
  "@testing-library/jest-dom": "^6.6.3",
96
95
  "@testing-library/svelte": "^5.2.8",
97
96
  "@vitest/coverage-v8": "^3.2.4",
98
97
  "autoprefixer": "^10.4.21",
99
98
  "browserslist": "^4.25.1",
100
- "eslint": "^9.30.0",
99
+ "eslint": "^9.31.0",
101
100
  "eslint-config-prettier": "^10.1.5",
102
- "eslint-plugin-jsdoc": "^51.3.1",
101
+ "eslint-plugin-jsdoc": "^51.3.4",
103
102
  "eslint-plugin-svelte": "^3.10.1",
104
- "globals": "^16.2.0",
103
+ "globals": "^16.3.0",
105
104
  "jsdom": "^26.1.0",
106
105
  "lightningcss": "^1.30.1",
107
106
  "markdownlint": "^0.38.0",
108
107
  "markdownlint-cli2": "^0.18.1",
109
108
  "mdsvex": "^0.12.6",
110
- "playwright": "^1.53.2",
109
+ "playwright": "^1.54.1",
111
110
  "postcss": "^8.5.6",
112
111
  "prettier": "^3.6.2",
113
112
  "prettier-plugin-svelte": "^3.4.0",
114
- "stylelint": "^16.21.0",
113
+ "stylelint": "^16.21.1",
115
114
  "stylelint-config-html": "^1.1.0",
116
115
  "stylelint-config-recommended": "^16.0.0",
117
116
  "stylelint-order": "^7.0.0",
@@ -119,7 +118,7 @@
119
118
  "svelte-eslint-parser": "^1.2.0",
120
119
  "svelte-preprocess": "^6.0.3",
121
120
  "typescript": "^5.8.3",
122
- "vite": "6.3.5",
121
+ "vite": "^7.0.4",
123
122
  "vite-plugin-lightningcss": "^0.0.5",
124
123
  "vite-tsconfig-paths": "^5.1.4",
125
124
  "vitest": "^3.2.4"
@@ -0,0 +1,84 @@
1
+ /* ==========================================================================
2
+ scripts/testRedirects.js
3
+
4
+ Copyright © 2025 Network Pro Strategies (Network Pro™)
5
+ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
6
+ This file is part of Network Pro.
7
+ ========================================================================== */
8
+
9
+ /**
10
+ * @file testRedirects.js
11
+ * @description Tests Netlify redirects against actual endpoints to verify
12
+ * trailing-slash behavior.
13
+ *
14
+ * @module scripts/
15
+ * @author SunDevil311
16
+ * @updated 2025-07-01
17
+ */
18
+
19
+ import https from 'https';
20
+ import { URL } from 'url';
21
+
22
+ /**
23
+ * @typedef {object} RedirectTest
24
+ * @property {string} from - The source URL to test.
25
+ * @property {string} to - The expected destination URL.
26
+ */
27
+
28
+ /** @type {RedirectTest[]} */
29
+ const urls = [
30
+ { from: 'https://netwk.pro/privacy-policy', to: 'https://netwk.pro/privacy' },
31
+ {
32
+ from: 'https://netwk.pro/privacy-policy/',
33
+ to: 'https://netwk.pro/privacy',
34
+ },
35
+ { from: 'https://netwk.pro/foss', to: 'https://netwk.pro/foss-spotlight' },
36
+ { from: 'https://netwk.pro/foss/', to: 'https://netwk.pro/foss-spotlight' },
37
+ {
38
+ from: 'https://www.netwk.pro/foss',
39
+ to: 'https://netwk.pro/foss-spotlight',
40
+ },
41
+ ];
42
+
43
+ /**
44
+ * Tests a single redirect by making a GET request and checking the status code and location header.
45
+ * @param {RedirectTest} redirect - The redirect configuration to test.
46
+ * @returns {Promise<boolean>} - Resolves to true if the redirect is correct, false otherwise.
47
+ */
48
+ function testRedirect({ from, to }) {
49
+ return new Promise((resolve) => {
50
+ const req = https.request(new URL(from), { method: 'GET' }, (res) => {
51
+ const location = res.headers.location;
52
+ const expectedPath = new URL(to).pathname;
53
+
54
+ if (res.statusCode === 301 && location === expectedPath) {
55
+ console.log(`✅ ${from} → ${location}`);
56
+ resolve(true);
57
+ } else {
58
+ console.error(
59
+ `❌ ${from} → Expected 301 to ${expectedPath}, got ${res.statusCode} to ${location}`,
60
+ );
61
+ resolve(false);
62
+ }
63
+ });
64
+
65
+ req.on('error', (err) => {
66
+ console.error(`❌ ${from} → Network error: ${err.message}`);
67
+ resolve(false);
68
+ });
69
+
70
+ req.end();
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Runs all redirect tests and exits the process with a status code reflecting success or failure.
76
+ * @returns {Promise<void>}
77
+ */
78
+ const runTests = async () => {
79
+ const results = await Promise.all(urls.map(testRedirect));
80
+ const failed = results.filter((r) => !r).length;
81
+ process.exit(failed > 0 ? 1 : 0);
82
+ };
83
+
84
+ runTests();
package/src/app.html CHANGED
@@ -63,7 +63,7 @@
63
63
  content="bx4ham0zkpvzztzu213bhpt76m9siq" />
64
64
  <!-- cspell:enable -->
65
65
 
66
- <meta name="generator" content="SvelteKit 2.22.2" />
66
+ <meta name="generator" content="SvelteKit 2.22.5" />
67
67
 
68
68
  <script src="/disableSw.js"></script>
69
69
 
@@ -25,7 +25,10 @@ export async function handle({ event, resolve }) {
25
25
  console.log('[CSP Debug] ENV_MODE:', process.env.ENV_MODE);
26
26
 
27
27
  // Determine report URI
28
- const reportUri = isProdEnvironment ? '/api/csp-report' : '/api/mock-csp';
28
+ const reportUri =
29
+ isProdEnvironment && !isTestEnvironment
30
+ ? 'https://csp.netwk.pro/.netlify/functions/csp-report'
31
+ : '/api/mock-csp';
29
32
 
30
33
  // Construct base policy
31
34
  const cspDirectives = [
@@ -40,7 +43,9 @@ export async function handle({ event, resolve }) {
40
43
  "object-src 'none';",
41
44
  "frame-ancestors 'none';",
42
45
  'upgrade-insecure-requests;',
46
+ // Report CSP violations to external endpoint hosted at csp.netwk.pro
43
47
  `report-uri ${reportUri};`,
48
+ 'report-to csp-endpoint;',
44
49
  ];
45
50
 
46
51
  // Loosen up CSP for test environments
@@ -54,6 +59,20 @@ export async function handle({ event, resolve }) {
54
59
  cspDirectives[5] = "connect-src 'self';";
55
60
  }
56
61
 
62
+ response.headers.set(
63
+ 'Report-To',
64
+ JSON.stringify({
65
+ group: 'csp-endpoint',
66
+ max_age: 10886400, // 18 weeks
67
+ endpoints: [
68
+ {
69
+ url: 'https://csp.netwk.pro/.netlify/functions/csp-report',
70
+ },
71
+ ],
72
+ include_subdomains: true,
73
+ }),
74
+ );
75
+
57
76
  response.headers.set('Content-Security-Policy', cspDirectives.join(' '));
58
77
 
59
78
  // Set other security headers
@@ -8,6 +8,7 @@ This file is part of Network Pro.
8
8
 
9
9
  <script>
10
10
  import { onMount } from 'svelte';
11
+ import { redirectWithBrowserAwareness } from '$lib/utils/redirect.js';
11
12
  import FullWidthSection from '$lib/components/FullWidthSection.svelte';
12
13
 
13
14
  export let to;
@@ -15,16 +16,7 @@ This file is part of Network Pro.
15
16
  export let delay = 3;
16
17
 
17
18
  onMount(() => {
18
- if (!to) {
19
- console.warn('⛔ No redirect target provided');
20
- return;
21
- }
22
-
23
- console.log('✅ Starting redirect to:', to);
24
-
25
- setTimeout(() => {
26
- window.location.href = to;
27
- }, delay * 1000);
19
+ redirectWithBrowserAwareness(to, delay);
28
20
  });
29
21
  </script>
30
22
 
@@ -47,27 +39,3 @@ This file is part of Network Pro.
47
39
  </FullWidthSection>
48
40
 
49
41
  <!-- END MAIN CONTENT -->
50
-
51
- <style>
52
- .redirect-content {
53
- text-align: center;
54
- font-family: system-ui, sans-serif;
55
- margin-top: 2rem; /* adjust as needed */
56
- }
57
-
58
- .loading-spinner {
59
- width: 48px;
60
- height: 48px;
61
- margin: 2rem auto;
62
- border: 4px solid #ddd;
63
- animation: spin 1s linear infinite;
64
- border-radius: 50%;
65
- border-top: 4px solid #ffc627;
66
- }
67
-
68
- @keyframes spin {
69
- to {
70
- transform: rotate(360deg);
71
- }
72
- }
73
- </style>
@@ -22,6 +22,8 @@ This file is part of Network Pro.
22
22
  const lhubLink = `${base}/links`;
23
23
  const fossLink = `${base}/foss-spotlight`;
24
24
  const blogLink = 'https://blog.netwk.pro';
25
+ const discussLink =
26
+ 'https://github.com/netwk-pro/netwk-pro.github.io/discussions';
25
27
 
26
28
  /**
27
29
  * Navigation link object.
@@ -30,6 +32,7 @@ This file is part of Network Pro.
30
32
  * @property {string} href - The URL or anchor the link points to.
31
33
  * @property {string} target - Specifies where to open the link (e.g., "_self" or "_blank").
32
34
  * @property {boolean} external - Whether the link points to an external resource.
35
+ * @property {boolean} [redirect=false] - Indicates whether the external link is actually an internal redirect.
33
36
  */
34
37
 
35
38
  /**
@@ -48,15 +51,17 @@ This file is part of Network Pro.
48
51
  },
49
52
  {
50
53
  label: 'discussions',
51
- href: 'https://github.com/netwk-pro/netwk-pro.github.io/discussions',
54
+ href: discussLink,
52
55
  target: PAGE.BLANK,
53
56
  external: true,
57
+ redirect: false,
54
58
  },
55
59
  {
56
60
  label: 'link hub',
57
61
  href: lhubLink,
58
62
  target: PAGE.BLANK,
59
63
  external: true,
64
+ redirect: true,
60
65
  },
61
66
  ];
62
67
 
@@ -69,8 +74,8 @@ This file is part of Network Pro.
69
74
 
70
75
  <!-- BEGIN DEFAULT HEADER -->
71
76
  <nav class="center-nav" aria-label="Site navigation">
72
- {#each nav as { label, href, target, external }, index}
73
- <a {href} {target} rel={external ? rel : undefined}>
77
+ {#each nav as { label, href, target, external, redirect }, index}
78
+ <a {href} {target} rel={!redirect && external ? rel : undefined}>
74
79
  {label}
75
80
  </a>
76
81
  {#if external}
@@ -21,6 +21,8 @@ This file is part of Network Pro.
21
21
  const fossLink = `${base}/foss-spotlight`;
22
22
  const lhubLink = `${base}/links`;
23
23
  const blogLink = 'https://blog.netwk.pro';
24
+ const discussLink =
25
+ 'https://github.com/netwk-pro/netwk-pro.github.io/discussions';
24
26
 
25
27
  /**
26
28
  * Navigation link object.
@@ -29,6 +31,7 @@ This file is part of Network Pro.
29
31
  * @property {string} href - The URL or anchor the link points to.
30
32
  * @property {string} target - Specifies where to open the link (e.g., "_self" or "_blank").
31
33
  * @property {boolean} external - Whether the link points to an external resource.
34
+ * @property {boolean} [redirect=false] - Indicates whether the external link is actually an internal redirect.
32
35
  */
33
36
 
34
37
  /**
@@ -46,15 +49,17 @@ This file is part of Network Pro.
46
49
  },
47
50
  {
48
51
  label: 'discussions',
49
- href: 'https://github.com/netwk-pro/netwk-pro.github.io/discussions',
52
+ href: discussLink,
50
53
  target: PAGE.BLANK,
51
54
  external: true,
55
+ redirect: false,
52
56
  },
53
57
  {
54
58
  label: 'link hub',
55
59
  href: lhubLink,
56
60
  target: PAGE.BLANK,
57
61
  external: true,
62
+ redirect: true,
58
63
  },
59
64
  ];
60
65
 
@@ -67,8 +72,8 @@ This file is part of Network Pro.
67
72
 
68
73
  <!-- BEGIN HOME HEADER -->
69
74
  <nav class="center-nav" aria-label="Homepage navigation">
70
- {#each nav as { label, href, target, external }, index}
71
- <a {href} {target} rel={external ? rel : undefined}>
75
+ {#each nav as { label, href, target, external, redirect }, index}
76
+ <a {href} {target} rel={!redirect && external ? rel : undefined}>
72
77
  {label}
73
78
  </a>
74
79
  {#if external}
@@ -217,7 +217,7 @@ This file is part of Network Pro.
217
217
  <div class="spacer"></div>
218
218
 
219
219
  <p>
220
- <a rel={PAGE.REL} href={contactLink} target={PAGE.BLANK}>Let's connect</a>
220
+ <a href={contactLink} target={PAGE.BLANK}>Let's connect</a>
221
221
  to discuss how we can help secure and strengthen your business today.
222
222
  </p>
223
223
 
@@ -487,8 +487,7 @@ This file is part of Network Pro.
487
487
  {:else if link.id === 'contact'}
488
488
  <p>
489
489
  The Company can be contacted via our
490
- <a rel={PAGE.REL} href={contactLink} target={PAGE.BLANK}
491
- >Contact Form</a>
490
+ <a href={contactLink} target={PAGE.BLANK}>Contact Form</a>
492
491
  or by email at:<br />
493
492
  📧 <strong>{CONTACT.EMAIL}</strong>
494
493
  </p>
@@ -237,7 +237,6 @@ This file is part of Network Pro.
237
237
  </p>
238
238
  <p>
239
239
  To exercise any of these rights, you may submit a request through our <a
240
- rel={PAGE.REL}
241
240
  href={prightsLink}
242
241
  target={PAGE.BLANK}>Privacy Rights Request Form</a
243
242
  >. Alternatively, you can email us at
@@ -580,3 +580,25 @@ footer .container {
580
580
  text-align: center;
581
581
  margin-top: 4rem;
582
582
  }
583
+
584
+ .redirect-content {
585
+ text-align: center;
586
+ font-family: system-ui, sans-serif;
587
+ margin-top: 2rem;
588
+ }
589
+
590
+ .loading-spinner {
591
+ width: 48px;
592
+ height: 48px;
593
+ margin: 2rem auto;
594
+ border: 4px solid #ddd;
595
+ animation: spin 1s linear infinite;
596
+ border-radius: 50%;
597
+ border-top: 4px solid #ffc627;
598
+ }
599
+
600
+ @keyframes spin {
601
+ to {
602
+ transform: rotate(360deg);
603
+ }
604
+ }
@@ -3,4 +3,4 @@ Copyright © 2025 Network Pro Strategies (Network Pro™)
3
3
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
4
4
  This file is part of Network Pro.
5
5
  ========================================================================== */
6
- html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#111;background-color:#ffc627}a:visited,a:visited:hover{color:#cba557}a:visited:focus,a:visited:focus-visible{color:#111!important}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#cba557}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{overflow-wrap:break-word;resize:none;white-space:normal;word-break:normal;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-family:monospace;font-size:.875rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.pgp-entry{flex-wrap:wrap;align-items:center;gap:2rem;margin-bottom:2rem;display:flex}.pgp-text{flex:2;min-width:250px}.pgp-qr{flex:1;min-width:150px}.obtainium-direct-label{margin:.25rem 0 .75rem;font-weight:700}.obtainium-manual-label{margin-top:.75rem;font-weight:700}.obtainium-img{width:185px;height:55px;margin-bottom:.25rem}.obtainium-margin{margin-left:4px}.obtainium-fa-down{color:#ffc627;margin-left:4px}.obtainium-icon{width:50px;height:50px}.proton-img{width:168px;height:24px}.redirect-text{text-align:center;margin-top:4rem}
6
+ html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#111;background-color:#ffc627}a:visited,a:visited:hover{color:#cba557}a:visited:focus,a:visited:focus-visible{color:#111!important}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#cba557}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{overflow-wrap:break-word;resize:none;white-space:normal;word-break:normal;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-family:monospace;font-size:.875rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.pgp-entry{flex-wrap:wrap;align-items:center;gap:2rem;margin-bottom:2rem;display:flex}.pgp-text{flex:2;min-width:250px}.pgp-qr{flex:1;min-width:150px}.obtainium-direct-label{margin:.25rem 0 .75rem;font-weight:700}.obtainium-manual-label{margin-top:.75rem;font-weight:700}.obtainium-img{width:185px;height:55px;margin-bottom:.25rem}.obtainium-margin{margin-left:4px}.obtainium-fa-down{color:#ffc627;margin-left:4px}.obtainium-icon{width:50px;height:50px}.proton-img{width:168px;height:24px}.redirect-text{text-align:center;margin-top:4rem}.redirect-content{text-align:center;margin-top:2rem;font-family:system-ui,sans-serif}.loading-spinner{border:4px solid #ddd;border-top-color:#ffc627;border-radius:50%;width:48px;height:48px;margin:2rem auto;animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}