deploy: zensical build from 4421378
This commit is contained in:
parent
6908bc82a9
commit
7e42c3854d
19 changed files with 1044 additions and 12 deletions
6
404.html
6
404.html
|
|
@ -62,6 +62,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,799 @@
|
||||||
boot();
|
boot();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Action Points — Interactive Checklist
|
||||||
|
═══════════════════════════════════════════════════════════════════════ */
|
||||||
|
;(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// ── Feature flag ───────────────────────────────────
|
||||||
|
// Disabled via zensical.toml: [project.extra.action_points] enabled = false
|
||||||
|
// (surfaced as window.TOA_ACTION_POINTS_ENABLED by overrides/main.html).
|
||||||
|
// When off, bail before ANY listeners, observers, storage, or per-click work
|
||||||
|
// are wired up — checkboxes fall back to Material's read-only rendering.
|
||||||
|
if (window.TOA_ACTION_POINTS_ENABLED === false) return;
|
||||||
|
|
||||||
|
// ── Constants ──────────────────────────────────────
|
||||||
|
var STORAGE_KEY = "toa-action-points";
|
||||||
|
var VERSION = 2;
|
||||||
|
var REDUCED_MOTION = window.matchMedia(
|
||||||
|
"(prefers-reduced-motion: reduce)"
|
||||||
|
).matches;
|
||||||
|
|
||||||
|
function todayStr() {
|
||||||
|
return new Date().toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Storage ────────────────────────────────────────
|
||||||
|
// In-memory write-through cache of the parsed store. A single checkbox toggle
|
||||||
|
// calls getStorage() ~15x; without this each call re-parsed the whole JSON
|
||||||
|
// blob (and saveStorage re-stringified it 3x). The cache keeps reads O(1) and
|
||||||
|
// is kept in sync on every save, so reflow — not parsing — dominates a toggle.
|
||||||
|
var _storeCache = null;
|
||||||
|
|
||||||
|
function getStorage() {
|
||||||
|
if (_storeCache) return _storeCache;
|
||||||
|
try {
|
||||||
|
var raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
_storeCache = _fresh();
|
||||||
|
return _storeCache;
|
||||||
|
}
|
||||||
|
var data = JSON.parse(raw);
|
||||||
|
if (data.version !== VERSION) {
|
||||||
|
_storeCache = _migrate(data);
|
||||||
|
return _storeCache;
|
||||||
|
}
|
||||||
|
_storeCache = data;
|
||||||
|
return _storeCache;
|
||||||
|
} catch (e) {
|
||||||
|
_storeCache = _fresh();
|
||||||
|
return _storeCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _fresh() {
|
||||||
|
return { version: VERSION, pages: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _migrate(old) {
|
||||||
|
// v1 → v2: add completedDates tracking
|
||||||
|
var data = _fresh();
|
||||||
|
if (old.pages) {
|
||||||
|
Object.keys(old.pages).forEach(function (path) {
|
||||||
|
var op = old.pages[path];
|
||||||
|
data.pages[path] = {
|
||||||
|
lastUpdated: op.lastUpdated || null,
|
||||||
|
checked: op.checked || [],
|
||||||
|
completedDates: {},
|
||||||
|
_checked: op._checked,
|
||||||
|
_total: op._total,
|
||||||
|
};
|
||||||
|
// Seed completedDates from checked array (assume checked=today)
|
||||||
|
var td = todayStr();
|
||||||
|
(op.checked || []).forEach(function (label) {
|
||||||
|
data.pages[path].completedDates[label] = [td];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
saveStorage(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveStorage(data) {
|
||||||
|
_storeCache = data; // keep the in-memory cache authoritative
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||||
|
} catch (e) {
|
||||||
|
/* quota exceeded — silently drop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPagePath() {
|
||||||
|
var p = location.pathname;
|
||||||
|
if (p.slice(-1) !== "/") p += "/";
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNormLabel(li) {
|
||||||
|
// Clone li, remove the task-list-control label, get remaining text
|
||||||
|
var clone = li.cloneNode(true);
|
||||||
|
var ctrl = clone.querySelector(".task-list-control");
|
||||||
|
if (ctrl) ctrl.remove();
|
||||||
|
return (clone.textContent || "").replace(/\s+/g, " ").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageData(path) {
|
||||||
|
var store = getStorage();
|
||||||
|
if (!store.pages[path]) {
|
||||||
|
store.pages[path] = { checked: [], completedDates: {}, lastUpdated: null };
|
||||||
|
}
|
||||||
|
if (!store.pages[path].completedDates) {
|
||||||
|
store.pages[path].completedDates = {};
|
||||||
|
}
|
||||||
|
return store.pages[path];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageChecked(path, label, checked) {
|
||||||
|
var store = getStorage();
|
||||||
|
if (!store.pages[path]) {
|
||||||
|
store.pages[path] = { checked: [], completedDates: {}, lastUpdated: null };
|
||||||
|
}
|
||||||
|
var page = store.pages[path];
|
||||||
|
if (!page.completedDates) page.completedDates = {};
|
||||||
|
if (!page.completedDates[label]) page.completedDates[label] = [];
|
||||||
|
|
||||||
|
var td = todayStr();
|
||||||
|
if (checked) {
|
||||||
|
if (page.checked.indexOf(label) === -1) page.checked.push(label);
|
||||||
|
if (page.completedDates[label].indexOf(td) === -1) {
|
||||||
|
page.completedDates[label].push(td);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
page.checked = page.checked.filter(function (l) {
|
||||||
|
return l !== label;
|
||||||
|
});
|
||||||
|
page.completedDates[label] = page.completedDates[label].filter(function (d) {
|
||||||
|
return d !== td;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
page.lastUpdated = new Date().toISOString();
|
||||||
|
saveStorage(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist page summary (checked count, total) for nav aggregation
|
||||||
|
function savePageSummary(path, checkedCount, total) {
|
||||||
|
var store = getStorage();
|
||||||
|
if (!store.pages[path]) {
|
||||||
|
store.pages[path] = { checked: [], lastUpdated: null };
|
||||||
|
}
|
||||||
|
store.pages[path]._checked = checkedCount;
|
||||||
|
store.pages[path]._total = total;
|
||||||
|
// Don't bump lastUpdated for summary-only writes
|
||||||
|
saveStorage(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageSummary(path) {
|
||||||
|
var store = getStorage();
|
||||||
|
var page = store.pages[path];
|
||||||
|
if (!page || page._total === undefined) return null;
|
||||||
|
return { checked: page._checked || 0, total: page._total || 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build-time totals manifest ─────────────────────
|
||||||
|
// toa-totals.json maps page-URL -> task count, generated at build time
|
||||||
|
// (see main.py). Seeding _total for every page lets nav badges show
|
||||||
|
// correct totals on FIRST load, before the user has visited each page.
|
||||||
|
// Fetched once per session; cached by the browser thereafter.
|
||||||
|
var _totalsLoaded = false;
|
||||||
|
|
||||||
|
function seedTotalsFromManifest(cb) {
|
||||||
|
if (_totalsLoaded) {
|
||||||
|
cb && cb(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_totalsLoaded = true; // mark immediately so concurrent navs don't double-fetch
|
||||||
|
|
||||||
|
// Resolve the manifest relative to the site root. The script tag for
|
||||||
|
// extra.js is loaded from <base>/javascripts/extra.js, so derive the URL
|
||||||
|
// from document scripts to stay correct under any base_url.
|
||||||
|
var url = "javascripts/toa-totals.json";
|
||||||
|
try {
|
||||||
|
var scripts = document.querySelectorAll('script[src*="javascripts/extra.js"]');
|
||||||
|
if (scripts.length) {
|
||||||
|
url = scripts[0].src.replace(/extra\.js.*$/, "toa-totals.json");
|
||||||
|
} else {
|
||||||
|
// Fallback: resolve from origin
|
||||||
|
url = new URL("javascripts/toa-totals.json", location.origin + "/").href;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* use relative fallback */
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, { cache: "force-cache" })
|
||||||
|
.then(function (r) {
|
||||||
|
return r.ok ? r.json() : null;
|
||||||
|
})
|
||||||
|
.then(function (totals) {
|
||||||
|
if (!totals) {
|
||||||
|
cb && cb(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var store = getStorage();
|
||||||
|
var changed = false;
|
||||||
|
Object.keys(totals).forEach(function (p) {
|
||||||
|
var path = p.slice(-1) === "/" ? p : p + "/";
|
||||||
|
if (!store.pages[path]) {
|
||||||
|
store.pages[path] = { checked: [], completedDates: {}, lastUpdated: null };
|
||||||
|
}
|
||||||
|
// Only seed _total; never clobber _checked (real progress wins).
|
||||||
|
if (store.pages[path]._total !== totals[p]) {
|
||||||
|
store.pages[path]._total = totals[p];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (store.pages[path]._checked === undefined) {
|
||||||
|
store.pages[path]._checked = (store.pages[path].checked || []).length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) saveStorage(store);
|
||||||
|
cb && cb(true);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
cb && cb(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Checkbox initialization ────────────────────────
|
||||||
|
function initCheckboxes() {
|
||||||
|
var path = getPagePath();
|
||||||
|
var pageData = getPageData(path);
|
||||||
|
|
||||||
|
var items = document.querySelectorAll("li.task-list-item");
|
||||||
|
if (!items.length) return { checked: 0, total: 0 };
|
||||||
|
|
||||||
|
var total = items.length;
|
||||||
|
var checkedCount = 0;
|
||||||
|
|
||||||
|
items.forEach(function (li) {
|
||||||
|
var cb = li.querySelector('input[type="checkbox"]');
|
||||||
|
if (!cb) return;
|
||||||
|
|
||||||
|
var label = getNormLabel(li);
|
||||||
|
var isChecked = pageData.checked.indexOf(label) !== -1;
|
||||||
|
|
||||||
|
// Enable checkbox
|
||||||
|
cb.disabled = false;
|
||||||
|
cb.checked = isChecked;
|
||||||
|
if (isChecked) checkedCount++;
|
||||||
|
|
||||||
|
cb.addEventListener("change", function () {
|
||||||
|
var now = cb.checked;
|
||||||
|
setPageChecked(path, label, now);
|
||||||
|
|
||||||
|
// Recompute & re-render
|
||||||
|
var counts = computeHeadingCounts();
|
||||||
|
savePageSummary(path, counts.pageChecked, counts.pageTotal);
|
||||||
|
renderHeadingPills(counts);
|
||||||
|
updatePageHeading(counts);
|
||||||
|
updateNavBadges();
|
||||||
|
checkCompletions(counts);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { checked: checkedCount, total: total };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Heading counters ───────────────────────────────
|
||||||
|
function computeHeadingCounts() {
|
||||||
|
var article = document.querySelector("article");
|
||||||
|
if (!article) return { groups: [], pageChecked: 0, pageTotal: 0 };
|
||||||
|
|
||||||
|
var groups = [];
|
||||||
|
var current = null;
|
||||||
|
|
||||||
|
// Tree-walk: assign items to the nearest preceding h2/h3 in document order.
|
||||||
|
// This is immune to compareDocumentPosition quirks.
|
||||||
|
function walk(node) {
|
||||||
|
if (node.nodeType !== 1) return; // skip non-elements
|
||||||
|
var tag = node.tagName;
|
||||||
|
if (tag === "H2" || tag === "H3") {
|
||||||
|
current = node;
|
||||||
|
groups.push({ heading: node, items: [], level: parseInt(tag[1]) });
|
||||||
|
} else if (tag === "LI" && node.classList.contains("task-list-item")) {
|
||||||
|
if (current) {
|
||||||
|
groups[groups.length - 1].items.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recurse into children
|
||||||
|
var children = node.childNodes;
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
walk(children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk article's children (skip article itself to avoid h1)
|
||||||
|
var kids = article.childNodes;
|
||||||
|
for (var i = 0; i < kids.length; i++) {
|
||||||
|
walk(kids[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute checked counts
|
||||||
|
var pageChecked = 0;
|
||||||
|
var pageTotal = 0;
|
||||||
|
groups.forEach(function (g) {
|
||||||
|
var checked = 0;
|
||||||
|
g.items.forEach(function (li) {
|
||||||
|
var cb = li.querySelector('input[type="checkbox"]');
|
||||||
|
if (cb && cb.checked) checked++;
|
||||||
|
});
|
||||||
|
g.checked = checked;
|
||||||
|
g.total = g.items.length;
|
||||||
|
pageChecked += checked;
|
||||||
|
pageTotal += g.items.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { groups: groups, pageChecked: pageChecked, pageTotal: pageTotal };
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeadingPills(counts) {
|
||||||
|
// Clear previous data attributes from all article headings
|
||||||
|
document.querySelectorAll("article h2, article h3").forEach(function (h) {
|
||||||
|
h.removeAttribute("data-toa-count");
|
||||||
|
h.removeAttribute("data-toa-complete");
|
||||||
|
h.classList.remove("toa-heading--complete");
|
||||||
|
});
|
||||||
|
|
||||||
|
counts.groups.forEach(function (g) {
|
||||||
|
if (g.total === 0) return;
|
||||||
|
g.heading.setAttribute("data-toa-count", g.checked + "/" + g.total);
|
||||||
|
if (g.checked === g.total) {
|
||||||
|
g.heading.setAttribute("data-toa-complete", "true");
|
||||||
|
g.heading.classList.add("toa-heading--complete");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageHeading(counts) {
|
||||||
|
var h1 = document.querySelector("article h1");
|
||||||
|
if (!h1) return;
|
||||||
|
h1.removeAttribute("data-toa-count");
|
||||||
|
h1.removeAttribute("data-toa-complete");
|
||||||
|
h1.classList.remove("toa-heading--complete");
|
||||||
|
|
||||||
|
if (counts.pageTotal === 0) return;
|
||||||
|
|
||||||
|
h1.setAttribute("data-toa-count", counts.pageChecked + "/" + counts.pageTotal);
|
||||||
|
if (counts.pageChecked === counts.pageTotal) {
|
||||||
|
h1.setAttribute("data-toa-complete", "true");
|
||||||
|
h1.classList.add("toa-heading--complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Completion detection ───────────────────────────
|
||||||
|
var _completedHeadings = {};
|
||||||
|
var _pageWasComplete = false;
|
||||||
|
|
||||||
|
function checkCompletions(counts) {
|
||||||
|
var path = getPagePath();
|
||||||
|
|
||||||
|
// Check heading completions
|
||||||
|
counts.groups.forEach(function (g) {
|
||||||
|
if (g.total === 0) return;
|
||||||
|
var id = g.heading.id || g.heading.textContent.replace(/\s+/g, "_");
|
||||||
|
var justCompleted = g.checked === g.total && !_completedHeadings[id];
|
||||||
|
_completedHeadings[id] = g.checked === g.total;
|
||||||
|
|
||||||
|
if (justCompleted) {
|
||||||
|
fireHeadingComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check page completion
|
||||||
|
var pageComplete =
|
||||||
|
counts.pageTotal > 0 && counts.pageChecked === counts.pageTotal;
|
||||||
|
if (pageComplete && !_pageWasComplete) {
|
||||||
|
firePageComplete();
|
||||||
|
}
|
||||||
|
_pageWasComplete = pageComplete;
|
||||||
|
|
||||||
|
// Save summary for nav aggregation
|
||||||
|
savePageSummary(path, counts.pageChecked, counts.pageTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Confetti ───────────────────────────────────────
|
||||||
|
// Single shared canvas + one rAF loop. Multiple completions add particles
|
||||||
|
// to the same pool instead of stacking N fullscreen canvases (each of which
|
||||||
|
// would independently repaint the whole viewport at 60fps — the cause of
|
||||||
|
// click-induced slowdown). The loop fully stops when no particles remain,
|
||||||
|
// and the canvas is removed so there is no idle fullscreen layer.
|
||||||
|
var _confettiCanvas = null;
|
||||||
|
var _confettiCtx = null;
|
||||||
|
var _confettiParticles = [];
|
||||||
|
var _confettiRAF = 0;
|
||||||
|
|
||||||
|
var CONFETTI_COLORS = [
|
||||||
|
"#4a8262", "#a8c5b6", "#3d6b52", "#e9dfc6", "#92ad9e", "#d4e8dc",
|
||||||
|
];
|
||||||
|
|
||||||
|
function _ensureConfettiCanvas() {
|
||||||
|
if (_confettiCanvas) return;
|
||||||
|
var canvas = document.createElement("canvas");
|
||||||
|
canvas.style.cssText =
|
||||||
|
"position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:99999;";
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
_confettiCanvas = canvas;
|
||||||
|
_confettiCtx = canvas.getContext("2d");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _destroyConfettiCanvas() {
|
||||||
|
if (_confettiCanvas) {
|
||||||
|
_confettiCanvas.remove();
|
||||||
|
_confettiCanvas = null;
|
||||||
|
_confettiCtx = null;
|
||||||
|
}
|
||||||
|
_confettiParticles = [];
|
||||||
|
_confettiRAF = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _confettiTick(now) {
|
||||||
|
var ctx = _confettiCtx;
|
||||||
|
var canvas = _confettiCanvas;
|
||||||
|
if (!ctx || !canvas) {
|
||||||
|
_confettiRAF = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
var alive = 0;
|
||||||
|
for (var i = 0; i < _confettiParticles.length; i++) {
|
||||||
|
var p = _confettiParticles[i];
|
||||||
|
var elapsed = now - p.start;
|
||||||
|
var progress = elapsed / p.duration;
|
||||||
|
if (progress >= 1) continue; // dead — skip
|
||||||
|
alive++;
|
||||||
|
|
||||||
|
p.x += p.vx;
|
||||||
|
p.vy += 0.12;
|
||||||
|
p.y += p.vy;
|
||||||
|
p.rot += p.rotV;
|
||||||
|
var fade = progress > 0.7 ? 1 - (progress - 0.7) / 0.3 : 1;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(p.x, p.y);
|
||||||
|
ctx.rotate((p.rot * Math.PI) / 180);
|
||||||
|
ctx.globalAlpha = fade;
|
||||||
|
ctx.fillStyle = p.color;
|
||||||
|
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alive === 0) {
|
||||||
|
_destroyConfettiCanvas();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically compact the array so dead particles don't accumulate.
|
||||||
|
if (_confettiParticles.length > 400) {
|
||||||
|
_confettiParticles = _confettiParticles.filter(function (p) {
|
||||||
|
return now - p.start < p.duration;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_confettiRAF = requestAnimationFrame(_confettiTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireConfetti(intensity) {
|
||||||
|
if (REDUCED_MOTION) return;
|
||||||
|
|
||||||
|
_ensureConfettiCanvas();
|
||||||
|
var canvas = _confettiCanvas;
|
||||||
|
|
||||||
|
var count = intensity === "page" ? 140 : 55;
|
||||||
|
var duration = intensity === "page" ? 3200 : 2200;
|
||||||
|
var drift = intensity === "page" ? 1.8 : 1.2;
|
||||||
|
var start = performance.now();
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++) {
|
||||||
|
_confettiParticles.push({
|
||||||
|
x: Math.random() * canvas.width,
|
||||||
|
y: -(Math.random() * 60 + 10),
|
||||||
|
w: Math.random() * 7 + 2,
|
||||||
|
h: Math.random() * 4 + 1.5,
|
||||||
|
color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
|
||||||
|
vx: (Math.random() - 0.5) * drift,
|
||||||
|
vy: Math.random() * 2 + 1.2,
|
||||||
|
rot: Math.random() * 360,
|
||||||
|
rotV: (Math.random() - 0.5) * 8,
|
||||||
|
start: start,
|
||||||
|
duration: duration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the single loop only if it isn't already running.
|
||||||
|
if (!_confettiRAF) {
|
||||||
|
_confettiRAF = requestAnimationFrame(_confettiTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the shared canvas sized to the viewport.
|
||||||
|
window.addEventListener("resize", function () {
|
||||||
|
if (_confettiCanvas) {
|
||||||
|
_confettiCanvas.width = window.innerWidth;
|
||||||
|
_confettiCanvas.height = window.innerHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Sound ──────────────────────────────────────────
|
||||||
|
var _audioCtx = null;
|
||||||
|
|
||||||
|
function getAudioCtx() {
|
||||||
|
if (!_audioCtx) {
|
||||||
|
_audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
}
|
||||||
|
if (_audioCtx.state === "suspended") {
|
||||||
|
_audioCtx.resume();
|
||||||
|
}
|
||||||
|
return _audioCtx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function playTone(freq, duration, when) {
|
||||||
|
if (REDUCED_MOTION) return;
|
||||||
|
try {
|
||||||
|
var ctx = getAudioCtx();
|
||||||
|
var osc = ctx.createOscillator();
|
||||||
|
var gain = ctx.createGain();
|
||||||
|
var t = when || ctx.currentTime;
|
||||||
|
|
||||||
|
osc.type = "sine";
|
||||||
|
osc.frequency.setValueAtTime(freq, t);
|
||||||
|
|
||||||
|
gain.gain.setValueAtTime(0, t);
|
||||||
|
gain.gain.linearRampToValueAtTime(0.3, t + 0.04);
|
||||||
|
gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
|
||||||
|
|
||||||
|
osc.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
osc.start(t);
|
||||||
|
osc.stop(t + duration);
|
||||||
|
} catch (e) {
|
||||||
|
/* Web Audio not available */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fireHeadingComplete() {
|
||||||
|
playTone(432, 1.6);
|
||||||
|
fireConfetti("heading");
|
||||||
|
}
|
||||||
|
|
||||||
|
function firePageComplete() {
|
||||||
|
var ctx;
|
||||||
|
try {
|
||||||
|
ctx = getAudioCtx();
|
||||||
|
} catch (e) {
|
||||||
|
fireConfetti("page");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ascending arpeggio centered on 528 Hz
|
||||||
|
var notes = [528 * 0.667, 528 * 0.75, 528 * 0.833, 528];
|
||||||
|
var t = ctx.currentTime;
|
||||||
|
notes.forEach(function (freq, i) {
|
||||||
|
playTone(freq, 0.7, t + i * 0.18);
|
||||||
|
});
|
||||||
|
fireConfetti("page");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Navigation badges ──────────────────────────────
|
||||||
|
function updateNavBadges() {
|
||||||
|
// Pause observer during our own DOM changes to prevent feedback loops
|
||||||
|
if (_navObserver) _navObserver.disconnect();
|
||||||
|
|
||||||
|
var store = getStorage();
|
||||||
|
// Only badge PAGE links in the primary navigation. Exclude:
|
||||||
|
// - the secondary nav ("On this page" / table-of-contents), whose links
|
||||||
|
// are in-page #anchors and would all resolve to the current page total
|
||||||
|
// - any link whose href is/contains a # fragment (same reason)
|
||||||
|
var navLinks = document.querySelectorAll(
|
||||||
|
".md-nav--primary .md-nav__link[href]"
|
||||||
|
);
|
||||||
|
|
||||||
|
navLinks.forEach(function (link) {
|
||||||
|
var href = link.getAttribute("href");
|
||||||
|
// Existing badge on this link, if any — we update in place rather than
|
||||||
|
// tearing down every badge in the sidebar each toggle (a full nav
|
||||||
|
// teardown/rebuild forces Material to relayout the whole sidebar, which
|
||||||
|
// was the dominant reflow cost per checkbox click).
|
||||||
|
var existing = link.querySelector(":scope > .toa-nav-badge");
|
||||||
|
|
||||||
|
// Skip in-page anchor links (TOC entries, heading permalinks).
|
||||||
|
if (!href || href.charAt(0) === "#" || href.indexOf("#") !== -1) {
|
||||||
|
if (existing) existing.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Defensive: skip if this link lives in a secondary (TOC) nav.
|
||||||
|
if (link.closest(".md-nav--secondary")) {
|
||||||
|
if (existing) existing.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve relative URLs to absolute paths
|
||||||
|
var url;
|
||||||
|
try {
|
||||||
|
url = new URL(href, location.origin + location.pathname).pathname;
|
||||||
|
} catch (e) {
|
||||||
|
if (existing) existing.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (url.slice(-1) !== "/") url += "/";
|
||||||
|
|
||||||
|
// Check if this is a section (index) page with child pages.
|
||||||
|
// NOTE: Material wraps the ACTIVE page's .md-nav__item with a nested
|
||||||
|
// <nav class="md-nav md-nav--secondary"> holding the "On this page" TOC.
|
||||||
|
// A naive ".md-nav--nested, nav.md-nav" probe matches that TOC and
|
||||||
|
// mis-classifies the active leaf page as a section. So we detect real
|
||||||
|
// section items via Material's own marker class and explicitly ignore
|
||||||
|
// any nested nav that is the secondary (TOC) nav.
|
||||||
|
var parentNav = link.closest(".md-nav__item");
|
||||||
|
var nestedNav = parentNav
|
||||||
|
? parentNav.querySelector(":scope > nav.md-nav:not(.md-nav--secondary)")
|
||||||
|
: null;
|
||||||
|
var isSection =
|
||||||
|
(parentNav && parentNav.classList.contains("md-nav__item--nested")) ||
|
||||||
|
!!nestedNav;
|
||||||
|
|
||||||
|
var badge;
|
||||||
|
if (isSection) {
|
||||||
|
// Aggregate all sub-pages whose paths start with this URL
|
||||||
|
badge = aggregateChildren(url, store);
|
||||||
|
} else {
|
||||||
|
badge = getPageSummary(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!badge || badge.total === 0) {
|
||||||
|
if (existing) existing.remove();
|
||||||
|
link.classList.remove("toa-nav--complete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isComplete = badge.checked === badge.total;
|
||||||
|
var desiredText = isComplete ? "\u2713" : badge.checked + "/" + badge.total;
|
||||||
|
var desiredClass = isComplete
|
||||||
|
? "toa-nav-badge toa-nav-badge--complete"
|
||||||
|
: "toa-nav-badge";
|
||||||
|
|
||||||
|
// Reuse the existing span; only write when something actually changed, so
|
||||||
|
// unchanged links cause no DOM mutation and no style recalc.
|
||||||
|
var span = existing;
|
||||||
|
if (!span) {
|
||||||
|
span = document.createElement("span");
|
||||||
|
link.appendChild(span);
|
||||||
|
}
|
||||||
|
if (span.className !== desiredClass) span.className = desiredClass;
|
||||||
|
if (span.textContent !== desiredText) span.textContent = desiredText;
|
||||||
|
if (isComplete) {
|
||||||
|
if (!link.classList.contains("toa-nav--complete")) {
|
||||||
|
link.classList.add("toa-nav--complete");
|
||||||
|
}
|
||||||
|
} else if (link.classList.contains("toa-nav--complete")) {
|
||||||
|
link.classList.remove("toa-nav--complete");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resume observer
|
||||||
|
if (_navObserver) {
|
||||||
|
var sb = document.querySelector(".md-sidebar--primary");
|
||||||
|
if (sb) _navObserver.observe(sb, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function aggregateChildren(parentUrl, store) {
|
||||||
|
var checked = 0;
|
||||||
|
var total = 0;
|
||||||
|
Object.keys(store.pages).forEach(function (path) {
|
||||||
|
if (path === parentUrl) return;
|
||||||
|
if (path.indexOf(parentUrl) === 0) {
|
||||||
|
var s = store.pages[path];
|
||||||
|
if (s && s._total !== undefined) {
|
||||||
|
checked += s._checked || 0;
|
||||||
|
total += s._total || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (total === 0) return null;
|
||||||
|
return { checked: checked, total: total };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Export ─────────────────────────────────────────
|
||||||
|
function createExportLink() {
|
||||||
|
// Only add if there's data to export
|
||||||
|
var store = getStorage();
|
||||||
|
var hasData = Object.keys(store.pages).length > 0;
|
||||||
|
var existing = document.querySelector(".toa-export");
|
||||||
|
if (existing) existing.remove();
|
||||||
|
if (!hasData) return;
|
||||||
|
|
||||||
|
// Find a good spot — footer copyright area or near comments
|
||||||
|
var container =
|
||||||
|
document.querySelector(".md-copyright") ||
|
||||||
|
document.querySelector("#__comments") ||
|
||||||
|
document.querySelector("article");
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
var link = document.createElement("a");
|
||||||
|
link.className = "toa-export";
|
||||||
|
link.textContent = "Download my progress";
|
||||||
|
link.href = "#";
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var raw = localStorage.getItem(STORAGE_KEY) || "{}";
|
||||||
|
var blob = new Blob([raw], { type: "application/json" });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "tree-of-ascension-progress.json";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main init ──────────────────────────────────────
|
||||||
|
var _lastInitPath = null;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var path = getPagePath();
|
||||||
|
if (_lastInitPath === path) return; // guard against duplicate instant-nav fires
|
||||||
|
_lastInitPath = path;
|
||||||
|
|
||||||
|
_completedHeadings = {};
|
||||||
|
_pageWasComplete = false;
|
||||||
|
|
||||||
|
var result = initCheckboxes();
|
||||||
|
var counts = computeHeadingCounts();
|
||||||
|
savePageSummary(path, result.checked, result.total);
|
||||||
|
renderHeadingPills(counts);
|
||||||
|
updatePageHeading(counts);
|
||||||
|
updateNavBadges();
|
||||||
|
createExportLink();
|
||||||
|
|
||||||
|
// Seed build-time totals so nav badges show correct numbers for pages
|
||||||
|
// the user hasn't visited yet, then re-render badges once loaded.
|
||||||
|
seedTotalsFromManifest(function (ok) {
|
||||||
|
if (ok) updateNavBadges();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark existing completions so we don't re-trigger on page load
|
||||||
|
counts.groups.forEach(function (g) {
|
||||||
|
if (g.total === 0) return;
|
||||||
|
var id = g.heading.id || g.heading.textContent.replace(/\s+/g, "_");
|
||||||
|
_completedHeadings[id] = g.checked === g.total;
|
||||||
|
if (g.checked === g.total) {
|
||||||
|
g.heading.classList.add("toa-heading--complete");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_pageWasComplete =
|
||||||
|
counts.pageTotal > 0 && counts.pageChecked === counts.pageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Boot ───────────────────────────────────────────
|
||||||
|
var _navObserver = null;
|
||||||
|
|
||||||
|
function setupNavObserver() {
|
||||||
|
if (_navObserver) return;
|
||||||
|
var sidebar = document.querySelector(".md-sidebar--primary");
|
||||||
|
if (!sidebar) return;
|
||||||
|
_navObserver = new MutationObserver(function () {
|
||||||
|
updateNavBadges();
|
||||||
|
});
|
||||||
|
_navObserver.observe(sidebar, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready(fn) {
|
||||||
|
if (typeof document$ !== "undefined") {
|
||||||
|
document$.subscribe(function () {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
requestAnimationFrame(fn);
|
||||||
|
});
|
||||||
|
setupNavObserver();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
fn();
|
||||||
|
setupNavObserver();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
setupNavObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(init);
|
||||||
|
})();
|
||||||
|
|
|
||||||
4
javascripts/toa-totals.json
Normal file
4
javascripts/toa-totals.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"/practices/daily-sadhana/": 13,
|
||||||
|
"/strategies/digital-sovereignty/": 7
|
||||||
|
}
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1087,21 +1093,29 @@
|
||||||
<p><em>Sadhana</em> means daily spiritual practice — not something you do when you have time, but the foundation you build everything else upon. It doesn't require a temple or a teacher. It requires showing up.</p>
|
<p><em>Sadhana</em> means daily spiritual practice — not something you do when you have time, but the foundation you build everything else upon. It doesn't require a temple or a teacher. It requires showing up.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="meditation">Meditation<a class="headerlink" href="#meditation" title="Permanent link">¶</a></h2>
|
<h2 id="meditation">Meditation<a class="headerlink" href="#meditation" title="Permanent link">¶</a></h2>
|
||||||
<p>Sit. Breathe. That's the whole instruction.</p>
|
<ul class="task-list">
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Sit quietly for 5 minutes — a chair, a floor, anywhere</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Notice when the mind wanders and gently return to breath</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Do this every morning for one week</li>
|
||||||
|
</ul>
|
||||||
<p>Start with five minutes. The mind will wander — that's what minds do. The practice is not emptying the mind but noticing when it has wandered and gently returning. Every return is a rep. Like any muscle, attention strengthens with use.</p>
|
<p>Start with five minutes. The mind will wander — that's what minds do. The practice is not emptying the mind but noticing when it has wandered and gently returning. Every return is a rep. Like any muscle, attention strengthens with use.</p>
|
||||||
<p>No app needed. No special cushion. A chair, a floor, a patch of grass — anywhere you can be still.</p>
|
<p>No app needed. No special cushion. A chair, a floor, a patch of grass — anywhere you can be still.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="gratitude-journal">Gratitude journal<a class="headerlink" href="#gratitude-journal" title="Permanent link">¶</a></h2>
|
<h2 id="gratitude-journal">Gratitude journal<a class="headerlink" href="#gratitude-journal" title="Permanent link">¶</a></h2>
|
||||||
<p>Before bed or first thing in the morning, write down three things you are genuinely grateful for. Not generic blessings — specific moments. The warmth of sunlight through a window. A conversation that landed. The taste of a ripe fruit.</p>
|
<ul class="task-list">
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Before bed or first thing, write down 3 specific things you're grateful for</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Do this every day for one week and notice the shift</li>
|
||||||
|
</ul>
|
||||||
|
<p>Not generic blessings — specific moments. The warmth of sunlight through a window. A conversation that landed. The taste of a ripe fruit.</p>
|
||||||
<p>Gratitude is not toxic positivity. It is the deliberate practice of noticing what is already good, which rewires the brain away from its negativity bias over time.</p>
|
<p>Gratitude is not toxic positivity. It is the deliberate practice of noticing what is already good, which rewires the brain away from its negativity bias over time.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="personality-defect-removal">Personality defect removal<a class="headerlink" href="#personality-defect-removal" title="Permanent link">¶</a></h2>
|
<h2 id="personality-defect-removal">Personality defect removal<a class="headerlink" href="#personality-defect-removal" title="Permanent link">¶</a></h2>
|
||||||
<p>This is the inner work few want to do: asking honestly where you are still reactive.</p>
|
<p>This is the inner work few want to do: asking honestly where you are still reactive.</p>
|
||||||
<ul>
|
<ul class="task-list">
|
||||||
<li>Where am I still carrying anger?</li>
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Ask yourself: Where am I still carrying anger?</li>
|
||||||
<li>What am I afraid of?</li>
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Ask yourself: What am I afraid of?</li>
|
||||||
<li>How can I be more patient today?</li>
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Ask yourself: How can I be more patient today?</li>
|
||||||
<li>Where could I replace judgment with kindness?</li>
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Replace one judgment with kindness today</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Don't fix it all at once. Pick one thread and follow it. The goal is not to become perfect — it is to become less defended, more available, more unconditionally loving. Not toward everyone else first — toward yourself.</p>
|
<p>Don't fix it all at once. Pick one thread and follow it. The goal is not to become perfect — it is to become less defended, more available, more unconditionally loving. Not toward everyone else first — toward yourself.</p>
|
||||||
<details class="question">
|
<details class="question">
|
||||||
|
|
@ -1112,11 +1126,18 @@
|
||||||
<h2 id="movement-energy-and-strength">Movement: energy and strength<a class="headerlink" href="#movement-energy-and-strength" title="Permanent link">¶</a></h2>
|
<h2 id="movement-energy-and-strength">Movement: energy and strength<a class="headerlink" href="#movement-energy-and-strength" title="Permanent link">¶</a></h2>
|
||||||
<p>The body needs two kinds of movement every day:</p>
|
<p>The body needs two kinds of movement every day:</p>
|
||||||
<p><strong>Energy practice</strong> — yoga, tai chi, qigong, or simple stretching with breath awareness. This moves <em>prana</em> (life force) through the channels of the body. Even ten minutes shifts the nervous system from fight-or-flight to rest-and-digest.</p>
|
<p><strong>Energy practice</strong> — yoga, tai chi, qigong, or simple stretching with breath awareness. This moves <em>prana</em> (life force) through the channels of the body. Even ten minutes shifts the nervous system from fight-or-flight to rest-and-digest.</p>
|
||||||
|
<ul class="task-list">
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Do 10 minutes of energy practice this morning (yoga, stretching, tai chi)</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Do 20 minutes of cardio or strength work today</li>
|
||||||
|
</ul>
|
||||||
<p><strong>Cardio and strength</strong> — walking briskly, running, bodyweight exercises, lifting. The body evolved to move against resistance. Without it, the mind stagnates too.</p>
|
<p><strong>Cardio and strength</strong> — walking briskly, running, bodyweight exercises, lifting. The body evolved to move against resistance. Without it, the mind stagnates too.</p>
|
||||||
<p>Pair them. Energy practice in the morning to open; strength in the afternoon to ground.</p>
|
<p>Pair them. Energy practice in the morning to open; strength in the afternoon to ground.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="visualization-morning-and-night">Visualization: morning and night<a class="headerlink" href="#visualization-morning-and-night" title="Permanent link">¶</a></h2>
|
<h2 id="visualization-morning-and-night">Visualization: morning and night<a class="headerlink" href="#visualization-morning-and-night" title="Permanent link">¶</a></h2>
|
||||||
<p>Every morning and every night before sleep, spend two minutes visualizing the future you want — for yourself, your family, your community, your 'tribe.' See it as if it is real <em>now</em>. Feel the gratitude as if it has already arrived.</p>
|
<ul class="task-list">
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Spend 2 minutes each morning visualizing the future you want — see it as real <em>now</em></li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Spend 2 minutes before sleep visualizing the same — feel the gratitude as if it arrived</li>
|
||||||
|
</ul>
|
||||||
<p>This is not magical thinking. It is the practice of orienting the subconscious toward what you are building, so that during the day, you recognize the doors when they appear.</p>
|
<p>This is not magical thinking. It is the practice of orienting the subconscious toward what you are building, so that during the day, you recognize the doors when they appear.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<p><strong>Related:</strong> <a href="../../../strategies/finding-purpose/">Finding Your Purpose</a> — when sadhana meets direction.</p>
|
<p><strong>Related:</strong> <a href="../../../strategies/finding-purpose/">Finding Your Purpose</a> — when sadhana meets direction.</p>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1114,8 +1120,12 @@
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="network-sovereignty">Network sovereignty<a class="headerlink" href="#network-sovereignty" title="Permanent link">¶</a></h2>
|
<h2 id="network-sovereignty">Network sovereignty<a class="headerlink" href="#network-sovereignty" title="Permanent link">¶</a></h2>
|
||||||
<p><strong>Switch from WiFi to Ethernet</strong> for your main computer. It's faster, more stable, and eliminates constant radio-frequency exposure at close range.</p>
|
<ul class="task-list">
|
||||||
<p><strong>On your phone</strong>: switch 5G to 4G (lower frequency = less power = less exposure). Keep your phone on airplane mode when not actively needed. Keep it away from your body — not in a pocket, not against your head. Zero wearables (no smartwatch, no fitness tracker, no bluetooth earbuds).</p>
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Switch your main computer from WiFi to Ethernet</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> On your phone: switch 5G to 4G, use airplane mode when not needed</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Install and use a trusted VPN with a clear no-logging policy</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Keep your phone away from your body — not in a pocket, not against your head</li>
|
||||||
|
</ul>
|
||||||
<p><strong>VPN</strong>: A simple, trusted VPN prevents your internet provider from building a profile of every site you visit. Choose one with a clear no-logging policy and preferably based outside surveillance alliances.</p>
|
<p><strong>VPN</strong>: A simple, trusted VPN prevents your internet provider from building a profile of every site you visit. Choose one with a clear no-logging policy and preferably based outside surveillance alliances.</p>
|
||||||
<p><strong>Meshtastic</strong>: When the grid is not your friend, build your own. <a href="https://meshtastic.org/">Meshtastic</a> lets you create off-grid, encrypted text networks using inexpensive LoRa radios. It's community infrastructure — no cell towers, no subscriptions, no central authority.</p>
|
<p><strong>Meshtastic</strong>: When the grid is not your friend, build your own. <a href="https://meshtastic.org/">Meshtastic</a> lets you create off-grid, encrypted text networks using inexpensive LoRa radios. It's community infrastructure — no cell towers, no subscriptions, no central authority.</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -1150,7 +1160,11 @@
|
||||||
<p>The principle is simple: <strong>self-hosted first, local-first, privacy-first</strong>. If your data lives on someone else's computer, it is not your data.</p>
|
<p>The principle is simple: <strong>self-hosted first, local-first, privacy-first</strong>. If your data lives on someone else's computer, it is not your data.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="passwords-and-keys">Passwords and keys<a class="headerlink" href="#passwords-and-keys" title="Permanent link">¶</a></h2>
|
<h2 id="passwords-and-keys">Passwords and keys<a class="headerlink" href="#passwords-and-keys" title="Permanent link">¶</a></h2>
|
||||||
<p>Use <a href="https://keepassxc.org/">KeePassXC</a> — a local, offline, open-source password manager. Unlike browser-based password managers or online services, your password database lives on your device. You control the file. Nobody can leak what they don't have.</p>
|
<ul class="task-list">
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Install KeePassXC and create your first password database</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Generate unique passwords for every important service</li>
|
||||||
|
<li class="task-list-item"><label class="task-list-control"><input type="checkbox" disabled/><span class="task-list-indicator"></span></label> Get a hardware security key (like a YubiKey) where possible</li>
|
||||||
|
</ul>
|
||||||
<p>Generate unique passwords for every service. Use a hardware security key (like a YubiKey) where possible. Treat your digital keys as carefully as the keys to your home.</p>
|
<p>Generate unique passwords for every service. Use a hardware security key (like a YubiKey) where possible. Treat your digital keys as carefully as the keys to your home.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h2 id="the-bigger-picture">The bigger picture<a class="headerlink" href="#the-bigger-picture" title="Permanent link">¶</a></h2>
|
<h2 id="the-bigger-picture">The bigger picture<a class="headerlink" href="#the-bigger-picture" title="Permanent link">¶</a></h2>
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,12 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.TOA_ACTION_POINTS_ENABLED =
|
||||||
|
true;
|
||||||
|
</script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -858,4 +858,123 @@ article h1, article h2, article h3 {
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
padding: 1.1rem 1.1rem 3.2rem 1.1rem;
|
padding: 1.1rem 1.1rem 3.2rem 1.1rem;
|
||||||
}
|
}
|
||||||
|
/* ═════════════════════════════════════════════════════════════════
|
||||||
|
Action Points — Interactive Checklist
|
||||||
|
═════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Checkbox interaction ─────────────────────────── */
|
||||||
|
li.task-list-item input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.18s var(--gp-ease, cubic-bezier(0.22, 0.61, 0.36, 1));
|
||||||
|
}
|
||||||
|
li.task-list-item input[type="checkbox"]:active {
|
||||||
|
transform: scale(0.82);
|
||||||
|
}
|
||||||
|
li.task-list-item:has(input:checked) {
|
||||||
|
text-decoration: line-through;
|
||||||
|
color: var(--gp-text-muted);
|
||||||
|
opacity: 0.72;
|
||||||
|
transition: opacity 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Pill badges (via ::after pseudo-element — clean textContent) ── */
|
||||||
|
h1[data-toa-count]::after,
|
||||||
|
h2[data-toa-count]::after,
|
||||||
|
h3[data-toa-count]::after {
|
||||||
|
content: attr(data-toa-count);
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
background: var(--gp-card-bg, rgba(255,255,255,0.74));
|
||||||
|
border: 1px solid var(--gp-card-border, rgba(122,170,150,0.22));
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.08em 0.62em;
|
||||||
|
font-size: 0.62em;
|
||||||
|
font-weight: 550;
|
||||||
|
color: var(--gp-text-muted, #4e5e4b);
|
||||||
|
margin-left: 0.5em;
|
||||||
|
line-height: 1.6;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
transition:
|
||||||
|
background 0.35s ease,
|
||||||
|
color 0.35s ease,
|
||||||
|
border-color 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1[data-toa-count]::after {
|
||||||
|
font-size: 0.48em;
|
||||||
|
padding: 0.1em 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1[data-toa-complete="true"]::after,
|
||||||
|
h2[data-toa-complete="true"]::after,
|
||||||
|
h3[data-toa-complete="true"]::after {
|
||||||
|
background: #3d6b52;
|
||||||
|
color: #fff !important;
|
||||||
|
/* Headings set -webkit-text-fill-color (gradient/fill effect); pseudo-els
|
||||||
|
inherit it and it overrides `color`. Override it on the pill ONLY so the
|
||||||
|
count text is white on green — the heading's own text color is untouched. */
|
||||||
|
-webkit-text-fill-color: #fff !important;
|
||||||
|
border-color: #3d6b52;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Heading completion ───────────────────────────── */
|
||||||
|
.toa-heading--complete {
|
||||||
|
color: #3d6b52 !important;
|
||||||
|
transition: color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Nav badges ───────────────────────────────────── */
|
||||||
|
.toa-nav-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2em;
|
||||||
|
background: var(--gp-card-bg, rgba(255,255,255,0.74));
|
||||||
|
border: 1px solid var(--gp-card-border, rgba(122,170,150,0.22));
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0 0.42em;
|
||||||
|
font-size: 0.68em;
|
||||||
|
font-weight: 550;
|
||||||
|
color: var(--gp-text-muted, #4e5e4b);
|
||||||
|
margin-left: 0.4em;
|
||||||
|
line-height: 1.65;
|
||||||
|
transition:
|
||||||
|
background 0.35s ease,
|
||||||
|
color 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toa-nav-badge--complete {
|
||||||
|
background: #3d6b52;
|
||||||
|
color: #fff !important;
|
||||||
|
-webkit-text-fill-color: #fff !important;
|
||||||
|
border-color: #3d6b52;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The complete nav link goes green (below). Make sure the badge text on its
|
||||||
|
green background stays white and isn't dragged green by the link's
|
||||||
|
!important color. */
|
||||||
|
.md-nav__link.toa-nav--complete .toa-nav-badge--complete {
|
||||||
|
color: #fff !important;
|
||||||
|
-webkit-text-fill-color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-nav__link.toa-nav--complete {
|
||||||
|
color: #3d6b52 !important;
|
||||||
|
transition: color 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Export link ──────────────────────────────────── */
|
||||||
|
.toa-export {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--gp-text-muted, #4e5e4b);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
text-decoration-color: var(--gp-secondary, #a8c5b6);
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
transition: color 0.22s ease;
|
||||||
|
}
|
||||||
|
.toa-export:hover {
|
||||||
|
color: var(--gp-primary, #4a8262);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue