1
0
Files
tutorials/privacy.html
T
2026-06-04 18:41:30 +02:00

627 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<title>Privacy — Pouya's Field Guides</title>
<meta name="description" content="What this site does and doesn't do with your visit — the short, honest version. No cookies, no JavaScript tracking, no third-party analytics; only anonymized, short-lived server logs.">
<style>
/* =========================================================================
FIELD GUIDES — PRIVACY (single-file, standalone)
Direction: editorial engineering field-guide. Reuses the established
landing-page design system VERBATIM: cool lavender-paper / midnight-indigo
palette, violet-indigo signature accent (#6d4aff light / #8c6bff dark),
Georgia display serif + system sans body + ui-monospace labels, faint
engineering grid, theme pill, calm single reveal.
The :root tokens, dark palette, body::before grid, masthead, themebar,
footer and the two inline scripts are copied unchanged from index.html so
the two pages theme 1:1 and share the 'pwguide-theme' store key.
========================================================================= */
:root{
/* --- light: cool lavender paper, navy ink, violet/indigo signature ------ */
--bg: #eef0fb;
--bg-grain: #e4e6f7;
--surface: #f7f8ff;
--surface-2: #eaecf9;
--ink: #15172b;
--ink-soft: #383b58;
--muted: #5d6080;
--faint: #8b8ead;
--border: #d4d6ee;
--border-strong: #bfc1e3;
--rule: #cdcfeb;
--accent: #6d4aff; /* violet/indigo signature */
--accent-ink: #5634d6;
--accent-wash: #e4ddff;
--good: #0f7a73; /* teal */
--good-wash: #d4ece9;
--bad: #d62828; /* danger red */
--bad-wash: #f7dcdc;
--warn: #b06a00; /* amber-bronze warn (kept warm for semantics) */
--warn-wash: #f6e7cd;
--slate: #3f5180; /* neutral / domain */
--slate-wash: #dde2f1;
--shadow: 0 1px 0 rgba(21,23,43,.04), 0 10px 30px -22px rgba(21,23,43,.45);
--shadow-lift: 0 2px 0 rgba(21,23,43,.05), 0 24px 50px -28px rgba(21,23,43,.5);
--serif: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, "Helvetica Neue", sans-serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", "Menlo", "Consolas", monospace;
--step--1: clamp(.78rem, .76rem + .12vw, .85rem);
--step-0: clamp(.95rem, .92rem + .2vw, 1.05rem);
--step-1: clamp(1.15rem, 1.05rem + .4vw, 1.4rem);
--step-2: clamp(1.5rem, 1.3rem + .8vw, 2rem);
--step-3: clamp(2rem, 1.6rem + 1.6vw, 3rem);
--step-4: clamp(2.6rem, 2rem + 3vw, 4.4rem);
--maxw: 1080px;
--gap: clamp(1.5rem, 1rem + 2vw, 3rem);
}
/* ----- dark palette, applied both by system default and explicit toggle -- */
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
--bg: #0c0e1c;
--bg-grain: #12152a;
--surface: #161a32;
--surface-2: #1e2240;
--ink: #eceefb;
--ink-soft: #c4c8e6;
--muted: #9094bd;
--faint: #62668c;
--border: #2a2f52;
--border-strong: #3a4068;
--rule: #232845;
--accent: #8c6bff;
--accent-ink: #a78cff;
--accent-wash: #241d4a;
--good: #46c2b6;
--good-wash: #102e2a;
--bad: #ff6b6b;
--bad-wash: #3a1b1f;
--warn: #e0a64a;
--warn-wash: #392b16;
--slate: #9aa9d6;
--slate-wash: #1c2240;
--shadow: 0 1px 0 rgba(0,0,0,.4), 0 14px 36px -24px rgba(0,0,0,.9);
--shadow-lift: 0 2px 0 rgba(0,0,0,.5), 0 30px 60px -30px rgba(0,0,0,.95);
}
}
:root[data-theme="dark"]{
--bg: #0c0e1c;
--bg-grain: #12152a;
--surface: #161a32;
--surface-2: #1e2240;
--ink: #eceefb;
--ink-soft: #c4c8e6;
--muted: #9094bd;
--faint: #62668c;
--border: #2a2f52;
--border-strong: #3a4068;
--rule: #232845;
--accent: #8c6bff;
--accent-ink: #a78cff;
--accent-wash: #241d4a;
--good: #46c2b6;
--good-wash: #102e2a;
--bad: #ff6b6b;
--bad-wash: #3a1b1f;
--warn: #e0a64a;
--warn-wash: #392b16;
--slate: #9aa9d6;
--slate-wash: #1c2240;
--shadow: 0 1px 0 rgba(0,0,0,.4), 0 14px 36px -24px rgba(0,0,0,.9);
--shadow-lift: 0 2px 0 rgba(0,0,0,.5), 0 30px 60px -30px rgba(0,0,0,.95);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
@media (prefers-reduced-motion: reduce){html{scroll-behavior:auto}}
body{
margin:0;
background:
radial-gradient(120% 80% at 100% 0%, var(--bg-grain) 0%, transparent 55%),
var(--bg);
color:var(--ink);
font-family:var(--sans);
font-size:var(--step-0);
line-height:1.6;
letter-spacing:.005em;
-webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility;
}
::selection{background:var(--accent);color:#fff}
:root[data-theme="dark"] ::selection{color:#fff}
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]) ::selection{color:#fff}}
a{color:var(--accent-ink);text-underline-offset:.18em}
:focus-visible{outline:2.5px solid var(--accent);outline-offset:3px;border-radius:3px}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 clamp(1.1rem,4vw,3rem)}
/* faint engineering grid behind everything */
body::before{
content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
background-image:
linear-gradient(to right, var(--rule) 1px, transparent 1px),
linear-gradient(to bottom, var(--rule) 1px, transparent 1px);
background-size:46px 46px;
opacity:.22;
mask-image:radial-gradient(120% 90% at 50% 0%, #000 10%, transparent 80%);
-webkit-mask-image:radial-gradient(120% 90% at 50% 0%, #000 10%, transparent 80%);
}
/* skip link */
.skip{position:absolute;top:0;left:0;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
.skip:focus{width:auto;height:auto;margin:0;overflow:visible;clip:auto;clip-path:none;
inset-inline-start:1rem;top:1rem;z-index:50;background:var(--ink);color:var(--bg);
padding:.6rem 1rem;border-radius:6px;font-family:var(--mono);font-size:var(--step--1)}
/* ---- type helpers ------------------------------------------------------- */
.mono{font-family:var(--mono)}
.num{font-family:var(--mono);font-variant-numeric:tabular-nums lining-nums}
.kicker{
font-family:var(--mono);font-size:var(--step--1);
letter-spacing:.22em;text-transform:uppercase;color:var(--accent);font-weight:600;
}
h1,h2,h3,h4{font-family:var(--serif);font-weight:700;line-height:1.06;letter-spacing:-.01em}
code{font-family:var(--mono);font-size:.88em;background:var(--surface-2);
padding:.06em .36em;border-radius:4px;color:var(--accent-ink);border:1px solid var(--border)}
/* =========================================================================
MASTHEAD — editorial cover treatment (matches index.html)
========================================================================= */
.masthead{position:relative;padding:clamp(2.4rem,6vw,5rem) 0 0}
.masthead .topline{
display:flex;flex-wrap:wrap;gap:.6rem 1.4rem;align-items:baseline;
font-family:var(--mono);font-size:var(--step--1);
color:var(--muted);letter-spacing:.04em;
border-bottom:1px solid var(--border-strong);
padding-bottom:.9rem;margin-bottom:clamp(1.6rem,4vw,2.8rem);
}
.masthead .topline strong{color:var(--ink);font-weight:600}
.masthead .topline strong a{color:inherit;text-decoration:none}
.masthead .topline strong a:hover{color:var(--accent-ink)}
.masthead .topline .sep{color:var(--faint)}
.masthead .doc-id{margin-left:auto;color:var(--accent)}
.masthead h1{font-size:var(--step-4);margin:0 0 .5rem;max-width:16ch}
.masthead h1 .accent{color:var(--accent)}
.masthead .sub{
font-family:var(--mono);color:var(--muted);font-size:var(--step-0);letter-spacing:.02em;
}
.masthead .lede{
margin:1.6rem 0 0;max-width:62ch;font-size:var(--step-1);line-height:1.45;
color:var(--ink-soft);font-family:var(--serif);
}
.masthead .lede b{color:var(--ink)}
/* ---- theme toggle (segmented pill, matches mono label system) ----- */
.themebar{
position:absolute;top:clamp(1rem,3vw,1.8rem);right:clamp(1.1rem,4vw,3rem);
display:flex;align-items:center;gap:.9rem;z-index:5;flex-wrap:wrap;justify-content:flex-end;
}
.themebar .tgctl{display:inline-flex;align-items:center;gap:.55rem}
.themebar .tglabel{
font-family:var(--mono);font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;
color:var(--faint);font-weight:700;
}
.themeseg{
display:inline-flex;border:1px solid var(--border-strong);border-radius:99px;
background:var(--surface);box-shadow:var(--shadow);overflow:hidden;
padding:2px;gap:2px;
}
.themeseg button{
appearance:none;border:0;cursor:pointer;background:transparent;
font-family:var(--mono);font-size:.72rem;font-weight:700;letter-spacing:.1em;
color:var(--muted);padding:.32em .82em;border-radius:99px;
transition:background-color .16s ease, color .16s ease;
line-height:1;display:inline-flex;align-items:center;gap:.35em;
}
.themeseg button:hover{color:var(--accent-ink)}
.themeseg button[aria-pressed="true"]{
background:var(--accent);color:#fff;box-shadow:0 1px 6px -2px rgba(0,0,0,.4);
}
:root[data-theme="dark"] .themeseg button[aria-pressed="true"]{color:#fff}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]) .themeseg button[aria-pressed="true"]{color:#fff}
}
.themeseg button:focus-visible{outline:2.5px solid var(--accent);outline-offset:2px}
@media(max-width:560px){
.themebar .tglabel{display:none}
.themebar{top:.7rem;right:.9rem}
}
/* =========================================================================
SECTION SHELL (matches index.html)
========================================================================= */
section{padding:clamp(2.6rem,6vw,4.4rem) 0;border-top:1px solid var(--rule)}
.sec-head{display:flex;align-items:baseline;gap:1rem;margin-bottom:2rem;flex-wrap:wrap}
.sec-head .tag{
font-family:var(--mono);font-weight:700;font-size:var(--step--1);
color:var(--bg);background:var(--ink);
padding:.28em .7em;border-radius:3px;letter-spacing:.1em;flex:none;
}
.sec-head h2{font-size:var(--step-2);margin:0;max-width:26ch}
.sec-head .note{
margin-left:auto;font-family:var(--mono);font-size:var(--step--1);
color:var(--muted);align-self:flex-end;text-align:right;max-width:36ch;line-height:1.5;
}
/* =========================================================================
PRIVACY PROSE — datasheet-style stacked clauses.
New to this page; built only from the shared tokens above so it themes 1:1.
Each clause is a bordered surface card with a mono index rail down the left,
echoing the .specimen rail language from the index without copying its grid.
========================================================================= */
.clauses{display:grid;gap:clamp(1rem,2.4vw,1.4rem);max-width:760px}
.clause{
position:relative;
background:
radial-gradient(130% 120% at 100% 0%, var(--accent-wash) 0%, transparent 46%),
var(--surface);
border:1px solid var(--border);border-radius:16px;
box-shadow:var(--shadow);
padding:clamp(1.3rem,3.4vw,1.9rem) clamp(1.3rem,3.4vw,2rem);
}
.clause .c-rail{
display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;margin-bottom:.85rem;
font-family:var(--mono);
}
.clause .c-no{
font-variant-numeric:tabular-nums lining-nums;font-weight:700;letter-spacing:-.02em;
font-size:clamp(1.5rem,1.1rem+1.4vw,2.1rem);line-height:.9;color:var(--accent);flex:none;
}
.clause .c-topic{
font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;font-weight:700;color:var(--accent);
}
.clause h2{
font-size:var(--step-2);margin:0 0 .55rem;color:var(--ink);max-width:30ch;
}
.clause p{margin:0 0 .85rem;max-width:66ch;color:var(--ink-soft)}
.clause p:last-child{margin-bottom:0}
.clause p b{color:var(--ink)}
.clause a{color:var(--accent-ink);font-weight:600}
/* the headline "no tracking" promise — a louder accent-washed card */
.clause.lead{
background:
radial-gradient(140% 130% at 0% 0%, var(--accent-wash) 0%, transparent 60%),
var(--surface);
border-color:var(--border-strong);box-shadow:var(--shadow-lift);
}
.clause.lead p{font-family:var(--serif);font-size:var(--step-1);line-height:1.45;color:var(--ink-soft)}
.clause.lead p b{color:var(--ink)}
/* mono fact rows inside a clause (what's recorded / retention) */
.facts{
list-style:none;margin:.2rem 0 0;padding:0;
display:grid;gap:.5rem;
}
.facts li{
display:flex;gap:.7rem;align-items:flex-start;
font-size:var(--step--1);color:var(--ink-soft);line-height:1.5;
}
.facts li::before{
content:"";flex:none;width:7px;height:7px;margin-top:.55em;border-radius:2px;
background:var(--accent);transform:rotate(45deg);
}
.facts li b{color:var(--ink)}
/* a compact key/value strip for the legal-basis line */
.legalbasis{
margin:.4rem 0 0;display:flex;flex-wrap:wrap;gap:.4rem;
font-family:var(--mono);font-size:.66rem;
}
.legalbasis .lb{
display:inline-flex;align-items:center;gap:.4rem;
border:1px solid var(--border);background:var(--surface-2);
color:var(--muted);padding:.34em .62em;border-radius:7px;letter-spacing:.03em;
}
.legalbasis .lb .lk{color:var(--faint);text-transform:uppercase;letter-spacing:.1em;font-size:.9em}
.legalbasis .lb .lv{color:var(--ink-soft);font-weight:700}
/* closing reconciliation line — quiet, mono, set apart */
.closing{
margin-top:clamp(1.4rem,3vw,2rem);max-width:760px;
border-left:3px solid var(--accent);padding:.4rem 0 .4rem 1.1rem;
font-family:var(--mono);font-size:var(--step--1);color:var(--muted);line-height:1.7;
}
.closing b{color:var(--ink-soft)}
/* back-to-shelf link */
.backlink{
margin-top:1.6rem;display:inline-flex;align-items:center;gap:.5rem;
font-family:var(--mono);font-size:var(--step--1);font-weight:700;letter-spacing:.04em;
color:var(--accent-ink);text-decoration:none;
}
.backlink svg{width:1em;height:1em}
.backlink:hover{text-decoration:underline}
/* =========================================================================
FOOTER (matches index.html)
========================================================================= */
footer{border-top:1px solid var(--border-strong);padding:2.4rem 0 4rem;margin-top:1rem}
footer .fnote{font-family:var(--mono);font-size:var(--step--1);color:var(--muted);line-height:1.7;max-width:80ch}
footer .fnote b{color:var(--ink-soft)}
footer .fnote .prims{color:var(--accent-ink)}
footer .flinks{
margin-top:1.1rem;display:flex;flex-wrap:wrap;gap:.5rem 1.4rem;align-items:center;
font-family:var(--mono);font-size:var(--step--1);
}
footer .flinks a{color:var(--accent-ink);text-decoration:none;font-weight:600}
footer .flinks a:hover{text-decoration:underline}
footer .flinks .sep{color:var(--faint)}
footer .sig{
margin-top:1.4rem;display:flex;flex-wrap:wrap;gap:.5rem 1.2rem;align-items:center;
font-family:var(--mono);font-size:var(--step--1);color:var(--faint);
}
footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)}
/* =========================================================================
MOTION — single calm reveal, reduced-motion safe (matches index.html)
========================================================================= */
@media (prefers-reduced-motion: no-preference){
.reveal{opacity:0;transform:translateY(12px);transition:opacity .6s ease, transform .6s cubic-bezier(.2,.7,.2,1)}
.reveal.in{opacity:1;transform:none}
}
/* =========================================================================
DARK-MODE: keep the section-number tag legible (accent-tinted slab).
========================================================================= */
:root[data-theme="dark"] .sec-head .tag{background:var(--accent-wash);color:var(--accent-ink)}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]) .sec-head .tag{background:var(--accent-wash);color:var(--accent-ink)}
}
/* =========================================================================
PRINT (matches index.html)
========================================================================= */
@media print{
:root{--bg:#fff;--surface:#fff;--surface-2:#f4f4f4;--ink:#000;--ink-soft:#222;--muted:#555;--border:#bbb;--rule:#ddd}
body{background:#fff}
body::before{display:none}
section{page-break-inside:avoid;border-top:1px solid #ccc}
.clause{box-shadow:none;break-inside:avoid}
.reveal{opacity:1!important;transform:none!important}
.themebar{display:none}
}
</style>
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<!-- ============================ MASTHEAD ============================ -->
<header class="masthead wrap">
<div class="themebar" role="group" aria-label="Colour theme">
<div class="tgctl" role="group" aria-label="Colour theme">
<span class="tglabel">Theme</span>
<div class="themeseg">
<button type="button" data-theme-set="light" aria-pressed="false" aria-label="Light theme">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19"/></svg><span>LIGHT</span></button>
<button type="button" data-theme-set="dark" aria-pressed="false" aria-label="Dark theme">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></svg><span>DARK</span></button>
</div>
</div>
</div>
<div class="topline">
<span><strong><a href="index.html">POUYA'S FIELD GUIDES</a></strong></span>
<span class="sep">/</span>
<span>Plain-English tutorials for normal humans</span>
<span class="doc-id num">PRIVACY / 00</span>
</div>
<p class="kicker" style="margin:0 0 .9rem">Transparency</p>
<h1>Privacy</h1>
<p class="sub">No cookies &middot; no tracking &middot; no analytics.</p>
<p class="lede">What this site does and doesn't do with your visit — the
<b>short, honest version</b>.</p>
</header>
<main id="main" class="wrap">
<!-- ============================ THE CLAUSES ============================ -->
<section aria-labelledby="privacy-h">
<div class="sec-head">
<span class="tag">POLICY</span>
<h2 id="privacy-h">How your visit is handled</h2>
<p class="note">Plain English. Last reviewed 2026-06-04.</p>
</div>
<div class="clauses">
<!-- 01 — the headline promise -->
<article class="clause lead reveal" aria-labelledby="cl-1">
<div class="c-rail">
<span class="c-no num" aria-hidden="true">01</span>
<span class="c-topic">The promise</span>
</div>
<h2 id="cl-1">No cookies. No JavaScript tracking. No third-party analytics.</h2>
<p>Nothing about your visit is sent to any other company, and nothing is
<b>shared or sold</b>. The pages don't phone home.</p>
</article>
<!-- 02 — what the server records -->
<article class="clause reveal" aria-labelledby="cl-2">
<div class="c-rail">
<span class="c-no num" aria-hidden="true">02</span>
<span class="c-topic">What's recorded</span>
</div>
<h2 id="cl-2">What the server records</h2>
<p>Like every web server, it keeps short-lived access logs. For each request
that means:</p>
<ul class="facts">
<li>the page requested</li>
<li>the time of the request</li>
<li>the referring link, if any</li>
<li>the browser type</li>
<li>a <b>truncated IP address</b> — the last part is removed, so it
identifies a <b>country, not a person</b></li>
</ul>
<p style="margin-top:.85rem">From these, only <b>aggregate, country-level</b>
visit counts are kept. Per-visitor IP detail is disabled.</p>
</article>
<!-- 03 — why / legal basis -->
<article class="clause reveal" aria-labelledby="cl-3">
<div class="c-rail">
<span class="c-no num" aria-hidden="true">03</span>
<span class="c-topic">Why &amp; legal basis</span>
</div>
<h2 id="cl-3">Why this is collected</h2>
<p>To keep the site running and secure, and to see roughly which guides get
read and from where.</p>
<div class="legalbasis" aria-label="Legal basis">
<span class="lb"><span class="lk">Basis</span><span class="lv">Legitimate interest &middot; GDPR Art. 6(1)(f)</span></span>
<span class="lb"><span class="lk">Security</span><span class="lv">Recital 49</span></span>
</div>
<p style="margin-top:.85rem">No consent banner is needed because there are
<b>no cookies and no client-side tracking</b>.</p>
</article>
<!-- 04 — retention -->
<article class="clause reveal" aria-labelledby="cl-4">
<div class="c-rail">
<span class="c-no num" aria-hidden="true">04</span>
<span class="c-topic">Retention</span>
</div>
<h2 id="cl-4">How long anything is kept</h2>
<p>Raw logs are rotated and deleted within <b>about 7 days</b>. Only the
anonymized aggregates are kept longer.</p>
</article>
<!-- 05 — nothing to act on -->
<article class="clause reveal" aria-labelledby="cl-5">
<div class="c-rail">
<span class="c-no num" aria-hidden="true">05</span>
<span class="c-topic">Nothing to act on</span>
</div>
<h2 id="cl-5">Nothing to opt out of</h2>
<p>Because what's recorded is anonymized and only ever aggregated, there's
<b>no personal record here to access, correct, or delete</b> — and nothing to
opt out of.</p>
<p>The only time a full IP address exists is the split-second, in-memory
rate-limit check that keeps the site up; it is never written to a log or
stored. That's the whole point of building it this way.</p>
</article>
</div>
<!-- closing reconciliation -->
<p class="closing reveal">
When this site says <b>"no tracking,"</b> it means no client-side tracking and
no cookies. The only record is anonymized, short-lived server logs.
</p>
<a class="backlink" href="index.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5M11 18l-6-6 6-6"/></svg>
Back to the shelf
</a>
</section>
</main>
<!-- ============================ FOOTER ============================ -->
<footer class="wrap">
<p class="fnote">
A small, slow-growing library of plain-English field guides —
<span class="prims">written to be read once, calmly, and then forgotten about</span>.
<br><br>
<b>Served as a static site.</b> The source of truth lives in the
<a href="https://git.pouya.duckdns.org/public/tutorials" target="_blank" rel="noopener"><code>public/tutorials</code></a> Gitea repository; this page and every guide are plain
self-contained HTML, built from there.
</p>
<div class="flinks">
<a href="index.html">The shelf</a>
<span class="sep">&middot;</span>
<a href="privacy.html">Privacy</a>
<span class="sep">&middot;</span>
<a href="https://git.pouya.duckdns.org/public/tutorials" target="_blank" rel="noopener">View the source repository &rarr;</a>
</div>
<div class="sig">
<span class="dot" aria-hidden="true"></span>
<span>Pouya's Field Guides</span>
<span class="num">PRIVACY&middot;00</span>
<span>Plain-English tutorials for normal humans</span>
</div>
</footer>
<script>
/* =========================================================================
THEME TOGGLE — explicit light/dark override on :root[data-theme].
Default: localStorage choice -> else follow the OS (prefers-color-scheme,
handled in CSS). With JS off, the CSS media query still themes the page,
so nothing breaks. Uses the SAME store key as the guide pages so the
visitor's choice is consistent across the whole site.
(Reused verbatim from the password-guide design system.)
========================================================================= */
(function(){
var STORE = 'pwguide-theme';
var root = document.documentElement;
var buttons = document.querySelectorAll('.themeseg button[data-theme-set]');
function systemPref(){
return matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function current(){
return root.getAttribute('data-theme') || systemPref();
}
function paint(theme){
buttons.forEach(function(b){
b.setAttribute('aria-pressed', b.getAttribute('data-theme-set') === theme ? 'true' : 'false');
});
}
function apply(theme, persist){
if(theme !== 'light' && theme !== 'dark') theme = systemPref();
root.setAttribute('data-theme', theme);
paint(theme);
if(persist){ try{ localStorage.setItem(STORE, theme); }catch(e){} }
}
var saved = null;
try{ saved = localStorage.getItem(STORE); }catch(e){}
if(saved === 'light' || saved === 'dark'){ apply(saved, false); }
else { paint(systemPref()); } /* leave data-theme unset -> CSS media query drives it */
buttons.forEach(function(b){
b.addEventListener('click', function(){ apply(b.getAttribute('data-theme-set'), true); });
});
})();
/* =========================================================================
CALM SINGLE REVEAL — fade the policy clauses in on first view.
Honors reduced-motion and degrades gracefully without IntersectionObserver.
(Reused from the password-guide design system.)
========================================================================= */
(function(){
var els = document.querySelectorAll('.reveal');
if(!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches){
els.forEach(function(el){ el.classList.add('in'); });
return;
}
var io = new IntersectionObserver(function(entries){
entries.forEach(function(en){
if(en.isIntersecting){ en.target.classList.add('in'); io.unobserve(en.target); }
});
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
els.forEach(function(el){ io.observe(el); });
})();
</script>
</body>
</html>