diff --git a/.prettierrc b/.prettierrc
index 9358f75..782f356 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,8 +1,8 @@
{
- "semi": false,
+ "semi": true,
"singleQuote": true,
"tabWidth": 4,
- "trailingComma": "none",
+ "trailingComma": "all",
"overrides": [
{
"files": "*.yml",
diff --git a/eslint.config.js b/eslint.config.js
index 88e0620..d75f919 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,4 +1,4 @@
-import prettierConfig from 'eslint-config-prettier/flat'
+import prettierConfig from 'eslint-config-prettier/flat';
export default [
{
@@ -9,12 +9,12 @@ export default [
globals: {
window: 'readonly',
document: 'readonly',
- dataLayer: 'writable'
- }
+ dataLayer: 'writable',
+ },
},
rules: {
- 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
- }
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
+ },
},
- prettierConfig
-]
+ prettierConfig,
+];
diff --git a/scripts/build.js b/scripts/build.js
index 1e987cb..faf3d10 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -8,71 +8,71 @@ import {
readFileSync,
writeFileSync,
cpSync,
- readdirSync
-} from 'fs'
-import { join, dirname, extname } from 'path'
-import { fileURLToPath } from 'url'
-import Beasties from 'beasties'
-import { minify as minifyJs } from 'terser'
-import CleanCSS from 'clean-css'
+ readdirSync,
+} from 'fs';
+import { join, dirname, extname } from 'path';
+import { fileURLToPath } from 'url';
+import Beasties from 'beasties';
+import { minify as minifyJs } from 'terser';
+import CleanCSS from 'clean-css';
-const __dirname = dirname(fileURLToPath(import.meta.url))
-const root = join(__dirname, '..')
-const srcDir = join(root, 'src')
-const distDir = join(root, 'dist')
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const root = join(__dirname, '..');
+const srcDir = join(root, 'src');
+const distDir = join(root, 'dist');
function getFiles(dir, files = []) {
- const entries = readdirSync(dir, { withFileTypes: true })
+ const entries = readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
- const full = join(dir, e.name)
- if (e.isDirectory()) getFiles(full, files)
- else files.push(full)
+ const full = join(dir, e.name);
+ if (e.isDirectory()) getFiles(full, files);
+ else files.push(full);
}
- return files
+ return files;
}
async function main() {
// 1. Clean and copy src → dist
- rmSync(distDir, { recursive: true, force: true })
- mkdirSync(distDir, { recursive: true })
- cpSync(srcDir, distDir, { recursive: true })
+ rmSync(distDir, { recursive: true, force: true });
+ mkdirSync(distDir, { recursive: true });
+ cpSync(srcDir, distDir, { recursive: true });
- const distFiles = getFiles(distDir)
+ const distFiles = getFiles(distDir);
// 2. Minify JS
- const jsFiles = distFiles.filter((f) => extname(f) === '.js')
+ const jsFiles = distFiles.filter((f) => extname(f) === '.js');
for (const f of jsFiles) {
- const code = readFileSync(f, 'utf8')
- const result = await minifyJs(code, { format: { comments: false } })
- if (result.code) writeFileSync(f, result.code)
+ const code = readFileSync(f, 'utf8');
+ const result = await minifyJs(code, { format: { comments: false } });
+ if (result.code) writeFileSync(f, result.code);
}
// 3. Minify CSS
- const cleanCss = new CleanCSS({ level: 2 })
- const cssFiles = distFiles.filter((f) => extname(f) === '.css')
+ const cleanCss = new CleanCSS({ level: 2 });
+ const cssFiles = distFiles.filter((f) => extname(f) === '.css');
for (const f of cssFiles) {
- const code = readFileSync(f, 'utf8')
- const result = cleanCss.minify(code)
- if (!result.errors.length) writeFileSync(f, result.styles)
+ const code = readFileSync(f, 'utf8');
+ const result = cleanCss.minify(code);
+ if (!result.errors.length) writeFileSync(f, result.styles);
}
// 4. Inline critical CSS with Beasties for all HTML files (no browser; works in CI)
- const htmlFiles = distFiles.filter((f) => extname(f) === '.html')
+ const htmlFiles = distFiles.filter((f) => extname(f) === '.html');
const beasties = new Beasties({
path: distDir,
preload: 'default',
- logLevel: 'warn'
- })
+ logLevel: 'warn',
+ });
for (const htmlFile of htmlFiles) {
- const html = readFileSync(htmlFile, 'utf8')
- const inlined = await beasties.process(html)
- writeFileSync(htmlFile, inlined)
+ const html = readFileSync(htmlFile, 'utf8');
+ const inlined = await beasties.process(html);
+ writeFileSync(htmlFile, inlined);
}
- console.log('Build complete: dist/')
+ console.log('Build complete: dist/');
}
main().catch((err) => {
- console.error(err)
- process.exit(1)
-})
+ console.error(err);
+ process.exit(1);
+});
diff --git a/src/assets/css/site.css b/src/assets/css/site.css
index 00c1889..3d3cd71 100644
--- a/src/assets/css/site.css
+++ b/src/assets/css/site.css
@@ -44,9 +44,53 @@ body {
}
body {
- display: flex;
align-items: center;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ justify-content: space-between;
+}
+
+.content {
+ display: flex;
+ flex: 1;
justify-content: center;
+ align-items: center;
+}
+
+.footer {
+ color: #374151;
+ font-size: 0.94em;
+ letter-spacing: 0.01em;
+ padding: 1rem;
+ text-align: center;
+ width: 100%;
+}
+
+@media (prefers-color-scheme: dark) {
+ .footer {
+ color: #aab2bd;
+ }
+}
+
+.skip-link {
+ position: absolute;
+ top: 0;
+ left: 0;
+ padding: 0.75rem 1.25rem;
+ background: var(--accent);
+ color: var(--button-text);
+ font-weight: 600;
+ text-decoration: none;
+ z-index: 100;
+ transform: translateY(-100%);
+ transition: transform 0.2s;
+}
+
+.skip-link:focus {
+ transform: translateY(0);
+ outline: 2px solid var(--accent-light);
+ outline-offset: 2px;
}
h1 {
@@ -194,9 +238,10 @@ td:first-child {
white-space: nowrap;
}
+/* Tip colors chosen for WCAG 2.2 AAA (≥7:1 contrast) */
.tip {
background: #eef2ff;
- color: var(--accent-light);
+ color: #3730a3;
border-radius: 0.7em;
font-size: 0.98em;
padding: 0.48em 0.8em;
@@ -207,7 +252,7 @@ td:first-child {
@media (prefers-color-scheme: dark) {
.tip {
background: #232555;
- color: #a5b4fc;
+ color: #c7d2fe;
}
}
@@ -295,14 +340,6 @@ td:first-child {
color: var(--faq-a);
}
-.footer {
- margin-top: 2.5em;
- text-align: center;
- color: #bbb;
- font-size: 0.94em;
- letter-spacing: 0.01em;
-}
-
@media (width <= 600px) {
.container {
padding: 1.1rem 0.5rem 1rem;
diff --git a/src/assets/js/accordion.js b/src/assets/js/accordion.js
new file mode 100644
index 0000000..c3f2a8b
--- /dev/null
+++ b/src/assets/js/accordion.js
@@ -0,0 +1,37 @@
+// Native accessible accordion
+document.querySelectorAll('.accordion-trigger').forEach((btn) => {
+ btn.addEventListener('click', function () {
+ const section = btn.closest('.accordion-section');
+ const expanded = btn.getAttribute('aria-expanded') === 'true';
+ document.querySelectorAll('.accordion-section').forEach((s) => {
+ if (s === section) {
+ s.classList.toggle('open', !expanded);
+ btn.setAttribute('aria-expanded', String(!expanded));
+ const content = btn.nextElementSibling;
+ content.style.maxHeight = !expanded
+ ? content.scrollHeight + 40 + 'px'
+ : '0px';
+ } else {
+ s.classList.remove('open');
+ s.querySelector('.accordion-trigger').setAttribute(
+ 'aria-expanded',
+ 'false',
+ );
+ s.querySelector('.accordion-content').style.maxHeight = '0px';
+ }
+ });
+ });
+
+ // Allow arrow navigation
+ btn.addEventListener('keydown', function (e) {
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
+ const triggers = Array.from(
+ document.querySelectorAll('.accordion-trigger'),
+ );
+ let idx = triggers.indexOf(e.target);
+ if (e.key === 'ArrowDown') idx = (idx + 1) % triggers.length;
+ else idx = (idx - 1 + triggers.length) % triggers.length;
+ triggers[idx].focus();
+ }
+ });
+});
diff --git a/src/assets/js/current-year.js b/src/assets/js/current-year.js
new file mode 100644
index 0000000..00af012
--- /dev/null
+++ b/src/assets/js/current-year.js
@@ -0,0 +1,4 @@
+(function () {
+ const year = new Date().getFullYear();
+ document.getElementById('current-year').textContent = `–${year}`;
+})();
diff --git a/src/assets/js/ga-init.js b/src/assets/js/ga-init.js
index 3820ff4..5c27172 100644
--- a/src/assets/js/ga-init.js
+++ b/src/assets/js/ga-init.js
@@ -1,11 +1,11 @@
-;(function () {
- var script = document.currentScript
- var id = script && script.getAttribute('data-ga-id')
- if (!id) return
- window.dataLayer = window.dataLayer || []
+(function () {
+ var script = document.currentScript;
+ var id = script && script.getAttribute('data-ga-id');
+ if (!id) return;
+ window.dataLayer = window.dataLayer || [];
function gtag() {
- window.dataLayer.push(arguments)
+ window.dataLayer.push(arguments);
}
- gtag('js', new Date())
- gtag('config', id, { anonymize_ip: true })
-})()
+ gtag('js', new Date());
+ gtag('config', id, { anonymize_ip: true });
+})();
diff --git a/src/help/index.html b/src/help/index.html
index 959cdb6..774b832 100644
--- a/src/help/index.html
+++ b/src/help/index.html
@@ -95,283 +95,248 @@
-
-
-
Welcome to Email from mifi Ventures
-
-
Let's get your inbox ready! 📬
-
- Friendly help for setting up your email—works with Outlook,
- Apple Mail, Thunderbird, phones, and more.
-
-
+
Skip to main content
+
+
+
+ Welcome to Email from mifi Ventures
+
+
Let's get your inbox ready! 📬
+
+ Friendly help for setting up your email—works with
+ Outlook, Apple Mail, Thunderbird, phones, and more.
+
+
-
- General Settings (All Clients)
-
-
- | Email Address |
- your.name@yourdomain.com |
-
-
- | Username |
- your.name@yourdomain.com |
-
-
- | Password |
- (your email password) |
-
-
- | Incoming Server |
- mail.mifi.holdings |
-
-
- | Outgoing Server |
- mail.mifi.holdings |
-
-
- | IMAP Port |
- 993 (SSL/TLS) |
-
-
- | POP3 Port |
- 995 (SSL/TLS) |
-
-
- | SMTP Port |
- 587 (STARTTLS) or 465 (SSL/TLS) |
-
-
- | Authentication |
- Required (use same as incoming) |
-
-
- | Encryption |
- SSL/TLS or STARTTLS |
-
-
- Tip: Always use your full email address as your
- username!
-
-
-
-
-
-
-
-
- - Go to File → Add Account
- - Enter your full email address
- -
- Choose Advanced options → check “Set up
- manually”
-
- - Select IMAP (recommended) or POP
- -
- Incoming server:
-
mail.mifi.holdings, port
- 993 (SSL/TLS)
-
- -
- Outgoing server:
-
mail.mifi.holdings, port
- 587 (STARTTLS) or 465 (SSL/TLS)
-
- -
- Username: full email address; Password: your
- password
-
- - Click Connect
-
-
If sending fails, make sure “Require logon using
- SPA” is unchecked.
-
+ General Settings (All Clients)
+
+
+ | Email Address |
+ your.name@yourdomain.com |
+
+
+ | Username |
+ your.name@yourdomain.com |
+
+
+ | Password |
+ (your email password) |
+
+
+ | Incoming Server |
+ mail.mifi.holdings |
+
+
+ | Outgoing Server |
+ mail.mifi.holdings |
+
+
+ | IMAP Port |
+ 993 (SSL/TLS) |
+
+
+ | POP3 Port |
+ 995 (SSL/TLS) |
+
+
+ | SMTP Port |
+ 587 (STARTTLS) or 465 (SSL/TLS) |
+
+
+ | Authentication |
+ Required (use same as incoming) |
+
+
+ | Encryption |
+ SSL/TLS or STARTTLS |
+
+
+ Tip: Always use your full email address as your
+ username!
-
-
-
-
-
- -
- Add Account → Other Mail Account
-
- - Enter your name, email, and password
- -
- Incoming/Outgoing server:
-
mail.mifi.holdings
-
- -
- IMAP port: 993 (SSL); SMTP port:
- 587 (STARTTLS) or 465 (SSL)
-
- - Use full email address for username
-
-
-
-
-
-
-
-
-
- - Menu → Account Settings → Add Mail Account
- - Fill in your name, email, and password
- -
- Click “Configure manually” and use settings
- above
-
-
-
-
-
-
-
-
-
-
- - Add Account → Other
- - Enter your email and password
- -
- Manual setup:
mail.mifi.holdings,
- correct ports, SSL/TLS required
-
- -
- Gmail app: tap profile → Add account → Other,
- fill in details, use IMAP
-
-
-
-
-
-
-
-
-
-
Q: My email won’t send?
-
- Check that you’re using your full email address for
- both incoming and outgoing username, and that the
- port is 587 or 465.
+
+
+
+
+
+
+ - Go to File → Add Account
+ - Enter your full email address
+ -
+ Choose Advanced options → check “Set
+ up manually”
+
+ - Select IMAP (recommended) or POP
+ -
+ Incoming server:
+
mail.mifi.holdings, port
+ 993 (SSL/TLS)
+
+ -
+ Outgoing server:
+
mail.mifi.holdings, port
+ 587 (STARTTLS) or
+ 465 (SSL/TLS)
+
+ -
+ Username: full email address; Password: your
+ password
+
+ - Click Connect
+
+
If sending fails, make sure “Require logon
+ using SPA” is unchecked.
- Q: SSL/TLS errors?
-
- Ensure SSL or STARTTLS is enabled for both incoming
- and outgoing mail.
-
- Q: Still stuck?
-
- Contact
-
postmaster@mifi.holdings.
- Please include any error messages, your mail app,
- and a screenshot if you can!
-
-
-
+
-
-
-
-
-
- -
- IMAP syncs your mail everywhere—choose
- IMAP unless you know you want POP3.
-
- -
- Your login is always your
- full email address.
-
- -
- Check your Spam/Junk folder for misfiled good
- emails.
-
- -
- Advanced: IMAP path prefix =
- (leave blank); SMTP authentication is
- always required.
-
-
-
-
-
-
+
+
+
+
+
+ -
+ Add Account → Other Mail Account
+
+ - Enter your name, email, and password
+ -
+ Incoming/Outgoing server:
+
mail.mifi.holdings
+
+ -
+ IMAP port: 993 (SSL); SMTP port:
+ 587 (STARTTLS) or 465 (SSL)
+
+ - Use full email address for username
+
+
+
+
+
+
+
+
+
+ -
+ Menu → Account Settings → Add Mail Account
+
+ - Fill in your name, email, and password
+ -
+ Click “Configure manually” and use settings
+ above
+
+
+
+
+
+
+
+
+
+
+ - Add Account → Other
+ - Enter your email and password
+ -
+ Manual setup:
+
mail.mifi.holdings, correct
+ ports, SSL/TLS required
+
+ -
+ Gmail app: tap profile → Add account →
+ Other, fill in details, use IMAP
+
+
+
+
+
+
+
+
+
+
Q: My email won’t send?
+
+ Check that you’re using your full email address
+ for both incoming and outgoing username, and
+ that the port is 587 or 465.
+
+
Q: SSL/TLS errors?
+
+ Ensure SSL or STARTTLS is enabled for both
+ incoming and outgoing mail.
+
+
Q: Still stuck?
+
+ Contact
+
postmaster@mifi.holdings.
+ Please include any error messages, your mail
+ app, and a screenshot if you can!
+
+
+
+
+
+
+
+
+
+ -
+ IMAP syncs your mail
+ everywhere—choose IMAP unless you know you
+ want POP3.
+
+ -
+ Your login is always your
+ full email address.
+
+ -
+ Check your Spam/Junk folder for misfiled
+ good emails.
+
+ -
+ Advanced: IMAP path prefix =
+ (leave blank); SMTP authentication is
+ always required.
+
+
+
+
+
+
-
+
+
+