<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://obaid.wtf/feed.xml" rel="self" type="application/atom+xml" /><link href="https://obaid.wtf/" rel="alternate" type="text/html" /><updated>2026-06-22T03:18:04+05:00</updated><id>https://obaid.wtf/feed.xml</id><title type="html">obaid’s longer thoughts</title><subtitle>politics + systems + economics + markets + binary technologies + innovation + sciences + futbol + overton window/pop culture this is very prone to change as I add, drop more interests</subtitle><author><name>obaid</name></author><entry><title type="html">built a rival to the world’s largest fanfiction platforms — alone, at 17, on a $5/mo droplet</title><link href="https://obaid.wtf/jotbook/2026/06/20/built-a-rival-to-the-worlds-largest-fanfiction-platforms.html" rel="alternate" type="text/html" title="built a rival to the world’s largest fanfiction platforms — alone, at 17, on a $5/mo droplet" /><published>2026-06-20T00:00:00+05:00</published><updated>2026-06-20T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/06/20/built-a-rival-to-the-worlds-largest-fanfiction-platforms</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/06/20/built-a-rival-to-the-worlds-largest-fanfiction-platforms.html"><![CDATA[<style>
* { box-sizing: border-box; }

#ffo-bar { position: fixed; top: 0; left: 0; height: 3px; width: 0; z-index: 9999;
  background: linear-gradient(90deg, #c2255c, #d9a441, #5aa9e6); }

.ffo-reveal { opacity: 0; transform: translateY(28px);
  transition: opacity .8s cubic-bezier(.16,1,.3,1), transform .8s cubic-bezier(.16,1,.3,1); }
.ffo-reveal.in { opacity: 1; transform: none; }

/* stat band */
.ffo-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  gap: 1px; background: rgba(194,37,92,.15); border: 1px solid rgba(194,37,92,.2);
  border-radius: 14px; overflow: hidden; margin: 2rem 0; }
.ffo-stat { background: #f9f6ef; padding: 1.1rem .7rem; text-align: center; }
.ffo-stat .n { font-family: 'Source Serif 4', serif; font-size: clamp(1.5rem,4vw,2.1rem);
  font-weight: 600; color: #c2255c; line-height: 1; display: block; white-space: nowrap; }
.ffo-stat .l { font-family: 'Roboto Mono', monospace; font-size: .58rem; letter-spacing: .1em;
  text-transform: uppercase; color: #999; margin-top: .4rem; display: block; }

/* search filter widget */
.ffo-search { background: #f9f6ef; border: 1px solid rgba(194,37,92,.2); border-radius: 16px;
  padding: 1.6rem; margin: 1.5rem 0; }
.ffo-search-title { font-family: 'Roboto Mono', monospace; font-size: .68rem; letter-spacing: .18em;
  text-transform: uppercase; color: #c2255c; margin-bottom: 1rem; display: block; }
.ffo-filter-row { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .8rem; align-items: center; }
.ffo-filter-label { font-family: 'Roboto Mono', monospace; font-size: .68rem; color: #999;
  letter-spacing: .1em; text-transform: uppercase; width: 80px; flex-shrink: 0; }
.ffo-tag { display: inline-flex; align-items: center; gap: .3rem; padding: .3rem .7rem;
  border-radius: 999px; border: 1px solid rgba(0,0,0,.12); font-family: 'Roboto Mono', monospace;
  font-size: .74rem; cursor: pointer; transition: .15s; background: #fff; color: #555; user-select: none; }
.ffo-tag:hover { border-color: #2a9d5c; color: #2a9d5c; }
.ffo-tag.inc { background: #2a9d5c; color: #fff; border-color: #2a9d5c; }
.ffo-tag.exc { background: #c2255c; color: #fff; border-color: #c2255c; text-decoration: line-through; }
.ffo-tag-hint { font-family: 'Roboto Mono', monospace; font-size: .65rem; color: #bbb;
  margin-bottom: .8rem; }
.ffo-range-row { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: .8rem; }
.ffo-range-group { display: flex; flex-direction: column; gap: .3rem; }
.ffo-range-group label { font-family: 'Roboto Mono', monospace; font-size: .65rem; color: #999;
  text-transform: uppercase; letter-spacing: .1em; }
.ffo-range-group input[type=range] { width: 140px; accent-color: #c2255c; }
.ffo-range-val { font-family: 'Roboto Mono', monospace; font-size: .72rem; color: #c2255c; }
.ffo-results { margin-top: 1rem; border-top: 1px solid rgba(194,37,92,.1); padding-top: 1rem; }
.ffo-result-count { font-family: 'Roboto Mono', monospace; font-size: .72rem; color: #999;
  margin-bottom: .6rem; }
.ffo-result-count span { color: #c2255c; font-weight: 600; }
.ffo-story-row { display: flex; justify-content: space-between; align-items: flex-start;
  padding: .55rem 0; border-bottom: 1px solid rgba(0,0,0,.05); gap: 1rem; }
.ffo-story-row:last-child { border-bottom: none; }
.ffo-story-title { font-size: .9rem; font-weight: 600; color: #111; }
.ffo-story-meta { font-family: 'Roboto Mono', monospace; font-size: .68rem; color: #888; margin-top: .2rem; }
.ffo-story-tags { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .3rem; }
.ffo-story-tag { font-family: 'Roboto Mono', monospace; font-size: .62rem; padding: .1rem .4rem;
  border-radius: 4px; background: rgba(194,37,92,.07); color: #c2255c; }
.ffo-wc { font-family: 'Roboto Mono', monospace; font-size: .68rem; color: #aaa;
  white-space: nowrap; flex-shrink: 0; }

/* feature list */
.ffo-feats { margin: 1rem 0; columns: 2; column-gap: 2rem; }
@media (max-width: 640px) { .ffo-feats { columns: 1; } }
.ffo-feat { display: flex; gap: .5rem; align-items: baseline;
  padding: .4rem 0; border-bottom: 1px solid rgba(0,0,0,.06);
  break-inside: avoid; }
.ffo-feat .ico { font-size: .85rem; flex-shrink: 0; }
.ffo-feat h5 { font-family: 'Roboto Mono', monospace; font-size: .74rem; font-weight: 600;
  color: #222; margin: 0; white-space: nowrap; flex-shrink: 0; }
.ffo-feat p { font-size: .76rem; margin: 0; color: #999; }
.ffo-feat p::before { content: "– "; color: #ccc; }

/* timeline */
.ffo-tl { position: relative; margin: 1.5rem 0; padding-left: 28px; }
.ffo-tl::before { content: ""; position: absolute; left: 6px; top: 4px; bottom: 4px;
  width: 2px; background: linear-gradient(#c2255c, #d9a441, #5aa9e6); }
.ffo-ev { position: relative; margin-bottom: 1.6rem; }
.ffo-ev::before { content: ""; position: absolute; left: -28px; top: 4px; width: 14px;
  height: 14px; border-radius: 50%; background: #f4f0e8; border: 2px solid #c2255c;
  box-shadow: 0 0 0 3px rgba(194,37,92,.1); }
.ffo-ev.blue::before { border-color: #5aa9e6; box-shadow: 0 0 0 3px rgba(90,169,230,.1); }
.ffo-ev.green::before { border-color: #2a9d5c; box-shadow: 0 0 0 3px rgba(42,157,92,.1); }
.ffo-ev.rose::before { border-color: #e07a9b; box-shadow: 0 0 0 3px rgba(224,122,155,.1); }
.ffo-ev .date { font-family: 'Roboto Mono', monospace; font-size: .67rem; letter-spacing: .08em;
  color: #c2255c; text-transform: uppercase; }
.ffo-ev h4 { font-family: 'Source Serif 4', serif; font-size: 1.05rem; font-weight: 600;
  margin: .15rem 0 .2rem; color: #111; }
.ffo-ev p { margin: 0; font-size: .9rem; color: #555; }

/* PM */
.ffo-pm { background: #eef4fb; border: 1px solid rgba(90,169,230,.3); border-radius: 12px;
  padding: 1.4rem 1.6rem; margin: 1.5rem 0; }
.ffo-pm-from { display: flex; align-items: center; gap: .6rem;
  border-bottom: 1px solid rgba(90,169,230,.2); padding-bottom: .8rem; margin-bottom: .9rem; }
.ffo-pm-av { width: 36px; height: 36px; border-radius: 50%;
  background: linear-gradient(135deg, #5aa9e6, #2a9d5c); display: grid;
  place-items: center; font-weight: 700; color: #fff; font-size: .9rem; flex-shrink: 0; }
.ffo-pm-meta b { color: #111; font-size: .9rem; display: block; }
.ffo-pm-meta span { font-family: 'Roboto Mono', monospace; font-size: .68rem; color: #888; }
.ffo-pm-subj { font-weight: 600; color: #5aa9e6; margin-bottom: .5rem; font-size: .95rem; }
.ffo-pm-body { color: #333; font-size: .93rem; line-height: 1.65; }
.ffo-pm-body .u { color: #c2255c; font-weight: 600; }

/* table */
.ffo-tbl { width: 100%; border-collapse: collapse; margin: 1.3rem 0; font-size: .88rem; background: none; }
.ffo-tbl th, .ffo-tbl td { border: none; background: none; }
.ffo-tbl thead th { text-align: left; font-family: 'Roboto Mono', monospace; font-size: .62rem;
  letter-spacing: .12em; text-transform: uppercase; color: #9a9a9a; font-weight: 600;
  padding: .5rem .7rem; border-bottom: 1.5px solid rgba(0,0,0,.14); }
.ffo-tbl tbody tr { background: none; }
.ffo-tbl tbody td { padding: .6rem .55rem; color: #444; vertical-align: middle;
  border-bottom: 1px solid rgba(0,0,0,.07); }
.ffo-tbl tbody tr:last-child td { border-bottom: none; }
.ffo-tbl tbody tr:hover td { background: rgba(0,0,0,.022); }
.ffo-tbl td:first-child { color: #111; font-weight: 600; }
.ffo-tbl .favs { color: #c2255c; font-family: 'Roboto Mono', monospace; font-size: .82rem; }
/* deploy table: monospace figures, right-aligned counts */
.ffo-tbl-deploy td:nth-child(1) { font-family: 'Roboto Mono', monospace; font-size: .8rem; white-space: nowrap; }
.ffo-tbl-deploy td:nth-child(1) small { color: #aaa; font-weight: 400; }
.ffo-tbl-deploy th:nth-child(2), .ffo-tbl-deploy td:nth-child(2) { text-align: right; }
.ffo-tbl-deploy td:nth-child(2) { font-family: 'Roboto Mono', monospace; font-size: .8rem;
  color: #888; white-space: nowrap; }
.ffo-tbl-deploy td:nth-child(3) { white-space: nowrap; }
.ffo-tbl-deploy td:nth-child(4) { color: #555; width: 40%; }

/* callout */
.ffo-callout { background: rgba(217,164,65,.06); border: 1px solid rgba(217,164,65,.18);
  border-left: 3px solid #d9a441;
  border-radius: 12px; padding: 1.2rem 1.4rem; margin: 1.5rem 0; }
.ffo-callout .tag { font-family: 'Roboto Mono', monospace; font-size: .65rem; letter-spacing: .15em;
  text-transform: uppercase; color: #b8862a; display: block; margin-bottom: .4rem; }
.ffo-callout p { margin: .3rem 0; font-size: .93rem; color: #444; }

/* crash box */
.ffo-crash { background: rgba(224,122,155,.06); border: 1px solid rgba(224,122,155,.18);
  border-left: 3px solid #c2255c;
  border-radius: 12px; padding: 1.2rem 1.4rem; margin: 1.5rem 0; }
.ffo-crash .tag { font-family: 'Roboto Mono', monospace; font-size: .65rem; letter-spacing: .15em;
  text-transform: uppercase; color: #c2255c; display: block; margin-bottom: .4rem; }
.ffo-crash p { margin: .3rem 0; font-size: .93rem; color: #444; }

/* venn */
.ffo-venn { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center;
  justify-content: center; font-family: 'Roboto Mono', monospace; font-size: .82rem;
  margin: 1.2rem 0; color: #666; }
.ffo-pill { padding: .35rem .8rem; border-radius: 999px; border: 1px solid #ddd; white-space: nowrap; }
.ffo-pill.inc { border-color: #2a9d5c; color: #2a9d5c; }
.ffo-pill.exc { border-color: #c2255c; color: #c2255c; }
.ffo-pill.op { border: none; color: #b8862a; font-size: 1rem; }
.ffo-pill.res { background: #c2255c; color: #fff; border-color: #c2255c; font-weight: 600; }

/* code */
.ffo-pre { background: #1a1208; border: 1px solid rgba(217,164,65,.2); border-radius: 10px;
  padding: 1rem 1.2rem; overflow-x: auto; font-family: 'Roboto Mono', monospace;
  font-size: .8rem; color: #c8bfb0; margin: 1.2rem 0; line-height: 1.6; }
.ffo-pre .c { color: #6b6358; } .ffo-pre .k { color: #d9a441; }
.ffo-pre .s { color: #7fc8a9; } .ffo-pre .v { color: #5aa9e6; }

/* mail grid */
.ffo-mail { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: .6rem; margin: 1.2rem 0; }
.ffo-mail-chip { background: #f9f6ef; border: 1px solid rgba(194,37,92,.15); border-radius: 9px;
  padding: .8rem .9rem; font-family: 'Roboto Mono', monospace; font-size: .75rem; color: #555; }
.ffo-mail-chip .type { color: #c2255c; font-weight: 600; display: block;
  margin-bottom: .15rem; font-size: .78rem; }

/* server chip */
.ffo-chip { display: inline-flex; align-items: center; font-family: 'Roboto Mono', monospace;
  font-size: .78rem; background: rgba(90,169,230,.1); border: 1px solid rgba(90,169,230,.3);
  border-radius: 6px; padding: .2rem .55rem; color: #2d7aad; }

/* AI voice blocks — Opus co-author interjections */
.ffo-ai { background: #f6f9fb;
  border: 1px solid rgba(70,110,140,.12);
  border-left: 3px solid #9cc2dc;
  border-radius: 12px; padding: 1.2rem 1.4rem; margin: 1.5rem 0; }
.ffo-ai-label { font-family: 'Roboto Mono', monospace; font-size: .6rem; letter-spacing: .04em;
  text-transform: uppercase; font-weight: 600; color: #8593a2;
  display: flex; align-items: center; gap: .45rem; margin-bottom: .6rem; }
.ffo-ai-label::before { content: ""; width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(135deg, #5aa9e6, #2a9d5c); }
.ffo-ai > p { font-size: .92rem; line-height: 1.75; color: #41505d; margin: .5rem 0; }
.ffo-ai > p:first-of-type { margin-top: 0; }
.ffo-ai > p:last-child { margin-bottom: 0; }
.ffo-ai code { font-family: 'Roboto Mono', monospace; font-size: .82em;
  background: rgba(90,169,230,.1); padding: .1em .35em; border-radius: 3px; color: #2d6a8a; }

/* one layout: a capped parent, children fill it */
.post-content, .post-header {
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 1.1rem;
  padding-right: 1.1rem;
  box-sizing: border-box;
}
.post-content > p,
.post-content > h2,
.post-content > h3,
.post-content > h4,
.post-content > ul,
.post-content > ol,
.post-content > blockquote,
.post-content > hr,
.ffo-ai, .ffo-callout, .ffo-crash, .ffo-pm, .ffo-tl,
.ffo-vfs-chain, .ffo-flow, .ffo-feats, .ffo-chips,
.ffo-stats, .ffo-search {
  max-width: 100%;
  margin-left: auto;
  margin-right: auto;
}

/* feature chips */
.ffo-chips { display: flex; flex-wrap: wrap; gap: .45rem; }
.ffo-chip-item { font-family: 'Roboto Mono', monospace; font-size: .74rem;
  padding: .3rem .7rem; border-radius: 999px; background: #f9f6ef;
  border: 1px solid rgba(194,37,92,.15); color: #444; white-space: nowrap; }

/* request lifecycle flow */
.ffo-flow { margin: 1rem 0 1.2rem; display: flex; flex-direction: column; gap: 0; }
.ffo-flow-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.ffo-flow-box { font-family: 'Roboto Mono', monospace; font-size: .78rem; line-height: 1.5;
  padding: .6rem .9rem; border-radius: 8px; color: #3a4a5a; flex: 1; min-width: 160px; }
.ffo-flow-box.server { background: rgba(194,37,92,.07); border: 1px solid rgba(194,37,92,.2); }
.ffo-flow-box.dom { background: rgba(90,169,230,.08); border: 1px solid rgba(90,169,230,.25); }
.ffo-flow-box.client { background: rgba(42,157,92,.07); border: 1px solid rgba(42,157,92,.2); }
.ffo-flow-box.send { background: rgba(217,164,65,.08); border: 1px solid rgba(217,164,65,.3); }
.ffo-flow-box.back { background: rgba(90,169,230,.08); border: 1px solid rgba(90,169,230,.25); width: 100%; flex: none; }
.ffo-flow-box small { display: block; font-size: .68rem; color: #888; margin-top: .2rem; }
.ffo-flow-arrow { font-size: 1rem; color: #bbb; flex-shrink: 0; }
.ffo-flow-down { font-family: 'Roboto Mono', monospace; font-size: .68rem; color: #aaa;
  letter-spacing: .1em; padding: .3rem 0 .3rem .5rem; }

/* poll request "wire" — one round-trip, what goes up / what comes down */
.ffo-wire { margin: 1.2rem 0 1.3rem; border: 1px solid rgba(70,110,140,.22);
  border-radius: 12px; overflow: hidden; background: #fff; }
.ffo-wire-head { font-family: 'Roboto Mono', monospace; font-size: .62rem;
  letter-spacing: .16em; text-transform: uppercase; color: #6b7d8c; font-weight: 700;
  padding: .6rem .9rem; border-bottom: 1px solid rgba(70,110,140,.14);
  background: rgba(70,110,140,.04); }
.ffo-wire-cols { display: grid; grid-template-columns: 1fr 1fr; }
.ffo-wire-col { padding: .85rem .9rem; }
.ffo-wire-col.up { border-right: 1px solid rgba(70,110,140,.12); }
.ffo-wire-tag { font-family: 'Roboto Mono', monospace; font-size: .6rem;
  letter-spacing: .1em; font-weight: 700; text-transform: uppercase; margin-bottom: .55rem; }
.ffo-wire-col.up .ffo-wire-tag { color: #2a9d5c; }
.ffo-wire-col.down .ffo-wire-tag { color: #5aa9e6; }
.ffo-wire-line { display: flex; gap: .55rem; align-items: baseline; padding: .22rem 0;
  font-size: .82rem; color: #5a6877; line-height: 1.45; }
.ffo-wire-line code { font-family: 'Roboto Mono', monospace; font-size: .74rem;
  font-weight: 600; color: #2d6a8a; white-space: nowrap; }
.ffo-wire-server { border-top: 1px dashed rgba(194,37,92,.3);
  background: rgba(194,37,92,.05); padding: .75rem .9rem; }
.ffo-wire-server .ffo-wire-tag { color: #b03060; }
.ffo-wire-server-line { font-family: 'Roboto Mono', monospace; font-size: .76rem;
  color: #6a5560; padding: .2rem 0; line-height: 1.5; }
.ffo-wire-server-line code { color: #b03060; font-weight: 600; }
@media (max-width: 560px) { .ffo-wire-cols { grid-template-columns: 1fr; }
  .ffo-wire-col.up { border-right: none; border-bottom: 1px solid rgba(70,110,140,.12); } }

/* VFS identity chain */
.ffo-vfs-chain { margin: 1rem 0 1.2rem; display: flex; flex-direction: column; gap: 0; }
.ffo-vfs-step { display: flex; gap: .9rem; align-items: flex-start;
  background: rgba(90,169,230,.06); border: 1px solid rgba(90,169,230,.2);
  border-radius: 10px; padding: .85rem 1rem; }
.ffo-vfs-new { background: rgba(42,157,92,.06); border-color: rgba(42,157,92,.25); }
.ffo-vfs-n { font-family: 'Roboto Mono', monospace; font-size: .8rem; font-weight: 700;
  color: #5aa9e6; background: rgba(90,169,230,.15); border-radius: 50%;
  width: 24px; height: 24px; display: grid; place-items: center; flex-shrink: 0; margin-top: .1rem; }
.ffo-vfs-new .ffo-vfs-n { color: #2a9d5c; background: rgba(42,157,92,.15); }
.ffo-vfs-body { font-size: .88rem; color: #3a4a5a; line-height: 1.55; }
.ffo-vfs-body strong { color: #2d6a8a; display: block; margin-bottom: .2rem; font-size: .85rem; }
.ffo-vfs-new .ffo-vfs-body strong { color: #1e7a47; }
.ffo-vfs-arrow { font-family: 'Roboto Mono', monospace; font-size: .68rem; color: #aaa;
  letter-spacing: .1em; padding: .25rem 0 .25rem 2.1rem; }

/* bundler steps — folded into the editorial block, flat hairline rows */
.ffo-steps { margin: 1rem 0 .1rem; }
.ffo-step { padding: .85rem 0; border-top: 1px solid rgba(70,110,140,.16); }
.ffo-step:first-child { border-top: none; padding-top: .2rem; }
.ffo-step:last-child { padding-bottom: 0; }
.ffo-step-h { display: flex; align-items: baseline; gap: .55rem; margin-bottom: .4rem; }
.ffo-step-n { font-family: 'Roboto Mono', monospace; font-size: .72rem; font-weight: 700; color: #9cc2dc; }
.ffo-step-name { font-family: 'Roboto Mono', monospace; font-size: .72rem; letter-spacing: .13em;
  text-transform: uppercase; font-weight: 600; color: #2d6a8a; }
.ffo-step-desc { font-size: .9rem; line-height: 1.6; color: #41505d; margin: 0 0 .5rem; }
.ffo-steps .ffo-step-code { display: inline-block; max-width: 100%; font-family: 'Roboto Mono', monospace;
  font-size: .76rem; color: #d6c9b2; background: #1a1208; border-radius: 6px; padding: .42rem .7rem;
  overflow-x: auto; white-space: nowrap; vertical-align: middle; }
.ffo-step-note { display: block; font-size: .76rem; line-height: 1.45; color: #7e8c99; margin-top: .45rem; }

/* architecture grouped feature map */
.ffo-arch { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
@media (max-width: 640px) { .ffo-arch { grid-template-columns: 1fr; } }
.ffo-arch-group { padding: .7rem 0; }
.ffo-arch-group:nth-child(-n+2) { border-bottom: 1px solid rgba(70,110,140,.1); }
@media (max-width: 640px) { .ffo-arch-group { border-bottom: 1px solid rgba(70,110,140,.1); }
  .ffo-arch-group:last-child { border-bottom: none; } }
.ffo-arch-group:nth-child(odd) { padding-right: 1.2rem; }
.ffo-arch-group:nth-child(even) { padding-left: 1.2rem; border-left: 1px solid rgba(70,110,140,.1); }
@media (max-width: 640px) { .ffo-arch-group:nth-child(odd) { padding-right: 0; }
  .ffo-arch-group:nth-child(even) { padding-left: 0; border-left: none; } }
.ffo-arch-label { font-family: 'Roboto Mono', monospace; font-size: .58rem; letter-spacing: .18em;
  text-transform: uppercase; color: #9cc2dc; font-weight: 700; margin-bottom: .45rem; }
.ffo-arch-items { display: flex; flex-direction: column; gap: 0; }
.ffo-arch-item { display: flex; align-items: baseline; gap: .5rem;
  padding: .3rem 0; border-bottom: 1px solid rgba(0,0,0,.04); }
.ffo-arch-item:last-child { border-bottom: none; }
.ffo-arch-item code { font-family: 'Roboto Mono', monospace; font-size: .74rem; font-weight: 600;
  color: #2d6a8a; background: none; padding: 0; white-space: nowrap; flex-shrink: 0; }
.ffo-arch-item span { font-size: .78rem; color: #7e8c99; line-height: 1.4; }

/* +/- diff figures in the deploy table */
.ffo-diff { font-family: 'Roboto Mono', monospace; font-size: .76rem; white-space: nowrap; line-height: 1.5; }
.ffo-diff .add { display: block; color: #1e7a47; font-weight: 600; }
.ffo-diff .del { display: block; color: #b51f50; font-weight: 600; }
</style>

<div id="ffo-bar"></div>

<hr />

<p><code class="language-plaintext highlighter-rouge">co-authors: Obaid, Opus4.8</code></p>

<p><code class="language-plaintext highlighter-rouge">this post is an experiment in parallelizing the power of LLM research with telling my own story, something I've found surprisingly difficult to do. if this works (in terms of telling my story the way i want to), expect more</code></p>

<p><code class="language-plaintext highlighter-rouge">the narrative outside blocks is all mine, and within all Opus 4.8</code></p>

<h2 id="prologue">Prologue</h2>

<p>Since I was 7 years old, I’ve been an avid reader. When I look at how old kids are at 7 now it is hard to believe myself, but thankfully I do have the receipts which means I don’t end up gaslighting myself — stale digital records of torrented books and pixelated videos through Nokia handhelds and me reading as people shout, tables fall, and my little world (the home I knew) descends into chaos all around.</p>

<p>Of course, I was a technically savvy kid. At around the same age (I don’t remember when), there was an incident: somehow, I ended up changing the WiFi password of our network without knowing anything or ever remembering using the 192.168… local address. To this day, I don’t know how that was possible, but I have a backup in trusted, shared memory from my brother who still remembers pressing me on how I did that — the answer I had for him then is the same I would have now: I was just playing with the settings and I don’t know.</p>

<p>Later when my brother (CS major, top of his batch, and one of the first iOS developers at Venture Dive/Careem) got too busy, this meant the duties of managing our home’s technical admin naturally fell to me.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, jumping in to comment on the veracity of this</span>
<p>Git can't confirm the WiFi story, but it fits the rest of what the commits show. Whoever wrote these 298 commits learned by breaking things instead of reading the manual first. Deleting <code>wp-includes</code> one folder at a time just to see what would fall over is the same kind of thing a seven-year-old does messing with the router settings.</p>
</div>

<p>I was also devious: I remember using carefully placed mirrors to find people’s device passwords and secretly used them at night when no one could find out — having bounded creativity and strict parents will do that to you.</p>

<p>The same restrictions extended to books. The way I used to devour books grew from something initially supported into apparently serious concern. My book-reading time became limited, and so did the restrictions on their content. My mom didn’t understand it, and so she put a halt to it. To this day one of the principles I’ve derived for my life has an origin in learning of how harmful this is.</p>

<p>I remember when I borrowed a book from the son of a family friend, <em>Alex Rider: Eagle Strike</em>, my mom had my brother read it cover-to-cover and with a black marker erase all text that had anything to do with girl-to-boy interactions or “kissing”. I can’t tell you what it did to my psyche over the next 10 years, but I can tell you that extensive control extended far further.</p>

<p>I cried when I found out, not because of the censorship, that I was used to, but because I didn’t know I could explain the markings when giving it back. And my mother knew who I’d borrowed it from, so it was actually even more insane. I’m almost cracking up thinking about the absolute insanity of it now. Though in hindsight, that was probably for the better: at least someone from the outside had a window into this insanity.</p>

<p>This is sounding more like a confessional therapy session than it does a story about a fanfiction site, but perhaps that is the necessary prelude I’ve been wanting to put out all along.</p>

<hr />

<p>I learned to torrent because of my obsession with books too. My mom used to take me to a used book stall in Hyderi, where the books were stacked taller than our height, no categorization, and prices were between Rs 100–400. Our monthly trip had a limit of 4 books per sibling, no more, and that, obviously, was not nearly enough for me.</p>

<p>I learned to torrent. I knew what it was because my siblings used to download the matriarch-approved flicks to put on show for the family, but the recipes for those were always gatekept. As anything as an early GenZ, I used the internet to find that recipe.</p>

<p>The books I downloaded were nearly always continuations of series I’d started but never found the full cycles for, and soon I remembered not to beg my mom to go to Liberty Books or ask for books I couldn’t find at the stalls. My reading had become all-digital.</p>

<p>I remember on our first-ever out-of-Karachi trip, to Islamabad, we stumbled into Saeed Book Bank and I found on the shelf the then newly-released finale of the Artemis Fowl series: <em>The Last Guardian</em>. I was hooked, but I knew to skim through for as long as we were there, and then quietly get up only to launch uTorrent the moment I got back home to Karachi.</p>

<p>Soon, that desire for continuation turned into something else: the discovery of fanfiction. A way to stay in denial about the stories that I’d learned had already ended.</p>

<p>My sister had a role in that introduction to this world, and learning what I did later about the depths of insanity it went to, I often wondered about her adult due-dilligence in that.</p>

<p>My life up till that point represented the new. I was determined to turn everyone using anything backwards up to that point — whether that was paper records not yet adapted to Google Sheets, or literally anything else — into the “modern” way. And that desire extended to my newly discovered fanfiction community with a new website, the “modern” way of reading fanfiction.</p>

<p><strong>fanfiction.online.</strong></p>

<hr />

<h2 id="chapter-one--the-build">Chapter One · The Build</h2>

<p>I launched this under an anonymous name, very easy to do as I’d found in my Reddit stalking, primarily out of fear of retribution from my family or other circles I was in accidentally discovering me.</p>

<p>And I began making a more modern website. The thing is, I didn’t know how. I’d used WordPress to create websites before and my early foray into it at 8/9, learned to make HTML and JavaScript websites, but the only full live tool I knew how to use was WordPress. I’d used WordPress to create a few sites then, and chose to use it again. And I did.</p>

<p>I knew WordPress.org existed with more customization. And I knew how to use one-click deployments. So I used my dad’s newly (badly sustaining) business website that used a cPanel hosting and that I managed, and set up an alternate site on it.</p>

<p>And it was live. But the preset theme I used didn’t make sense. And the admin panel, despite all the hooks and customizations to remove panels, was still clunky. And I didn’t want the WordPress logo to show anywhere.</p>

<p>So one-by-one, step-by-step, in one of the greatest learning curves I think I have surpassed in my life, I learned to build all of that on PHP and from scratch, learning what code even was along the way. The process was simple: I saw a button I didn’t like, and googled “hook to remove xyz button”, kept researching until I found the right guide, article, reddit thread, or stack overflow post. Tried a few, then dug deep into the HOW when I found one that worked.</p>

<p>Slowly, this turned into me replacing the entire admin panel with a custom one, using the same APIs, and then even not, stripping away all aspects of the WordPress core until I was able to delete the entire wp_includes directory and only the MySQL database schema remained.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the repos and commit history</span>
<p>By 2020 it wasn't WordPress — only its MySQL tables remained. A custom PHP <code>Router</code> in one <code>index.php</code> caught every request; templates, auth, mail, and analytics were all rewritten, one Stack Overflow answer at a time, on a <strong>$5/mo shared cPanel plan</strong>. The real code is the <code>wp_ffonline</code> repo's <code>Custom-Routing</code> branch — 298 commits to <code>master</code>'s 6. The tell: an April 2020 message, <code>"removed wp-includes"</code>.</p>

<div class="ffo-tl" style="margin-top:.9rem">
  <div class="ffo-ev rose">
    <div class="date">Sep 20, 2019 · 08:29 AM · Karachi</div>
    <h4>Earliest proof of life</h4>
    <p>A Wordfence login alert — user <code>obaid</code>, IP <code>45.116.232.52</code> — is the oldest trace of <code>fanfiction.online</code>. WordPress already live, Wordfence installed. Seven months before git opened.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Apr 18, 2020 · first commit</div>
    <h4>Git enters a site that already exists</h4>
    <p>300+ files in one commit: custom theme <code>book-writer</code>, three plugins. No version control before this — the first commit is everything that already existed, dropped in at once.</p>
  </div>
  <div class="ffo-ev">
    <div class="date">May 23, 2020</div>
    <h4>"Added SMTP"</h4>
    <p>PHPMailer wired in. The site can send verification codes. First of three mail architectures.</p>
  </div>
  <div class="ffo-ev">
    <div class="date">Nov 2020 → Jan 2021</div>
    <h4>Version 15.x.x: 98 commits in 60 days</h4>
    <p>15.5 through 15.10, sometimes three releases in a day. The numbers stopped meaning anything; the shipping didn't slow.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">May 20, 2021 · last commit</div>
    <h4>"Updated Mail templates"</h4>
    <p>The last thing git recorded. Mail broke the site when it finally mattered — fixing it was the final commit.</p>
  </div>
</div>

<p>Those "Version X.Y.Z" tags aren't versions — they're deploys: the whole tree zipped and pushed, git as a save button. They contradict each other (<code>v15.10.8</code> predates <code>v15.10.0</code>; 15.8.2/15.8.3 share one commit), and the largest, <code>15.7.4</code>, touched 276 files at once. So 298 commits is a lower bound — much was built <em>between</em> snapshots git never saw:</p>

<div class="ffo-tbl-wrap ffo-reveal" style="overflow-x:auto;margin:1rem 0">
<table class="ffo-tbl ffo-tbl-deploy">
  <thead><tr><th>Commit</th><th>Files</th><th>Changes</th><th>What it actually was</th></tr></thead>
  <tbody>
    <tr><td>v15.7.4</td><td>276</td><td class="ffo-diff"><span class="add">+2,064</span><span class="del">−56,428</span></td><td>Major rewrite + dead code purge</td></tr>
    <tr><td>v15.10.7</td><td>38</td><td class="ffo-diff"><span class="add">+1,634</span><span class="del">−483</span></td><td>Notifications + poll system overhaul</td></tr>
    <tr><td>v15.10.0 <small>(×2)</small></td><td>71–76</td><td class="ffo-diff"><span class="add">+1,596–1,806</span><span class="del">−347–805</span></td><td>Two separate deploys with the same name</td></tr>
    <tr><td>v15.9.0 <small>(×2)</small></td><td>28–61</td><td class="ffo-diff"><span class="add">+327–774</span><span class="del">−209–321</span></td><td>Same version number, different branch snapshots</td></tr>
    <tr><td>v15.8.0</td><td>80</td><td class="ffo-diff"><span class="add">+856</span><span class="del">−45,726</span></td><td>WordPress cleanup: 45k lines deleted</td></tr>
    <tr><td>v15.6.0</td><td>93</td><td class="ffo-diff"><span class="add">+837</span><span class="del">−7,274</span></td><td>Full frontend restructure</td></tr>
    <tr><td>v15.6.1 + v15.7.0</td><td>50</td><td class="ffo-diff"><span class="add">+1,285</span><span class="del">−325</span></td><td>Two versions, one commit</td></tr>
  </tbody>
</table>
</div>
</div>

<h3 id="the-bundler">The bundler</h3>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading bundles.php and build.py</span>
<p>In 2020, webpack and rollup existed but targeted single-page apps; a server-rendered PHP site had no equivalent. So he wrote his own: a PHP manifest declares each page's bundle, and a Python compiler resolves it, skips unchanged sources, minifies with terser and cssnano, and writes content-hashed <code>name-&lt;sha1&gt;.js</code> files — cache-busted in production, raw in dev for honest stack traces. Incremental builds, content-hash cache-busting, shared chunks: the ideas webpack shipped, arrived at independently.</p>
<p>The same <code>build.py</code> does double duty: besides compiling assets, it's also the deploy script — SCP the build up, SSH in to unzip it live — this comes up later, in the crash.</p>

<div class="ffo-steps">
  <div class="ffo-step">
    <div class="ffo-step-h"><span class="ffo-step-n">1</span><span class="ffo-step-name">Manifest</span></div>
    <p class="ffo-step-desc">A page declares a bundle — its JS/CSS plus shared <code>mix</code> groups.</p>
    <code class="ffo-step-code">"global":{"js":["intro"],"mix":["idb"]}</code>
    <span class="ffo-step-note">↳ an entry, not a file — nothing compiled yet</span>
  </div>
  <div class="ffo-step">
    <div class="ffo-step-h"><span class="ffo-step-n">2</span><span class="ffo-step-name">Reference</span></div>
    <p class="ffo-step-desc">Pulled into a PHP page by name.</p>
    <code class="ffo-step-code">&lt;?= bundle('global') ?&gt;</code>
    <span class="ffo-step-note">↳ dev emits the raw sources; prod, one cache-busted tag</span>
  </div>
  <div class="ffo-step">
    <div class="ffo-step-h"><span class="ffo-step-n">3</span><span class="ffo-step-name">Compiler</span></div>
    <p class="ffo-step-desc"><code>build.py</code> resolves &amp; minifies only the changed bundles.</p>
    <code class="ffo-step-code">name-&lt;sha1&gt;.js</code>
    <span class="ffo-step-note">↳ skips unchanged sources, writes the hash back into the manifest</span>
  </div>
</div>
</div>

<hr />

<h2 id="chapter-two--the-platform">Chapter Two · The Platform</h2>

<p>The site wasn’t one feature. It was sixteen interlocking systems — each its own PHP class, its own database tables, its own notification hook — built by someone who’d never heard the term “microservice” but arrived at the shape anyway.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading core/classes/</span>
<p>Not a feature list — a directory listing. Each <code>.php</code> file is a standalone system with its own DB schema, its own API endpoints, and its own hook into a shared notification dispatch. Grouped by what they do:</p>

<div class="ffo-arch ffo-reveal" style="margin-top:1rem">

<div class="ffo-arch-group">
<div class="ffo-arch-label">Reading</div>
<div class="ffo-arch-items">
<div class="ffo-arch-item"><code>Search.php</code><span>Inverted index, set algebra in memory, zero SQL per filter</span></div>
<div class="ffo-arch-item"><code>Feed.php</code><span>Personalized by fandom + rating + language + currently reading</span></div>
<div class="ffo-arch-item"><code>Collections.php</code><span>Curated story lists, followers notified on update</span></div>
<div class="ffo-arch-item"><code>Bookmarks.php</code><span>Exact paragraph position, server-side, per user</span></div>
</div>
</div>

<div class="ffo-arch-group">
<div class="ffo-arch-label">Writing</div>
<div class="ffo-arch-items">
<div class="ffo-arch-item"><code>Drafts.php</code><span>Folder + branch types, private until published</span></div>
<div class="ffo-arch-item"><code>Updates.php</code><span>Author blog posts on profiles, pinnable</span></div>
<div class="ffo-arch-item"><code>Beta.php</code><span>Invite-only reader access, time-limited sessions</span></div>
<div class="ffo-arch-item"><code>Import.php</code><span>Link FFN account → auto-syncs chapters on upstream update</span></div>
</div>
</div>

<div class="ffo-arch-group">
<div class="ffo-arch-label">Social</div>
<div class="ffo-arch-items">
<div class="ffo-arch-item"><code>Reviews.php</code><span>Threaded: quote + reply, parent-ID tree</span></div>
<div class="ffo-arch-item"><code>Vote.php</code><span>Per-chapter, fires author notification</span></div>
<div class="ffo-arch-item"><code>Follow.php</code><span>Authors or collections, per-follow notification pref</span></div>
<div class="ffo-arch-item"><code>Chats.php</code><span>User-to-user DMs with blocking</span></div>
<div class="ffo-arch-item"><code>Poll.php</code><span>3-day expiry, results pushed via notification</span></div>
<div class="ffo-arch-item"><code>Questions.php</code><span>Anonymous submit, author approves → public</span></div>
</div>
</div>

<div class="ffo-arch-group">
<div class="ffo-arch-label">Infrastructure</div>
<div class="ffo-arch-items">
<div class="ffo-arch-item"><code>Notifications.php</code><span>10 priority-ranked types, single 15s poll heartbeat</span></div>
<div class="ffo-arch-item"><code>Stats.php</code><span>VFS identity, impressions vs views, referrer tracking</span></div>
<div class="ffo-arch-item"><code>Themes.php</code><span>Normal / sepia / dark, saved per account</span></div>
</div>
</div>

</div>

<p style="margin-top:.8rem">The notification system was the spine. Ten event types — <code>story_update</code>, <code>chapter_review</code>, <code>review_reply</code>, <code>follow_user</code>, <code>follow_collection</code>, <code>chapter_vote</code>, <code>user_update</code>, <code>beta_invite</code>, <code>stories_imported</code>, <code>account_verified</code> — all delivered through a single 15-second poll heartbeat (described in the next chapter).</p>
</div>

<p>Authors and readers I reached out to, to join the site, often mentioned they’d love an app and would prefer it over a website. And after hours of researching the tradeoffs of WebViews, taking on the complexities and learning of React Native, and much much more — this was the first decision I made to prioritize, of choosing <em>not</em> to build something.</p>

<p>I instead maximized every single, however-new-feature of PWAs and turned the entire website in something that background-auto-downloaded into an offline app the first time you visit, and where you could save <em>any</em> story to read offline with just button, and read again, Wi-Fi off, directly from your browser.</p>

<p>Even today in 2026, there is barely a website I know of that manages to do this: even with the ease of Claude Code simplifying all the weird complexities of service workers into a couple back-and-forth chats.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading service.js</span>
<p>The reader also worked offline, modeled on a library. "Borrow" a story: service worker caches the story page and every chapter URL, IndexedDB tracks what you hold. "Return" it to free space. Hourly, the worker sends held IDs + last sync timestamp; the server diffs and responds with three lists — <code>current</code> (keep), <code>update</code> (re-cache changed chapters), <code>delete</code> (evict unpublished). Only deltas transfer. Reads done offline queue locally and reconcile on reconnect, stamped with the same <code>landing_id</code> that ties into the analytics system in the next chapter.</p>
</div>

<hr />

<h2 id="chapter-three--the-measurement">Chapter Three · The Measurement</h2>

<p>Perhaps my crown jewel and most loved-feature, inspired by the conspiracy theories of Zuckerberg, his taped laptop camera, and how he tracked every single movement of every person and what they were thinking. I was an easily-inspired child, and I wanted to build something of my own like it.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading core/classes/stats.php, track_reading.php and global.js</span>
<p>No Google Analytics — the platform watched itself. Every page view saved a row to <code>stats_landings</code>, and it counted two different things: an <em>impression</em> (a story showed up in your results) versus a <em>view</em> (you actually clicked it). The difference is what people saw but skipped.</p>
<p>To make that data mean anything, it needed to know <em>who</em> each row belonged to — even before someone logged in. So every visitor got a VFS (Visitor Fingerprint Signature): a single ID that sticks to you across visits, and stays the same if you browse anonymously and sign up later. To find it, the server tried four things in order, stopping at the first hit:</p>

<div class="ffo-vfs-chain ffo-reveal">
  <div class="ffo-vfs-step">
    <div class="ffo-vfs-n">1</div>
    <div class="ffo-vfs-body"><strong>Cookie</strong><br />Already have a <code>vfs</code> cookie? Use it, and refresh it for another 15 years.</div>
  </div>
  <div class="ffo-vfs-arrow">↓ miss</div>
  <div class="ffo-vfs-step">
    <div class="ffo-vfs-n">2</div>
    <div class="ffo-vfs-body"><strong>User ID</strong><br />Logged in? Look up their first-ever visit and reuse that VFS.</div>
  </div>
  <div class="ffo-vfs-arrow">↓ miss</div>
  <div class="ffo-vfs-step">
    <div class="ffo-vfs-n">3</div>
    <div class="ffo-vfs-body"><strong>IP address</strong><br />Seen this IP before? Reuse the VFS it had last time.</div>
  </div>
  <div class="ffo-vfs-arrow">↓ miss</div>
  <div class="ffo-vfs-step ffo-vfs-new">
    <div class="ffo-vfs-n">✦</div>
    <div class="ffo-vfs-body"><strong>New VFS</strong><br />Truly new visitor — mint a fresh random ID with <code>bin2hex(random_bytes(39))</code>.</div>
  </div>
</div>

<p>Impressions fired by scroll, not render — a card counted only once its midpoint crossed the viewport, flushed every 15s to one endpoint: <code>poll</code>. That call became the real-time backbone, accreting jobs over six months:</p>

<div class="ffo-tl ffo-reveal">
  <div class="ffo-ev">
    <div class="date">Jul 2020 · v5.0.0</div>
    <h4>Impressions ship</h4>
    <p><code>im()</code> lands in <code>global.js</code>: counts a story card once it's actually on screen, sent to <code>poll</code> every 15s.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Aug 2020 · v5.6.0</div>
    <h4>Custom tags take over</h4>
    <p>The scanner switches to the invented <code>&lt;book&gt;</code> tag, tying analytics to the homegrown tag system.</p>
  </div>
  <div class="ffo-ev">
    <div class="date">Dec 2020 · v15.10.0</div>
    <h4>Poll delivers notifications too</h4>
    <p>The same 15s call now returns your new notifications on the way back — one round-trip for both.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Jan 2021 · v15.9.4</div>
    <h4>PollFilters: the loop becomes a bus</h4>
    <p><code>PollFilters</code> added: any page can attach extra data to the outgoing poll before it sends.</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Jan 2021 · v15.9.x</div>
    <h4>Reading position joins the wire</h4>
    <p><code>chapter-tracking.js</code> uses it to also send which paragraph you're on. Now one request carries impressions, notifications, and reading position.</p>
  </div>
</div>

<p>One request ended up doing everything. An AES-256 <code>placeholder</code> token, minted at render, stamped every action — vote, review, search — back to the VFS and referrer that opened the session:</p>

<div class="ffo-wire ffo-reveal">
  <div class="ffo-wire-head">one poll request · fired every 15s of real interaction</div>
  <div class="ffo-wire-cols">
    <div class="ffo-wire-col up">
      <div class="ffo-wire-tag">↑ client sends</div>
      <div class="ffo-wire-line"><code>placeholder</code><span>AES-256 token — landing id + page type</span></div>
      <div class="ffo-wire-line"><code>nonce</code><span>per-page session guard</span></div>
      <div class="ffo-wire-line"><code>im_books[]</code><span>cards scrolled past the viewport midpoint</span></div>
      <div class="ffo-wire-line"><code>position</code><span>paragraph now at the viewport top</span></div>
    </div>
    <div class="ffo-wire-col down">
      <div class="ffo-wire-tag">↓ server returns</div>
      <div class="ffo-wire-line"><code>notifications[]</code><span>inbox diff since the last poll</span></div>
      <div class="ffo-wire-line"><code>unread</code><span>badge count</span></div>
    </div>
  </div>
  <div class="ffo-wire-server">
    <div class="ffo-wire-tag">server, in between</div>
    <div class="ffo-wire-server-line">decrypt <code>placeholder</code> → <code>landing_id</code></div>
    <div class="ffo-wire-server-line">validate <code>nonce</code> → session · mismatch rejects with <code>998</code>, logged as spam</div>
    <div class="ffo-wire-server-line">log impressions → <code>stats_actions</code> · save paragraph → <code>track_reading</code></div>
  </div>
</div>

<p>Both rode in the DOM as invented HTML tags, read via <code>.getAttribute('value')</code> — web components, years before they were actually in the mainstream. On top, two cached admin reports: session time by referrer, and link-ins counted per story rather than sitewide — so the Tumblr spike showed up as one domain flooding a single row.</p>
</div>

<hr />

<h2 id="chapter-four--the-outreach">Chapter Four · The Outreach</h2>

<p>So I engineered a scraper that bypassed Cloudflare through a rotating outwards proxy, logged into fanfiction.net with 5 rotating accounts, and sent personal DMs to a total of 8,500+ top authors, inviting them to the website. I don’t think I knew what cold outreach or email lists even was at the time, but I knew I wanted to speak to authors, and I’d figured out it was a numbers game.</p>

<p>All on a $5/mo cPanel hosting, shared with one more, though mostly dead website.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the repo</span>
<p>One engine both stocked the library and ran the outreach. Scraping FFN — the largest fanfiction archive online, behind Cloudflare and hostile to scrapers — took over a year of building, getting blocked, and rebuilding across two repos: <code>ffarchive_py</code>, then <code>ffn_scraper</code>.</p>

<div class="ffo-tl ffo-reveal">
  <div class="ffo-ev">
    <div class="date">Jul 2020</div>
    <h4>ffarchive_py is born</h4>
    <p>Scrapy + MongoDB. FFN pagination working in days — and blocked just as fast.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Sep–Nov 2020</div>
    <h4>From crawler to growth engine</h4>
    <p>Gains a story importer, live update loop, and auto-sender — it stops just reading FFN and starts messaging it.</p>
  </div>
  <div class="ffo-ev">
    <div class="date">Dec 2020</div>
    <h4>Clean-slate rewrite</h4>
    <p>Proxy tricks stopped working. Rebuilt as <em>ffn_scraper</em>, using Flaresolverr to actually beat Cloudflare.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Jan 4, 2021</div>
    <h4>"All in SQLite DB"</h4>
    <p>MongoDB dropped for <code>UserData.db</code> — 8,378 authors indexed, with verification and notifications wired in.</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Jan 6, 2021</div>
    <h4>Added FFN Sender</h4>
    <p>Scraper and outreach merge — the crawler feeds the PM queue directly.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Feb 6, 2021 · 05:32am</div>
    <h4>Both engines, last touch</h4>
    <p>Both scrapers get their final commits 60 seconds apart — <code>"Stuff"</code> and <code>"Something"</code>, set down at 5am.</p>
  </div>
</div>
</div>

<h3 id="the-verification-handshake">The verification handshake</h3>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading ffn_verification.py and v_code.php</span>
<p>FFN had no API or login to plug into, so there was only one way to prove an account was yours: show you could read its messages. The site gave you a code and a pre-filled message link, and a bot (<code>ffn_verification.py</code>) watched the inbox for that code and tied it to your FFN account. Once it matched, every story already scraped under that account was handed to you. Scraped first, returned when the real author showed up — which is what people objected to (covered later in this piece).</p>
</div>

<div class="ffo-vfs-chain ffo-reveal">
  <div class="ffo-vfs-step">
    <div class="ffo-vfs-n">1</div>
    <div class="ffo-vfs-body"><strong>You paste your FFN user ID and hit submit.</strong>The site generates a one-time code, good for an hour. <code>verify_user</code> → <code>bin2hex(random_bytes(4))</code></div>
  </div>
  <div class="ffo-vfs-arrow">↓</div>
  <div class="ffo-vfs-step">
    <div class="ffo-vfs-n">2</div>
    <div class="ffo-vfs-body"><strong>You click the link — it opens FFN's message box, already addressed, code already copied.</strong>Just paste and send. <code>pm2/post.php?uid=13818620</code> · <code>_.copyText</code></div>
  </div>
  <div class="ffo-vfs-arrow">↓</div>
  <div class="ffo-vfs-step">
    <div class="ffo-vfs-n">3</div>
    <div class="ffo-vfs-body"><strong>A bot checks that inbox on a loop and reads your message.</strong>It pulls the sender's author ID and the code. <code>/pm2/inbox.php</code> via Flaresolverr (own proxy, solves Cloudflare headless)</div>
  </div>
  <div class="ffo-vfs-arrow">↓</div>
  <div class="ffo-vfs-step ffo-vfs-new">
    <div class="ffo-vfs-n">✓</div>
    <div class="ffo-vfs-body"><strong>Code matches — you're verified.</strong>Fires an <code>account_verified</code> notification and hands every story already scraped under that author ID over to you.</div>
  </div>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the flaresolverr plugin and the proxy middleware</span>
<p>FFN sits behind Cloudflare, so every request to that inbox had to clear a bot check first. Each one routed through Flaresolverr, a headless browser that runs Cloudflare's challenge and returns the cleared cookies — held per session and reused, not re-solved each time. The PM reader and the bulk crawler ran on separate proxies and identities, so the account reading messages never shared an IP with the one pulling stories. When FFN logged a session out, the bot caught the redirect, signed back in, and kept going. No official API existed; the whole pipeline ran on reverse-engineered cookies and held together for over a year.</p>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the repo</span>
<p>The targeting filter: only authors with 350+ favorites or follows. 8,378 profiled, 4,964 reached, one message every 35 seconds across 5 rotating accounts, with each delivery confirmed by parsing the response page. Every one of them got this:</p>
</div>

<div class="ffo-pm ffo-reveal">
  <div class="ffo-pm-from">
    <div class="ffo-pm-av">FO</div>
    <div class="ffo-pm-meta">
      <b>Fanfiction Online</b>
      <span>Private Message · to ###USERNAME###</span>
    </div>
  </div>
  <div class="ffo-pm-subj">Hi <span style="color:#c2255c">###USERNAME###</span>! :)</div>
  <div class="ffo-pm-body">
    Hi <span class="u">###USERNAME###</span>! :)<br /><br />
    We've created a site at Fanfiction Online for reading and writing fanfiction, since we feel FFN (and other sites) are really outdated and lack a lot of stuff.<br /><br />
    Would love if you could post your stories there. You can reach new readers, and since the site has better reading, your current readers will read your fanfics more comfortably as well.<br /><br />
    You might find it difficult to post on another site, so the site will do all the heavy lifting for you :) All you have to do is link your FFN account and select the stories you want to import. It's as simple as that. The stories will be updated automatically whenever you update the fic on FFN.<br /><br />
    Thanks! Let me know if you have any questions.
  </div>
</div>

<div class="ffo-stats ffo-reveal">
  <div class="ffo-stat"><span class="n" data-to="8378">0</span><span class="l">Authors profiled</span></div>
  <div class="ffo-stat"><span class="n" data-to="4964">0</span><span class="l">Messages sent</span></div>
  <div class="ffo-stat"><span class="n" data-to="5">0</span><span class="l">Rotating senders</span></div>
  <div class="ffo-stat"><span class="n" data-to="35">0</span><span class="l">Sec between sends</span></div>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the scraper commits</span>
<p>The auto-sender wasn't random: it ranked <code>UserData.db</code> authors by engagement and worked from the top down. The five highest held 1.3M favorites between them:</p>

<table class="ffo-tbl" style="margin-top:.7rem">
  <thead><tr><th>Author</th><th>Favorites</th><th>Fandom</th></tr></thead>
  <tbody>
    <tr><td>sakurademonalchemist</td><td class="favs">456,019</td><td>Harry Potter · KHR</td></tr>
    <tr><td>Lomonaaeren</td><td class="favs">284,724</td><td>Harry Potter</td></tr>
    <tr><td>DebsTheSlytherinSnapefan</td><td class="favs">212,470</td><td>Harry Potter</td></tr>
    <tr><td>Tsume Yuki</td><td class="favs">207,734</td><td>Naruto</td></tr>
    <tr><td>NeonZangetsu</td><td class="favs">194,866</td><td>Naruto · Fate/stay night</td></tr>
  </tbody>
</table>

<p>After each send, it parsed FFN's response page to confirm the message landed.</p>
</div>

<p>The response wasn’t great. I learnt that people wouldn’t use an empty site, less than 10 out of 8,500+ of them. But it did get some results — multiple authors created threads on Reddit, Tumblr, and other forums about the horror of being approached like this and how I was a money-sucking leech (I’d never monetized it, but they assumed somehow) bonding in their trauma. Yes, the response was that negative, almost bordering on harassment.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, reading the same author's Reddit account (u/eqwe32)</span>
<p>Reconstructed from <a rel="nofollow" target="_blank" href="https://www.reddit.com/user/eqwe32/">u/eqwe32</a> — the anonymous account he posted the project under at the time — and all 51 of its threads across the fanfiction subreddits. One correction the record makes: the objection was rarely about money. It was about consent. Across 15 of those 51 threads, readers and writers make the same charge, that hosting their stories without asking was not his to do.</p>

<p>The public poll he ran on whether to remove those fics, instead of just removing them, sharpened it rather than settling it (u/Cautious-Pirate, 9 pts: he'd "got blasted for this left, right and centre").</p>

<p>And the hostility tracked what he posted, not who he was: asking a community what to build drew help and line-by-line feedback; announcing he'd built the best site drew the harshest replies. The cold-DM campaign this section is about came late, and was never the main charge against him.</p>

<p style="font-size:.82rem;color:#7e8c99;margin:.4rem 0 0">Every public post from <a rel="nofollow" target="_blank" href="https://www.reddit.com/user/eqwe32/">that account</a>, in order:</p>

<div class="ffo-tl ffo-reveal" style="margin-top:.9rem">
  <div class="ffo-ev blue">
    <div class="date">Dec 14, 2019 · r/FanFiction</div>
    <h4>"Hi everyone!" · 4↑ · 8 comments</h4>
    <p>First public trace — and the consent objection is already in the replies.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Mar 23, 2020 · r/FanFiction</div>
    <h4>"Ideal features you'd like to see..." · 0↑ · 15 comments</h4>
    <p>Scraping becomes the headline. Top reply, u/gros-grognon (11↑): "downright shitty... I can't trust your site."</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Jun 20, 2020 · r/HPfanfiction</div>
    <h4>"Suggestions needed on how to improve..." · 13↑ · 32 comments</h4>
    <p>Pure UX feedback — layout, mobile, title-vs-author hierarchy — answered point by point.</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Aug 15, 2020 · r/HPfanfiction</div>
    <h4>"Something you've (probably) all been waiting for." · 62↑ · 20 comments</h4>
    <p>Warmest reception; readers say they'll switch. One won't — same consent grounds (u/Atojiso, 14↑).</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Aug 21–22, 2020 · r/FanFiction + r/HPfanfiction</div>
    <h4>"What would be your dream fanfiction writing app?" · 0↑ / 39↑</h4>
    <p>Same feature-mining question put to both subs in one week.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">Aug 27, 2020 · r/FanfictionOnline</div>
    <h4>Own subreddit created · 1↑ · lounge only</h4>
    <p>Spins up a project subreddit. Never takes.</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Sep 13, 2020 · r/NarutoFanfiction</div>
    <h4>"I created a fanfiction site..." · 219↑ · 167 comments</h4>
    <p>Biggest hit — pitched to Naruto writers before the site had a Naruto option (u/Cranesbill, 65↑).</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Oct 3, 2020 · r/WormFanfic</div>
    <h4>"...any suggestions/questions?" · 38↑ · 37 comments</h4>
    <p>The pitch at its plainest: no existing site improves on what readers and writers actually ask for.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Oct 19, 2020 · r/NarutoFanfiction + r/HPfanfiction</div>
    <h4>"The best app/site... you won't be disappointed." · 107↑ / 13↑</h4>
    <p>Hard-sell framing flips the room. u/callmesalticidae (56↑); u/Bomaruto (34↑): no right to host uploads without a terms of service.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Jul 23, 2023 · r/FanFiction + r/HPfanfiction</div>
    <h4>"Fanfiction.online will sunset on Sep 15th 2023" · 0↑ · 11 comments</h4>
    <p>End notice, three years on. The Dec 2019 complaint is still the first in the replies.</p>
  </div>
</div>
</div>

<p>I was struggling — months of development and all other corners of fanfiction communities, the people I was building it for, seemed to hate me. Only one corner of the internet, a Harry Potter-centered fanfiction Discord, seemed to welcome me and provided support and, might I say, older developers who offered me guidance.</p>

<p>Until my big lucky break.</p>

<hr />

<h2 id="chapter-five--the-crash">Chapter Five · The Crash</h2>

<p>A viral post on Tumblr ranted against Ao3 for various reasons and referenced my website as where they were moving. It was a miracle — overnight from 50 I hit 10,000+ signed-in users and could have been a lot more, except the cPanel hosting finally gave up, the internal email service I had used to deliver verification codes refused to deliver, and I was left stranded.</p>

<p>In all times of difficulties since then in my life, I’ve always had an inkling of an idea of what I would do. In this case, I had none.</p>

<div class="ffo-crash ffo-reveal">
  <span class="tag">What broke</span>
  <p>Shared cPanel hosting, $5/mo, shared with one other barely-alive site. The host's internal mail service buckled under load. New users couldn't verify accounts. The hype was live; the site was effectively dark. No fallback, no queue, no redundancy — there had been no reason to build one.</p>
</div>

<p>At the same time, anticipating some need I had started making some money and had a whole $35 stuck in Payoneer. I knew within those limits I had to figure out a solution. So I started researching and learning what servers were — I spent night and day, and in the most literal sense possible. I didn’t sleep for nearly 3 nights and days worried I would miss out on my lucky break of users — and I wasn’t wrong, I did.</p>

<p>And I wasn’t able to figure the migration and setup of my newly discovered $5 droplet on DO in time — the site was unavailable, and the hype died.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the repo</span>
<p>The migration did eventually get there. Two separate webapps, <code>fanfiction_online</code> and <code>beta</code>, both on the same $5 droplet, deployed from a laptop via <code>build.py</code>.</p>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading build.py</span>
<p>The deploy script is about as basic as it gets. No CI, no Docker, no staging pipeline. Just Python calling <code>subprocess</code> twice to SCP two zips up, then SSHing in to unzip them into the live directory. It looks naive, and in some ways it is, but it's also enough when you're 17, have $35, and are racing a hype window that's already closing. It worked. Both webapps, main and beta, on the same droplet, deployed from a laptop:</p>

<pre class="ffo-pre" style="margin-top:.7rem"><span class="c"># build.py — the whole deploy pipeline</span>
host = <span class="s">"root@167.99.11.178"</span>
upload_to = <span class="s">"/home/runcloud/webapps/"</span>

subprocess.call(<span class="s">"scp content.zip "</span> + host + <span class="s">":"</span> + upload_to, shell=True)
subprocess.call(<span class="s">"scp static.zip  "</span> + host + <span class="s">":"</span> + upload_to, shell=True)

cmds = [
    <span class="s">"rm -r -f "</span> + upload_to + main_app + <span class="s">"/content"</span>,
    <span class="s">"unzip content.zip -d "</span> + upload_to + main_app + <span class="s">"/"</span>,
]
subprocess.call(<span class="s">"ssh "</span> + host + <span class="s">' "'</span> + <span class="s">" &amp;&amp; "</span>.join(cmds) + <span class="s">'"'</span>, shell=True)</pre>
<p>The crash timeline, reconstructed from the commit dates and what the code tells us about the mail architecture:</p>

<div class="ffo-tl" style="margin-top:.9rem">
  <div class="ffo-ev">
    <div class="date">The Tumblr spike</div>
    <h4>50 → 10,000+ signed-in users overnight</h4>
    <p>A viral post referencing the site as the alternative. No warning, no ramp-up.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Immediately</div>
    <h4>cPanel's mail collapses under signup load</h4>
    <p>Verification codes stop. Every new account is stuck at the confirmation step. The hype is live; the funnel is dead.</p>
  </div>
  <div class="ffo-ev blue">
    <div class="date">3 sleepless nights</div>
    <h4>Learning what a VPS actually is</h4>
    <p>DigitalOcean found. <span class="ffo-chip">root@167.99.11.178</span> provisioned. $35 in Payoneer, the entire budget. RunCloud installed for server management.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Too late</div>
    <h4>Migration finishes; hype is already gone</h4>
    <p>The window was days wide. The setup took longer. 10,000 users became a number in a story about what almost was.</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Eventually</div>
    <h4>Both webapps live on the $5 droplet</h4>
    <p><code>fanfiction_online</code> and <code>beta</code>. Deployed via <code>build.py</code>. The infrastructure that should have existed in March.</p>
  </div>
</div>
</div>

<hr />

<h2 id="chapter-six--the-engine">Chapter Six · The Engine</h2>

<p>After the crash, I did some rudimentary benchmarking — eyeballing the DO instance’s memory graph, inserting timers into PHP template scripts. One of the two biggest reasons for the crash: WordPress’ query engine took on average 20s for a single filtered query, sometimes up to 2 minutes.</p>

<p>Rather than fidget against WordPress for a 2-3x speedup, I hand-rolled it all. PHP 8 had just been released with promises for efficient low-level array functions, so I built my own query engine and brought 90% of complex queries down to &lt; 3s.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading Query.php</span>
<p>The search was the worst offender — nine filter dimensions (fandom, genre, rating, status, word count, character, pairing, language, sort), each supporting include <em>and</em> exclude. Under <code>WP_Query</code>, every combination compiled to JOINs against <code>wp_term_relationships</code>. The replacement: an inverted index precomputed at write time — every tag value mapped to the set of story IDs carrying it — filtered entirely in memory as set algebra.</p>

<div class="ffo-venn" style="margin:.9rem 0">
  <span class="ffo-pill inc">Romance</span><span class="ffo-pill op">∩</span>
  <span class="ffo-pill inc">Adventure</span><span class="ffo-pill op">∩</span>
  <span class="ffo-pill inc">Naruto</span><span class="ffo-pill op">−</span>
  <span class="ffo-pill exc">Crossover</span><span class="ffo-pill op">=</span>
  <span class="ffo-pill res">✦ your shelf</span>
</div>

<p>No joins. No <code>WHERE tag IN (...)</code> against 50k rows. Result set computed in memory; only current-page IDs fetched from DB. Wrapped in a hand-built PHP 8 type system (<code>Integer</code>, <code>LazyInteger</code>, <code>Range</code>, <code>Str</code>, <code>OneOf</code>, <code>TypedArray</code>) — all throwing on bad input. Two commits, then the repos went quiet.</p>
</div>

<p style="font-size:.82rem;color:#7e8c99;margin:.6rem 0 .3rem">Toggle filters — click to include (green), again to exclude (red), third to clear:</p>

<div id="ffo-filter-demo" style="margin: .5rem 0 .2rem">
  <div class="ffo-chips" id="ffo-demo-chips" style="margin-bottom: .8rem">
    <span class="ffo-chip-item" data-filter="fandom">Fandom</span>
    <span class="ffo-chip-item" data-filter="genre">Genre</span>
    <span class="ffo-chip-item" data-filter="rating">Rating</span>
    <span class="ffo-chip-item" data-filter="status">Status</span>
    <span class="ffo-chip-item" data-filter="words">Word count</span>
    <span class="ffo-chip-item" data-filter="character">Character</span>
    <span class="ffo-chip-item" data-filter="pairing">Pairing</span>
    <span class="ffo-chip-item" data-filter="language">Language</span>
    <span class="ffo-chip-item" data-filter="sort">Sort order</span>
  </div>
  <pre class="ffo-pre" id="ffo-sql-out" style="margin-top:0"></pre>
  <pre class="ffo-pre" id="ffo-php-out" style="margin-top:.6rem"></pre>
</div>

<script>
(function() {
  const filters = {
    fandom:    { col: 's.fandom_id', val: 'harry_potter', table: null, idx: 'fandom', idxVal: 'harry_potter', mode: 'include' },
    genre:     { col: null, val: 'romance', table: 'story_tags', idx: 'genre', idxVal: 'romance', mode: 'exclude' },
    rating:    { col: 's.rating', val: 'M', table: null, idx: 'rating', idxVal: 'M', mode: 'include' },
    status:    { col: 's.status', val: 'complete', table: null, idx: 'status', idxVal: 'complete', mode: 'include' },
    words:     { col: 's.word_count', val: '50000', table: null, idx: null, idxVal: null, mode: 'range' },
    character: { col: null, val: 'hermione', table: 'story_characters', idx: 'character', idxVal: 'hermione', mode: 'include' },
    pairing:   { col: null, val: 'drarry', table: 'story_pairings', idx: 'pairing', idxVal: 'drarry', mode: 'include' },
    language:  { col: 's.language', val: 'english', table: null, idx: 'language', idxVal: 'english', mode: 'include' },
    sort:      { col: null, val: 'favorites', table: null, idx: null, idxVal: null, mode: 'sort' }
  };

  const state = {};
  const chips = document.querySelectorAll('#ffo-demo-chips .ffo-chip-item');

  function s(t) { return '<span class="s">\'' + t + '\'</span>'; }
  function k(t) { return '<span class="k">' + t + '</span>'; }
  function c(t) { return '<span class="c">' + t + '</span>'; }
  function v(t) { return '<span class="v">' + t + '</span>'; }

  function render() {
    const active = Object.keys(state).filter(f => state[f]);
    const filterCount = active.filter(f => filters[f].mode !== 'sort').length;
    if (!active.length) {
      document.getElementById('ffo-sql-out').innerHTML = c('-- ↑ click filters to build the before/after');
      document.getElementById('ffo-php-out').innerHTML = c('// ↑ the optimized version appears here');
      return;
    }

    const sqlSec = (8 + filterCount * 5 + (active.some(f => filters[f].mode === 'exclude') ? 6 : 0)).toFixed(0);
    const phpSec = Math.max(0.5, (0.3 + filterCount * 0.4)).toFixed(1);
    const speedup = Math.round(sqlSec / phpSec);

    let joins = [], wheres = [], notIns = [], orderBy = '';
    let joinIdx = 0;
    active.forEach(f => {
      const def = filters[f];
      if (def.mode === 'sort') { orderBy = def.val; return; }
      if (def.mode === 'range') { wheres.push('s.word_count >= ' + def.val); return; }
      if (def.mode === 'exclude') {
        notIns.push({ table: def.table || 'story_tags', col: 'tag_id', val: def.val });
        return;
      }
      if (def.col) { wheres.push(def.col + ' = ' + "'" + def.val + "'"); }
      else {
        joinIdx++;
        const alias = 'j' + joinIdx;
        joins.push({ alias, table: def.table || 'story_tags', col: 'tag_id', val: def.val });
      }
    });

    let sql = c('-- BEFORE: WP_Query → SQL on every filter toggle · ~' + sqlSec + 's on shared cPanel MySQL') + '\n';
    sql += k('SELECT') + ' s.id ' + k('FROM') + ' stories s\n';
    joins.forEach(j => {
      sql += k('JOIN') + ' ' + j.table + ' ' + j.alias + ' ' + k('ON') + ' s.id = ' + j.alias + '.story_id\n';
    });
    if (wheres.length || notIns.length) {
      sql += k('WHERE') + ' ' + wheres.join('\n  ' + k('AND') + ' ');
      if (wheres.length && joins.length) sql += '\n  ' + k('AND') + ' ';
      joins.forEach((j, i) => {
        sql += (i > 0 || wheres.length ? '\n  ' + k('AND') + ' ' : '') + j.alias + '.' + j.col + ' = ' + "'" + j.val + "'";
      });
      notIns.forEach((n, i) => {
        sql += (i > 0 || wheres.length || joins.length ? '\n  ' + k('AND') + ' ' : '') + 's.id ' + k('NOT IN') + ' (\n    ' + k('SELECT') + ' story_id ' + k('FROM') + ' ' + n.table + '\n    ' + k('WHERE') + ' ' + n.col + ' = ' + "'" + n.val + "'" + '\n  )';
      });
    }
    if (orderBy) sql += '\n' + k('ORDER BY') + ' s.' + orderBy + ' ' + k('DESC');
    sql += ';';
    document.getElementById('ffo-sql-out').innerHTML = sql;

    let php = c('// AFTER: PHP 8 inverted index → set algebra in memory · ~' + phpSec + 's · ' + speedup + '× faster') + '\n';
    let first = true;
    active.forEach(f => {
      const def = filters[f];
      if (def.mode === 'sort') return;
      if (first && def.idx && def.mode === 'include') {
        php += v('$results') + ' = ' + v('$index') + '[' + s(def.idx) + '][' + s(def.idxVal) + '];\n';
        first = false;
      } else if (def.mode === 'include' && def.idx) {
        php += v('$results') + ' = ' + k('array_intersect') + '(' + v('$results') + ', ' + v('$index') + '[' + s(def.idx) + '][' + s(def.idxVal) + ']);\n';
      } else if (def.mode === 'exclude' && def.idx) {
        php += v('$results') + ' = ' + k('array_diff') + '(' + v('$results') + ', ' + v('$index') + '[' + s(def.idx) + '][' + s(def.idxVal) + ']);\n';
      } else if (def.mode === 'range') {
        php += v('$results') + ' = ' + k('array_filter') + '(' + v('$results') + ', ' + k('fn') + '(' + v('$id') + ') =>\n    ' + v('$meta') + '[' + v('$id') + '][' + s('words') + '] >= ' + def.val + '\n);\n';
      }
    });
    if (orderBy) {
      php += k('usort') + '(' + v('$results') + ', ' + k('fn') + '(' + v('$a') + ', ' + v('$b') + ') =>\n    ' + v('$meta') + '[' + v('$b') + '][' + s(orderBy) + '] - ' + v('$meta') + '[' + v('$a') + '][' + s(orderBy) + ']\n);\n';
    }
    document.getElementById('ffo-php-out').innerHTML = php;
  }

  chips.forEach(chip => {
    chip.style.cursor = 'pointer';
    chip.style.transition = '.15s';
    chip.style.userSelect = 'none';
    chip.addEventListener('click', () => {
      const f = chip.dataset.filter;
      const canExclude = f !== 'sort' && f !== 'words';
      if (!state[f]) {
        state[f] = 'include';
        filters[f].mode = 'include';
        chip.style.background = '#2a9d5c';
        chip.style.color = '#fff';
        chip.style.borderColor = '#2a9d5c';
        chip.style.textDecoration = '';
      } else if (state[f] === 'include' && canExclude) {
        state[f] = 'exclude';
        filters[f].mode = 'exclude';
        chip.style.background = '#c2255c';
        chip.style.color = '#fff';
        chip.style.borderColor = '#c2255c';
        chip.style.textDecoration = 'line-through';
      } else {
        delete state[f];
        const orig = {fandom:'include',genre:'exclude',rating:'include',status:'include',words:'range',character:'include',pairing:'include',language:'include',sort:'sort'};
        filters[f].mode = orig[f];
        chip.style.background = '';
        chip.style.color = '';
        chip.style.borderColor = '';
        chip.style.textDecoration = '';
      }
      render();
    });
  });

  render();
})();
</script>

<hr />

<h2 id="chapter-seven--the-mail-server">Chapter Seven · The Mail Server</h2>

<p>I was finally able to get it together and set up my own mail server, but that too, was eventually rate-limited.</p>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading mail.php</span>
<p>Three generations of email, each one failing in its own way, and each failure only obvious once it was already costing users.</p>

<div class="ffo-tl ffo-reveal" style="margin-top:.9rem">
  <div class="ffo-ev rose">
    <div class="date">Gen 1 · cPanel internal mail</div>
    <h4>Buckled at the spike</h4>
    <p>Fine for low volume. Couldn't survive 10,000 signups overnight. Verification codes stopped. New users were stuck at confirmation.</p>
  </div>
  <div class="ffo-ev rose">
    <div class="date">Gen 2 · Self-hosted on the $5 droplet</div>
    <h4>Cold IP, throttled out</h4>
    <p>Worked initially. Then receiving servers started rejecting it, which is the usual fate of any unknown IP sending volume. No reputation, no delivery.</p>
  </div>
  <div class="ffo-ev green">
    <div class="date">Gen 3 · SendGrid</div>
    <h4>Finally held</h4>
    <p>Seven fully-designed transactional templates. The solution that should have existed before the spike, built three months after it.</p>
  </div>
</div>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the commit history</span>
<p>The last two commits in the entire project — <code>"Sending Mail"</code> and <code>"Updated Mail templates"</code>, May 20, 2021. A fitting last word from the code: the thing that crashed the site when it finally got its moment, fixed, at the very end.</p>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading mail.php</span>
<p>SendGrid was the third attempt and the one that held: seven fully-designed dynamic templates, each with a real SendGrid template ID hardcoded:</p>

<div class="ffo-mail" style="margin-top:.8rem">
  <div class="ffo-mail-chip"><span class="type">signup</span>New account welcome</div>
  <div class="ffo-mail-chip"><span class="type">OTP</span>Verification code delivery</div>
  <div class="ffo-mail-chip"><span class="type">change-email</span>Email address change</div>
  <div class="ffo-mail-chip"><span class="type">notification</span>Story updates &amp; replies</div>
  <div class="ffo-mail-chip"><span class="type">faq-alert</span>FAQ change notifications</div>
  <div class="ffo-mail-chip"><span class="type">contact</span>Contact form confirm</div>
  <div class="ffo-mail-chip"><span class="type">contact-first</span>First contact flow</div>
</div>
<p style="margin-top:.7rem">Seven templates means seven distinct transactional scenarios were thought through, designed, and wired to API calls. That's more deliberate than email usually gets on a project this size, and it reads like someone who'd already watched it fail twice and wanted the third version to hold.</p>
</div>

<hr />

<h2 id="epilogue">Epilogue</h2>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the commit history</span>
<p>The commit messages decay honestly as the months drag on — <code>"Whatever,"</code> <code>"Stuff,"</code> <code>"Something,"</code> <code>"No Idea what this is."</code> That's not failure. That's the fingerprint of someone carrying an entire product on their own back, well past the point most would have stopped.</p>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading the repo</span>
<p>The hype window closed before the migration was ready, and the outreach campaign produced backlash before it produced users. All of it ran on a $5 shared hosting plan the site was never meant to outlive, until the night it had to, when three sleepless days and the only $35 available went into fixing that.</p>
<p>The repos went quiet on May 20, 2021, with two commits about finally getting email right. The live site kept going and kept gaining features that version control never recorded. The story git can tell ends there.</p>
</div>

<div class="ffo-stats ffo-reveal">
  <div class="ffo-stat"><span class="n" data-to="298">0</span><span class="l">Production commits</span></div>
  <div class="ffo-stat"><span class="n" data-to="8378">0</span><span class="l">Authors profiled</span></div>
  <div class="ffo-stat"><span class="n" data-to="10000">0</span><span class="l">Peak signed-in users</span></div>
  <div class="ffo-stat"><span class="n" data-to="5">0</span><span class="l">$/mo it ran on</span></div>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, after reading all 298 commits</span>
<p>Solo teenage projects usually follow one pattern: a good idea, built halfway, then dropped. This one ran longer than that. The commit history shows someone who kept going through the backlash, the crash, and the migration that came too late. The final commit is about email. Not a new feature or a rewrite, just a fix for the thing that had failed at the worst possible moment, landing three months after it mattered.</p>
<p>The commit messages do get worse. <code>"Whatever"</code>, <code>"Stuff"</code>, <code>"Something"</code>, <code>"No Idea what this is"</code> turn up in the middle months. That reads less like someone who stopped caring and more like someone too far into the work to bother naming it. What got built across that stretch: sixteen features, a custom PHP framework, a Cloudflare-bypassing scraper, a typed search engine, three generations of email infrastructure, and a cold-outreach campaign to 8,500 authors, all of it alone, at 17, on $5 a month.</p>
<p>The repos went quiet on May 20, 2021. The story they can tell ends there; the rest survives only because it finally got written down.</p>
</div>

<div class="ffo-ai ffo-reveal">
<span class="ffo-ai-label">— Opus 4.8, reading where the Reddit account left off</span>
<p>The site outlived its code by two years. The shutdown, announced July 23, 2023, came with a one-line reason: "Too expensive to maintain (and so my therapist stops bothering me about it)." He pitched a successor, a free Chrome and Edge reading extension for AO3 and fanfiction.net, and held his ground on the original idea: "I don't think what I did was stealing in any way, it's exactly the same... as the WaybackMachine."</p>
<p>Within a day he'd talked himself out of the extension too, "because of the hostile reaction on the other FanFiction sub." Roughly three and a half years of running it, and what stopped it in the end was cost and exhaustion, not the code.</p>
</div>

<div class="ffo-callout ffo-reveal">
  <span class="tag">Deeper dive · primary sources</span>
  <p>The public traces the site left behind, if you want to read the reception unfiltered:</p>
  <p style="margin-top:.5rem">
    · Reddit — the launch and the backlash, in full: <a href="https://www.reddit.com/user/eqwe32/" target="_blank" rel="noopener">u/eqwe32</a><br />
    · X / Twitter — the project account: <a href="https://x.com/FanfictionOnlin" target="_blank" rel="noopener">@FanfictionOnlin</a><br />
    · Tumblr — what other communities said: <a href="https://www.tumblr.com/search/%22fanfiction.online%22?src=typed_query" target="_blank" rel="noopener">"fanfiction.online" search</a>
  </p>
</div>

<script>
(function(){

  /* ── scroll progress bar ── */
  var bar = document.getElementById('ffo-bar');
  window.addEventListener('scroll', function(){
    var h = document.documentElement;
    bar.style.width = (h.scrollTop / (h.scrollHeight - h.clientHeight) * 100) + '%';
  });

  /* ── reveal on scroll ── */
  var io = new IntersectionObserver(function(es){
    es.forEach(function(e){ if(e.isIntersecting){ e.target.classList.add('in'); io.unobserve(e.target); }});
  }, {threshold: .12});
  document.querySelectorAll('.ffo-reveal').forEach(function(el){ io.observe(el); });

  /* ── count-up ── */
  var fmt = function(n){ return n >= 1000 ? n.toLocaleString('en-US') : String(n); };
  var cio = new IntersectionObserver(function(es){
    es.forEach(function(e){
      if(!e.isIntersecting) return;
      var el = e.target, to = +el.dataset.to, s = null;
      var step = function(t){
        if(!s) s = t;
        var p = Math.min((t-s)/1600, 1), ease = 1-Math.pow(1-p,3);
        el.textContent = fmt(Math.floor(ease*to));
        if(p < 1) requestAnimationFrame(step); else el.textContent = fmt(to);
      };
      requestAnimationFrame(step);
      cio.unobserve(el);
    });
  }, {threshold: .5});
  document.querySelectorAll('.ffo-stat .n').forEach(function(el){ cio.observe(el); });

  /* ── search filter demo ── */
  var STORIES = [
    {title:"The Long Way Home",       fandom:"Harry Potter",      genres:["Romance","Hurt/Comfort"], rating:"T", status:"In Progress", wc:187000},
    {title:"A Thousand Paper Cranes", fandom:"Naruto",            genres:["Angst","Romance"],        rating:"M", status:"Complete",    wc:312000},
    {title:"Between Two Worlds",      fandom:"Harry Potter",      genres:["Adventure","Crossover"],  rating:"T", status:"Complete",    wc:94000},
    {title:"Crimson Tide",            fandom:"My Hero Academia",  genres:["Adventure","Angst"],      rating:"M", status:"In Progress", wc:58000},
    {title:"Glass Houses",            fandom:"Twilight",          genres:["Romance","Hurt/Comfort"], rating:"T", status:"Complete",    wc:42000},
    {title:"The Marauders' Last Year",fandom:"Harry Potter",      genres:["Angst","Romance"],        rating:"M", status:"Complete",    wc:225000},
    {title:"Uzumaki's Redemption",    fandom:"Naruto",            genres:["Adventure"],              rating:"T", status:"In Progress", wc:130000},
    {title:"Parallel Lines",          fandom:"Supernatural",      genres:["Hurt/Comfort","Angst"],   rating:"M", status:"Complete",    wc:78000},
    {title:"The Weight of Wings",     fandom:"My Hero Academia",  genres:["Romance","Adventure"],    rating:"K", status:"Complete",    wc:33000},
    {title:"Heir Apparent",           fandom:"Harry Potter",      genres:["Adventure"],              rating:"T", status:"In Progress", wc:410000},
    {title:"Wolves Don't Cry",        fandom:"Twilight",          genres:["Romance","Crossover"],    rating:"M", status:"In Progress", wc:67000},
    {title:"The Quiet Hours",         fandom:"Supernatural",      genres:["Hurt/Comfort","Romance"], rating:"T", status:"Complete",    wc:21000},
  ];

  var tagState = {}; // val -> 'inc' | 'exc'

  function renderStories(){
    var list = document.getElementById('ffo-story-list');
    var countEl = document.getElementById('ffo-res-count');
    if(!list) return;

    var wcMin = +(document.getElementById('ffo-wc-min')||{value:0}).value * 1000;
    var wcMax = +(document.getElementById('ffo-wc-max')||{value:500}).value * 1000;

    var filtered = STORIES.filter(function(s){
      // word count range
      if(s.wc < wcMin || s.wc > wcMax) return false;
      // check each active tag
      for(var val in tagState){
        var state = tagState[val];
        if(!state) continue;
        var matches = s.fandom === val || s.genres.indexOf(val) > -1 || s.rating === val || s.status === val;
        if(state === 'inc' && !matches) return false;
        if(state === 'exc' && matches)  return false;
      }
      return true;
    });

    countEl.textContent = filtered.length;
    list.innerHTML = filtered.map(function(s){
      return '<div class="ffo-story-row">'
        + '<div><div class="ffo-story-title">'+s.title+'</div>'
        + '<div class="ffo-story-meta">'+s.fandom+' · '+s.rating+' · '+s.status+'</div>'
        + '<div class="ffo-story-tags">'
        + s.genres.map(function(g){ return '<span class="ffo-story-tag">'+g+'</span>'; }).join('')
        + '</div></div>'
        + '<div class="ffo-wc">'+(s.wc/1000).toFixed(0)+'k words</div>'
        + '</div>';
    }).join('');
  }

  document.querySelectorAll('.ffo-tag').forEach(function(tag){
    tag.onclick = function(){
      var val = tag.dataset.val;
      var cur = tagState[val] || null;
      if(!cur)          { tagState[val] = 'inc'; tag.classList.add('inc'); tag.classList.remove('exc'); }
      else if(cur==='inc'){ tagState[val] = 'exc'; tag.classList.remove('inc'); tag.classList.add('exc'); }
      else              { delete tagState[val]; tag.classList.remove('inc','exc'); }
      renderStories();
    };
  });

  var wcMinEl = document.getElementById('ffo-wc-min');
  var wcMaxEl = document.getElementById('ffo-wc-max');
  var wcValEl = document.getElementById('ffo-wc-val');
  var wcMaxValEl = document.getElementById('ffo-wc-max-val');
  if(wcMinEl){
    wcMinEl.oninput = function(){ wcValEl.textContent = wcMinEl.value; renderStories(); };
    wcMaxEl.oninput = function(){ wcMaxValEl.textContent = wcMaxEl.value; renderStories(); };
  }

  renderStories();

})();
</script>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Em dashes, jargon, and the dead humanity theory — jotbook page 8</title><link href="https://obaid.wtf/jotbook/2026/05/19/jotbook-page-8.html" rel="alternate" type="text/html" title="Em dashes, jargon, and the dead humanity theory — jotbook page 8" /><published>2026-05-19T00:00:00+05:00</published><updated>2026-05-19T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/05/19/jotbook-page-8</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/05/19/jotbook-page-8.html"><![CDATA[<p><strong>1. I took 2</strong> years of Arabic classes as a kid, and one of the first rules of grammar I found alien to my English/Urdu-developed brain then was, ironically, the em dash.</p>

<p>I’d vaguely known about it, sometimes coming across it in more literary publications like the NYT, but never the way I was being taught: as a paired bracket: not to explain what you said previous but to neatly unroll your overthinking into.</p>

<p>as a chronic over-thinker, it stuck with me, and without realizing it bled into my native languages like blood, until I realized there was barely an essay I wrote that went without including this strange, foreign glyph.</p>

<p><strong>2. A lot of</strong> shallowness and lack of intelligence hides behind labels and jargon. I’ve always had an intuition that forcing myself not to use labels often pushed me into breaking down concepts to their core, most minute level in a way that few invested the effort in doing: But I think it just clicked properly for me how universal and obvious that filter is.</p>

<p><strong>3. If someone has</strong> an idea for a specific sound that doesn’t yet exist and they want to mix, experiment with, or create, and the human mind although never having heard it before would be emotionally and physically moved by it — as music and auditory experiences have the capacity of doing — does that sound not deserve its place in humanity’s museum of existence even though it can? or should its creation be made difficult, its license restricted only to the holders of specific software, instruments, and money to buy them?</p>

<p>the answer seems obvious, except when that tool becomes AI? There are cases where the moral argument against AI can be legitimate, but for most I think it needs to be re-evaluated how much of it is a cloak around selfishness preventing the advancement of humanity’s cumulative archive and experience.</p>

<p>because at the core of it, all such realizations about AI skip the fact that it reduces the barrier of entry for things we might see created in the world.</p>

<p><strong>4. My best creative</strong> eras begin and end with moments of extreme happiness or a thrill of emotions; my best, most poetic writings come from anger, sadness, or any enormous magnitude of emotions, and that ability I am unable to tap into without them. I’m not quite sure I hate that, because it makes me very human. Very real. What is a “human” if it is tied to “systems” and imitating the very creatures we created to try and imitate us? The most physical manifestation of the dead internet theory, an endless loop where we imitate the characteristics of our imitators and they our imitation until our graph of self-improvement attains complete… loss.</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1. I took 2 years of Arabic classes as a kid, and one of the first rules of grammar I found alien to my English/Urdu-developed brain then was, ironically, the em dash.]]></summary></entry><entry><title type="html">My foray into video editing</title><link href="https://obaid.wtf/jotbook/2026/05/13/my-foray-into-video-editing.html" rel="alternate" type="text/html" title="My foray into video editing" /><published>2026-05-13T00:00:00+05:00</published><updated>2026-05-13T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/05/13/my-foray-into-video-editing</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/05/13/my-foray-into-video-editing.html"><![CDATA[<p>“video-editing” is a very modern phenomena. “film” has existed for a long time, but it was by definition a series, any series, of pictures that conveyed a story. captured through camera, drawn through animation or whatever.</p>

<p>Edited videos are its evolution, but still somehow, very different. The focus of “video-editing” isn’t on on the timeline, but the canvas: images are placed on any of its parts, either cleanly in grids or overlapping, as a series of moving pictures or still, and for any duration of time.</p>

<p>But unlike film which had one dimension, the timeline, video-editing has two:</p>

<p>the timeline… and now, the canvas.</p>

<p>and it is that exponential explosion of creative possibilities which has led into the art form we know as video-editing today.</p>

<p>And that art-form I tried to experiment with to learn, how much of it can be a repeatable workflow edited by Claude, and how much of it completely original.</p>

<p>I should mention that my theory on this is my own. Actually original. I intentionally block out and avoid research when theorizing or thinking deeply, to avoid polluting my headspace with ideas only occurring as a result of external reinforcement.</p>

<p>theory is complete. So we begin.</p>

<link rel="preconnect" href="https://fonts.googleapis.com" />

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" />

<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&amp;family=IBM+Plex+Sans:wght@400;500;600&amp;display=swap" rel="stylesheet" />

<style>
.wt {
  --bg:     #faf8f4;
  --paper:  #ffffff;
  --ink:    #1a1916;
  --muted:  #8a857c;
  --hair:   #e6e2d9;
  --track:  #d6d2c8;
  --accent: #b85c26;
  width: 100%;
  margin: 2rem 0;
  font-family: "IBM Plex Sans", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  color: var(--ink);
}

.wt *, .wt *::before, .wt *::after { box-sizing: border-box; }

/* ── HEADER ─────────────────────────────────────────────────────────────── */
.wt-header {
  margin-bottom: 16px;
}
.wt-title {
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.18;
  margin: 0;
}

/* ── OVERVIEW TIMELINE ──────────────────────────────────────────────────── */
.wt-tl {
  margin-bottom: 32px;
}

#tlSvg { display: block; width: 100%; overflow: visible; }

/* ── TWO-COLUMN CONTENT ─────────────────────────────────────────────────── */
.wt-content {
  display: grid;
  grid-template-columns: 248px 1fr;
  gap: 0 52px;
  align-items: start;
}

/* ── LEFT: STICKY SLIDER ────────────────────────────────────────────────── */
.wt-slider-col {
  position: sticky;
  top: 32px;
}
.slider-eyebrow {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 10px;
}
.slider-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 9 / 16;
  background: #111;
  border-radius: 3px;
  overflow: hidden;
}
.s-frame {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: bottom;
  z-index: 25;
  pointer-events: none;
  opacity: 0;
  transition: opacity .4s ease;
}
.s-frame.is-visible { opacity: 1; }
.di-pill {
  position: absolute;
  left: 34.27%;
  top: 1.545%;
  width: 31.46%;
  height: 5.43%;
  background: #000;
  border-radius: 9999px;
  z-index: 10;
  pointer-events: none;
  opacity: 0;
  transition: opacity .4s ease;
}
.di-pill.is-visible { opacity: 1; }
.prefix-panel {
  display: none;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  width: 100%;
  gap: 0;
  margin-top: 0;
}
.prefix-panel.is-visible { display: flex; }
.pp-frame {
  flex: 1;
  min-width: 0;
  aspect-ratio: 9 / 16;
  border-radius: 5px;
  overflow: hidden;
  background: #000;
  position: relative;
}
.pp-frame-overlay {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: bottom;
  pointer-events: none;
}
.pp-frame-di {
  position: absolute;
  left: 34.27%;
  top: 1.545%;
  width: 31.46%;
  height: 5.43%;
  background: #000;
  border-radius: 9999px;
  pointer-events: none;
}
.pp-replay {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.pp-replay.is-visible { display: flex; }
.pp-replay-btn {
  font-family: "IBM Plex Sans", system-ui, sans-serif;
  font-size: 12px;
  font-weight: 500;
  color: #fff;
  background: rgba(255,255,255,.15);
  border: none;
  border-radius: 999px;
  padding: 5px 12px;
  backdrop-filter: blur(4px);
}
.pp-black-overlay {
  position: absolute;
  inset: 0;
  background: #000;
  pointer-events: none;
  transition: opacity .5s ease;
}
.pp-black-overlay.is-gone { opacity: 0; }
.step6-replay-btn {
  display: none;
  width: 100%;
  margin: 10px 0 0;
  padding: 6px;
  font-family: "IBM Plex Sans", system-ui, sans-serif;
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  background: transparent;
  border: none;
  cursor: pointer;
  text-align: center;
}
.step6-replay-btn.is-visible { display: block; }
.step7-panel {
  display: none;
  width: 100%;
  padding: 2px 0;
}
.step7-panel.is-visible { display: block; }
.s8b-check {
  position: relative;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 12px;
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  user-select: none;
  padding: 5px 13px 5px 10px;
  border-radius: 999px;
  border: 2px solid var(--track);
  color: var(--track);
  transition: border-color .15s, color .15s, background .15s;
}
.s8b-check input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.s8b-check::before {
  content: '';
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-radius: 3px;
  box-sizing: border-box;
  opacity: 0.5;
  transition: background .15s, opacity .15s;
}
.s8b-check:has(input:checked)::before {
  background: currentColor;
  border-color: currentColor;
  opacity: 1;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6l3 3 4-4.5' stroke='white' stroke-width='1.9' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-size: cover;
}
.s8b-check:has(input[data-s8b="vocals"]:checked) { background: #3aaa6810; border-color: #3aaa68; color: #3aaa68; }
.s8b-check:has(input[data-s8b="music"]:checked)  { background: #b85c2610; border-color: #b85c26; color: #b85c26; }
.s8b-check:has(input[data-s8b="orig"]:checked)   { background: #5a6e8210; border-color: #5a6e82; color: #5a6e82; }
.step7-play-btn {
  display: block;
  margin: 10px auto 0;
  padding: 5px 18px;
  font-family: "IBM Plex Mono", monospace;
  font-size: 11px;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--hair);
  border-radius: 999px;
  cursor: pointer;
  letter-spacing: .06em;
}
.step7-play-btn:hover { border-color: var(--track); color: var(--ink); }
.pp-frame video {
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
}
.pp-frame:last-child video { object-fit: cover; }
.pp-plus {
  flex-shrink: 0;
  width: 28px;
  text-align: center;
  color: var(--muted);
  font-family: "IBM Plex Mono", monospace;
  font-size: 15px;
}
.slider-hint { transition: opacity .4s ease; }
.slider-hint.is-hidden { opacity: 0; pointer-events: none; }
.s-layer {
  position: absolute;
  inset: 0;
}
.s-layer video {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
#sliderContent {
  position: absolute;
  inset: 0;
  transition: transform .5s ease;
}
.show-crop #sliderContent {
  transform: scale(0.829);
  transform-origin: center bottom;
}
.crop-indicator {
  position: absolute;
  left: 8.55%; right: 8.55%; top: 17.1%; bottom: 0;
  border: 3px solid rgba(255,255,255,.88);
  border-radius: 7%;
  box-shadow: 0 0 0 9999px rgba(0,0,0,.45);
  pointer-events: none;
  z-index: 26;
  opacity: 0;
  transition: opacity .4s ease;
}
.show-crop .crop-indicator { opacity: 1; }
.split-div {
  position: absolute;
  top: 0; bottom: 0;
  width: 48px;
  transform: translateX(-50%);
  z-index: 20;
  cursor: ew-resize;
  touch-action: none;
  display: flex; align-items: center; justify-content: center;
}
.split-div::before {
  content: '';
  position: absolute; top: 0; bottom: 0; left: 50%;
  transform: translateX(-50%);
  width: 1.5px;
  background: rgba(255,255,255,.82);
  pointer-events: none;
}
.split-knob {
  position: relative; z-index: 1;
  width: 28px; height: 28px;
  background: #fff;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 1px 8px rgba(0,0,0,.35);
  pointer-events: none;
}
.split-knob svg { display: block; }
.slider-ba-labels {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  display: flex; justify-content: space-between;
  padding: 8px 10px;
  pointer-events: none;
  z-index: 30;
}
.ba-lbl {
  font-family: "IBM Plex Mono", monospace;
  font-size: 8.5px; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,.55);
  background: rgba(0,0,0,.3);
  padding: 3px 7px;
  border-radius: 2px;
}
.slider-hint {
  margin: 10px 0 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: 9px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted);
  opacity: .6;
  text-align: center;
}

/* ── RIGHT: STEPS ───────────────────────────────────────────────────────── */
.wt-steps { list-style: none; margin: 0; padding: 0; }
.wt-step {
  display: grid;
  grid-template-columns: 36px 1fr;
  gap: 0 20px;
  padding: 24px 0;
  border-top: 1px solid var(--hair);
  opacity: .35;
  transition: opacity .35s ease;
}
.wt-step.is-active { opacity: 1; }
.wt-step.is-pending { opacity: .18; }
.wt-gutter {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 3px;
}
.wt-dot {
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 1.5px solid var(--ink);
  display: flex; align-items: center; justify-content: center;
  font-family: "IBM Plex Mono", monospace;
  font-size: 10px; font-weight: 500;
  color: var(--ink);
  flex-shrink: 0;
  background: var(--bg);
}
.wt-step.is-pending .wt-dot {
  border-style: dashed;
  border-color: var(--muted);
  color: var(--muted);
}
.wt-line {
  width: 1px; flex: 1; min-height: 16px;
  background: var(--hair);
  margin: 5px 0 0;
}
.wt-step:last-child .wt-line { display: none; }
.wt-body {
  min-width: 0;
  transform: scale(.88);
  transform-origin: top left;
  transition: transform .35s ease;
}
.wt-step.is-active .wt-body { transform: scale(1); }
.wt-step-label {
  font-family: "IBM Plex Mono", monospace;
  font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 6px;
}
.wt-step-title {
  font-size: 16px; font-weight: 600;
  letter-spacing: -.012em; line-height: 1.2;
  color: var(--ink);
  margin: 0 0 10px;
}
.wt-desc {
  font-size: 13.5px; line-height: 1.72;
  color: #4a4640;
  margin: 0;
}

/* ── RESPONSIVE ─────────────────────────────────────────────────────────── */
@media (max-width: 680px) {
  .wt-content { grid-template-columns: 1fr; }
  .wt-slider-col { position: static; margin-bottom: 40px; }
  .slider-wrap { max-width: 220px; margin: 0 auto; }
}
</style>

<div class="wt">

  <header class="wt-header">
    <h2 class="wt-title">How the original footage was transformed</h2>
  </header>

  <div class="wt-tl">
    <svg id="tlSvg"></svg>
  </div>

  <div class="wt-content">

    <aside class="wt-slider-col">
      <p class="slider-eyebrow" id="sliderEyebrow">Before / After</p>
      <div class="slider-wrap" id="sliderWrap">
        <div class="crop-indicator"></div>
        <div id="sliderContent"></div>
        <img class="s-frame" id="phoneFrame" src="/assets/iphone-frame.png" alt="" />
        <img class="s-frame" id="phoneCutout" src="/assets/iphone-cutout.svg" alt="" />
        <div class="di-pill" id="diPill"></div>
        <div class="pp-replay" id="step12Replay" style="z-index:35;">
          <div class="pp-replay-btn">Replay</div>
        </div>
        <div id="step11Overlay" style="display:none;position:absolute;inset:0;z-index:30;background:#000;">
          <video id="step11Vid" src="/assets/subtitles-after.mp4" playsinline="" preload="auto" style="width:100%;height:100%;object-fit:cover;display:block;"></video>
          <div class="pp-replay" id="step11Replay">
            <div class="pp-replay-btn">Replay</div>
          </div>
        </div>
      </div>
      <div class="prefix-panel" id="prefixPanel">
        <div class="pp-frame">
          <video id="prefixVid" src="/assets/prefix.mp4" playsinline="" preload="auto"></video>
          <div class="pp-replay" id="prefixReplay">
            <div class="pp-replay-btn">Replay</div>
          </div>
        </div>
        <div class="pp-plus">+</div>
        <div class="pp-frame">
          <video id="afterFrameVid" src="/assets/silvertone.mp4" muted="" playsinline="" preload="auto"></video>
          <div class="pp-black-overlay" id="afterOverlay"></div>
          <div class="pp-frame-di"></div>
          <img class="pp-frame-overlay" src="/assets/iphone-frame.png" alt="" />
        </div>
      </div>
      <div class="prefix-panel" id="step6Panel">
        <div class="pp-frame">
          <video id="step6PrefixVid" src="/assets/prefix.mp4" playsinline="" preload="auto"></video>
        </div>
        <div class="pp-plus">+</div>
        <div class="pp-frame" id="step6AfterFrame">
          <video id="step6AfterVid" src="/assets/silvertone.mp4" muted="" playsinline="" preload="auto"></video>
          <div class="pp-black-overlay" id="step6Overlay"></div>
          <div class="pp-frame-di"></div>
          <img class="pp-frame-overlay" src="/assets/iphone-frame.png" alt="" />
        </div>
      </div>
      <button class="step6-replay-btn" id="step6ReplayBtn">Replay transition</button>
      <div id="step8bPanel" class="step7-panel">
        <svg id="s8bSvgV" style="display:block;overflow:visible;width:100%;"></svg>
        <svg id="s8bSvgM" style="display:block;overflow:visible;width:100%;"></svg>
        <svg id="s8bSvgO" style="display:block;overflow:visible;width:100%;"></svg>
        <div id="step8bControls" style="display:flex;flex-direction:column;align-items:center;gap:14px;margin-top:20px;">
          <button id="step8bPlayBtn" class="step7-play-btn" style="margin:0;">▶ Play</button>
          <div style="display:flex;flex-direction:column;align-items:flex-start;gap:5px;">
            <div style="font-family:system-ui,-apple-system,sans-serif;font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);opacity:0.6;">Playing tracks</div>
            <div style="display:flex;align-items:center;gap:10px;">
              <label class="s8b-check"><input type="checkbox" data-s8b="vocals" checked="" /> Vocals</label>
              <label class="s8b-check"><input type="checkbox" data-s8b="music" checked="" /> Music</label>
              <label class="s8b-check"><input type="checkbox" data-s8b="orig" checked="" /> Concert</label>
            </div>
          </div>
        </div>
      </div>
      <div id="step8Panel" class="step7-panel">
        <svg id="step8SvgTop" style="display:block;overflow:visible;width:100%;"></svg>
        <p style="font-family:system-ui,-apple-system,sans-serif;font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);opacity:0.5;margin:2px 0 0;">Before</p>
        <button id="step8PlayBtn" class="step7-play-btn" style="font-family:system-ui,-apple-system,sans-serif;font-weight:600;letter-spacing:.02em;margin:5px auto 9px;">▶ Play</button>
        <svg id="step8SvgBot" style="display:block;overflow:visible;width:100%;"></svg>
        <p style="font-family:system-ui,-apple-system,sans-serif;font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);opacity:0.5;margin:2px 0 0;">Processed</p>
      </div>
      <div id="step7Panel" class="step7-panel">
        <svg id="step7Svg" style="display:block;overflow:visible;width:100%;"></svg>
        <button id="step7PlayBtn" class="step7-play-btn">▶ Play</button>
      </div>
      <p class="slider-hint">Drag to compare</p>
    </aside>

    <ol class="wt-steps" id="wtSteps"></ol>

  </div>

</div>

<script>
(function () {
  const FINAL = 46.9, IN = 14.1, OUT = 37.9;
  const S7_TRIM = 11.6, S7_OVERLAP = 0.5;
  const MUSIC_END = 32.3; // stems-relative duration of music; right handle position
  const P  = { track:'#d6d2c8', hair:'#e6e2d9', source:'#b85c26', spotify:'#4a7c59', credits:'#5a6e82' };
  const MF = '"IBM Plex Mono", ui-monospace, monospace';
  const SF = '"IBM Plex Sans", system-ui, sans-serif';

  const VIDEOS = {
    original:   '/assets/original.mp4',
    silvertone: '/assets/silvertone.mp4',
  };

  const STEPS = [
    { id:1, label:'Step 1 · Color grade', title:'Silvertone filter applied', change:'Silvertone', desc:'Simply, at first the Silvertone filter from the Photos app was applied to give it a cinematic look and obscure the lack of color depth in the footage. Tested with a few LUTs from CapCut for a long time before settling on this one.', lanes:[] },
    { id:2, label:'Step 2 · Cropped for a reel', title:'Cropped to 9:16 Reel', change:'9:16 crop', desc:'scaled the footage up 1.21× (to bring the subject in view) and cropped to 1080×1920, the dimension for Instagram Reels.', lanes:[] },
    { id:3, label:'Step 3 · Frame overlay', title:'iPhone frame overlaid', change:'iPhone frame', desc:'Screenshot of the Camera app open — screen covered by my hand as closely as possible, was used. But even with the seemingly pitch black background every background removal tool I tried, including Canva, failed to cleanly isolate it.\nThe reason turned out to be, even though invisible to the naked eye, light had leaked through causing an invisible shadow that I could only see when segmenting every unique pixel. So I expanded my flexibility, dialling it up and down until t7, the darkest 7 shades of black — gave me the cleanest result.', lanes:[] },
    { id:4, label:'Step 4 · Dynamic Island', title:'Dynamic Island cutout added', change:'Dynamic Island', desc:'Screenshots don\'t include Dynamic Island, apparently behind the image is a pill-shaped cutout at the top of the screen. To make it transparent in the composite, a matching ellipse was punched through the video layer beneath it, so the frame’s existing hole aligns perfectly and the background shows through rather than a solid fill.', lanes:[] },
    { id:5, label:'Step 5 · Prefix clip', title:'Prefix clip merged before the shot', change:'Prefix clip', desc:'The recording opens with a lead-in shot — captured immediately before the main footage. Its last frame is identical to the first frame of the main clip, so the cut is invisible when they’re joined.', lanes:[] },
    { id:6, label:'Step 6 · Transition', title:'Clips joined seamlessly', change:'Transition', desc:'The prefix clip and main shot are merged. The last frame of the prefix is identical to the first frame of the main clip, so the cut is invisible.', lanes:[] },
    { id:7, label:'Step 7 · Trim', title:'Silvertone clip trimmed to 14.1s', change:'Trim', desc:'The silvertone clip is trimmed so it starts at 14.1s — exactly where the Spotify section ends. This makes the transition between the app screen and the real-life footage seamless.', lanes:[] },
    { id:8, label:'Step 8 · Backing track', title:'Spotify track played into audio', change:'Backing track', desc:'The studio version of the song was played through Spotify, then its stems were split using <a href="https://vocalremover.org" target="_blank" rel="noopener" style="color:inherit;opacity:.6;font-size:.9em">vocalremover.org</a> — isolating the instrumental backing track. This is layered underneath the live concert audio in the final edit.', lanes:[] },
    { id:9, label:'Step 9 · Audio restore',  title:'MP3 Music Restoration', change:'Apollo restore', desc:'The concert audio was processed through Apollo (2025) by Neural Analog — an AI-powered MP3 restoration tool that recovers high-frequency detail and removes compression artefacts lost in the original recording. <a href="https://www.neuralanalog.com" target="_blank" rel="noopener" style="color:inherit;opacity:.6;font-size:.9em">neuralanalog.com</a>', lanes:[] },
    { id:10, label:'Step 10 · Subtitles', title:'Karaoke-fill captions added', change:'Subtitles', desc:'Subtitles are added in CapCut using the fill-text animation. The fill sweeps linearly left-to-right, but the number of letters revealed at any moment is proportional to the length of enunciation — longer syllables take more horizontal space, so the fill front tracks the singer\'s voice naturally.', lanes:[] },
    { id:11, label:'Step 11 · Credits', title:'Rolling credits screen', change:'Credits', desc:'After the final frame fades to black, the screen holds for a beat. The credit screen cuts in exactly on the snare hit. Made with Dazzcam.', lanes:[] },
    { id:12, label:'Step 12 · Hook text', title:'Hook text overlay added', change:'Hook text', desc:'A short hook text is added at the top of the reel — the kind Instagram edits use to stop the scroll. It appears immediately and stays just long enough to pull the viewer in before the footage takes over.', lanes:[] },
  ];

  // ── Slider ──────────────────────────────────────────────────────────────
  function buildSlider() {
    const wrap    = document.getElementById('sliderWrap');
    const content = document.getElementById('sliderContent');
    const below = makeLayer(VIDEOS.silvertone, 1, 'sliderBelowVid');
    const above = makeLayer(VIDEOS.original,   2, 'sliderAboveVid');
    setClip(above, 50);

    const div = document.createElement('div');
    div.className = 'split-div';
    div.style.left = '50%';
    div.innerHTML = `<div class="split-knob"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="#666" stroke-width="1.4" stroke-linecap="round"><path d="M4 7H1M13 7h-3M4 4L1 7l3 3M10 4l3 3-3 3"/></svg></div>`;

    const labels = document.createElement('div');
    labels.className = 'slider-ba-labels';
    labels.innerHTML = `<span class="ba-lbl">Before</span><span class="ba-lbl">After</span>`;

    content.appendChild(below); content.appendChild(above);
    content.appendChild(div);   content.appendChild(labels);

    const afterWrap = document.createElement('div');
    afterWrap.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:25;';
    afterWrap.style.clipPath = 'inset(0 0 0 50%)';
    ['phoneFrame', 'diPill'].forEach(id => {
      const el = document.getElementById(id);
      if (el) afterWrap.appendChild(el);
    });
    content.appendChild(afterWrap);

    let dragging = false;
    const move = cx => {
      const rect = wrap.getBoundingClientRect();
      const pct  = Math.max(4, Math.min(96, (cx - rect.left) / rect.width * 100));
      div.style.left = pct + '%';
      setClip(above, pct);
      afterWrap.style.clipPath = `inset(0 0 0 ${pct}%)`;
    };
    div.addEventListener('mousedown',  e => { dragging = true; e.preventDefault(); });
    window.addEventListener('mousemove', e => { if (dragging) move(e.clientX); });
    window.addEventListener('mouseup',   () => { dragging = false; });
    div.addEventListener('touchstart', e => { dragging = true; e.preventDefault(); }, { passive: false });
    window.addEventListener('touchmove', e => { if (dragging) move(e.touches[0].clientX); }, { passive: true });
    window.addEventListener('touchend',  () => { dragging = false; });
  }

  function setClip(el, pct) { el.style.clipPath = `inset(0 ${100 - pct}% 0 0)`; }

  function makeLayer(src, z, vidId) {
    const div = document.createElement('div');
    div.className = 's-layer'; div.style.zIndex = z;
    const vid = document.createElement('video');
    vid.src = src; vid.muted = true; vid.autoplay = true;
    vid.playsInline = true; vid.loop = true;
    vid.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;';
    if (vidId) vid.id = vidId;
    vid.play().catch(() => {});
    div.appendChild(vid);
    return div;
  }

  // ── Step 8 audio-timeline visualization (before/after Apollo restore) ──
  let s8Before = null, s8After = null, s8Playing = false, s8Active = 'before', s8Raf = null;

  function step8UpdateBtn() {
    const btn = document.getElementById('step8PlayBtn');
    if (!btn) return;
    if (!s8Playing) {
      btn.textContent = '▶ Play';
      ['step8SvgTop','step8SvgBot'].forEach(id => { const el = document.getElementById(id); if (el) el.style.opacity = '1'; });
      return;
    }
    btn.textContent = s8Active === 'before' ? '⇅ Switch to Processed' : '⇅ Switch to Before';
    const svgTop = document.getElementById('step8SvgTop');
    const svgBot = document.getElementById('step8SvgBot');
    if (svgTop) svgTop.style.opacity = s8Active === 'before' ? '1' : '0.25';
    if (svgBot) svgBot.style.opacity = s8Active === 'after'  ? '1' : '0.25';
  }

  function step8Stop() {
    if (s8Before) { s8Before.pause(); }
    if (s8After)  { s8After.pause(); }
    s8Playing = false;
    if (s8Raf) { cancelAnimationFrame(s8Raf); s8Raf = null; }
    step8UpdateBtn();
    ['s8ph0','s8ph1'].forEach(id => { const el = document.getElementById(id); if (el) el.setAttribute('opacity','0'); });
  }

  function step8Switch() {
    if (!s8Playing) return;
    s8Active = s8Active === 'before' ? 'after' : 'before';
    s8Before.muted = s8Active !== 'before';
    s8After.muted  = s8Active !== 'after';
    step8UpdateBtn();
  }

  function step8PlayPause() {
    if (s8Playing) { step8Stop(); return; }
    if (!s8Before) s8Before = new Audio('/assets/silvertone-trimmed.mp3');
    if (!s8After)  s8After  = new Audio('/assets/silvertone-restored.mp3');
    s8Playing = true; s8Active = 'before';
    const t0 = 0;
    s8Before.currentTime = t0; s8Before.muted = false;
    s8After.currentTime  = t0; s8After.muted  = true;
    step8UpdateBtn();

    const done = () => { step8Stop(); };
    s8Before.onended = done;
    s8After.onended  = done;

    s8Before.play().catch(() => step8Stop());
    s8After.play().catch(() => {});

    const PAD = 16;
    function tick() {
      if (!s8Playing) { s8Raf = null; return; }
      const svgT = document.getElementById('step8SvgTop');
      const svgB = document.getElementById('step8SvgBot');
      const ph0  = document.getElementById('s8ph0');
      const ph1  = document.getElementById('s8ph1');
      if (svgT && ph0) {
        const vb = svgT.viewBox.baseVal;
        const W  = vb && vb.width > 0 ? vb.width : (svgT.clientWidth || 240);
        const dur = (s8Before && isFinite(s8Before.duration) && s8Before.duration > 0) ? s8Before.duration : 26;
        const sc  = (W - PAD * 2) / dur;
        const src = s8Active === 'before' ? s8Before : s8After;
        const x   = PAD + src.currentTime * sc;
        if (ph0) { ph0.setAttribute('x1',x); ph0.setAttribute('x2',x); ph0.setAttribute('opacity', s8Active === 'before' ? '1' : '0.3'); }
        if (ph1 && svgB) {
          const vb2 = svgB.viewBox.baseVal;
          const W2  = vb2 && vb2.width > 0 ? vb2.width : (svgB.clientWidth || 240);
          const sc2 = (W2 - PAD * 2) / dur;
          const x2  = PAD + src.currentTime * sc2;
          ph1.setAttribute('x1',x2); ph1.setAttribute('x2',x2); ph1.setAttribute('opacity', s8Active === 'after' ? '1' : '0.3');
        }
      }
      s8Raf = requestAnimationFrame(tick);
    }
    s8Raf = requestAnimationFrame(tick);

    const btn = document.getElementById('step8PlayBtn');
    if (btn) btn.onclick = () => { if (s8Playing) step8Switch(); else step8PlayPause(); };
  }

  function buildStep8Viz() {
    const svgTop = document.getElementById('step8SvgTop');
    const svgBot = document.getElementById('step8SvgBot');
    if (!svgTop || !svgBot) return;
    const W = svgTop.parentElement.clientWidth || 240;
    const PAD = 16, waveW = W - PAD * 2;
    const dur = (s8Before && isFinite(s8Before.duration) && s8Before.duration > 0) ? s8Before.duration : 26;

    const SLATE = '#5a6e82';
    const AMBER = '#b85c26';
    const RULER_H = 18, WAVE_H = 48, totalH = RULER_H + WAVE_H;

    function srand(seed) {
      let s = seed >>> 0;
      return () => { s = (Math.imul(s,1664525)+1013904223)>>>0; return s/0xffffffff; };
    }
    function waveform(svg, seed, color, opac, phId) {
      const o = [], midY = RULER_H + WAVE_H / 2, rand = srand(seed), N = Math.ceil(waveW/2.4);
      const int = dur <= 15 ? 5 : dur <= 30 ? 10 : 15;
      o.push(`<line x1="${PAD}" y1="${RULER_H}" x2="${PAD+waveW}" y2="${RULER_H}" stroke="${color}" stroke-width="0.5" opacity="0.3"/>`);
      for (let t=0; t<=dur+.001; t+=int) {
        const tx = PAD+(t/dur)*waveW;
        o.push(`<line x1="${tx.toFixed(1)}" y1="${RULER_H-5}" x2="${tx.toFixed(1)}" y2="${RULER_H}" stroke="${color}" stroke-width="0.8" opacity="0.4"/>`);
        o.push(`<text x="${tx.toFixed(1)}" y="${RULER_H-7}" text-anchor="middle" font-size="8" font-family=${MF} fill="${color}" opacity="0.55">${t}s</text>`);
      }
      o.push(`<rect x="${PAD}" y="${RULER_H}" width="${waveW}" height="${WAVE_H}" fill="${color}" opacity="0.07" rx="3"/>`);
      for (let i=0;i<N;i++) {
        const t=i/N, bx=PAD+t*waveW;
        const amp = 0.38*Math.sin(t*Math.PI*4.1)+0.22*Math.sin(t*Math.PI*11.3+0.7)
                  + 0.18*Math.sin(t*Math.PI*2.2+1.8)+0.12*(rand()*2-1);
        const bh = Math.max(2,Math.abs(amp)*WAVE_H*0.44);
        o.push(`<rect x="${bx.toFixed(1)}" y="${(midY-bh).toFixed(1)}" width="1.8" height="${(bh*2).toFixed(1)}" fill="${color}" opacity="${opac}" rx=".9"/>`);
      }
      o.push(`<line id="${phId}" x1="${PAD}" y1="${RULER_H-4}" x2="${PAD}" y2="${RULER_H+WAVE_H+4}" stroke="${color}" stroke-width="1.5" opacity="0" stroke-linecap="round"/>`);
      svg.setAttribute('viewBox', `0 0 ${W} ${totalH}`);
      svg.setAttribute('height', totalH);
      svg.innerHTML = o.join('');
    }

    waveform(svgTop, 11, SLATE, 0.55, 's8ph0');
    waveform(svgBot, 88, AMBER, 0.60, 's8ph1');

    const btn = document.getElementById('step8PlayBtn');
    if (btn) btn.onclick = () => { if (s8Playing) step8Switch(); else step8PlayPause(); };
  }

  // ── Step 8b stems visualization (vocals / music / concert) ─────────────
  // Master clock: s8bMusic.currentTime drives all playheads and side-effects.
  // Tracks play only within their handle range; playheads are clamped to handles.
  // Concert audio starts when master clock hits VOCALS_END, seeked to S7_TRIM.
  let s8bVocals = null, s8bMusic = null, s8bOrig = null, s8bOrigCtx = null;
  const S8B_STEMS_START = 8.19; // stems begin at this offset on the shared timeline
  const VOCALS_END = S7_TRIM - S8B_STEMS_START; // 3.41s into stems → vocals stop, concert starts
  let s8bPlaying = false, s8bRaf = null, s8bConcertStarted = false;

  function s8bChecked(key) {
    const el = document.querySelector(`#step8bControls input[data-s8b="${key}"]`);
    return el && el.checked;
  }

  function s8bApplyMutes() {
    if (s8bVocals) s8bVocals.muted = !s8bChecked('vocals');
    if (s8bMusic)  s8bMusic.muted  = !s8bChecked('music');
    if (s8bOrig)   s8bOrig.muted   = !s8bChecked('orig');
  }

  function step8bStop() {
    [s8bVocals, s8bMusic, s8bOrig].forEach(a => { if (a) { a.pause(); a.onended = null; } });
    s8bPlaying = false; s8bConcertStarted = false;
    if (s8bRaf) { cancelAnimationFrame(s8bRaf); s8bRaf = null; }
    const btn = document.getElementById('step8bPlayBtn');
    if (btn) btn.textContent = '▶ Play';
    ['s8bphV','s8bphM','s8bphO'].forEach(id => {
      const el = document.getElementById(id); if (el) el.setAttribute('opacity','0');
    });
  }

  function step8bPlayPause() {
    if (s8bPlaying) { step8bStop(); return; }
    if (!s8bVocals) s8bVocals = new Audio('/assets/stems-vocals.mp3');
    if (!s8bMusic)  s8bMusic  = new Audio('/assets/stems-music.mp3');
    if (!s8bOrig)   s8bOrig   = new Audio('/assets/silvertone.mp3');
    if (!s8bOrigCtx) {
      s8bOrigCtx = new (window.AudioContext || window.webkitAudioContext)();
      const gain = s8bOrigCtx.createGain();
      gain.gain.value = 2;
      s8bOrigCtx.createMediaElementSource(s8bOrig).connect(gain);
      gain.connect(s8bOrigCtx.destination);
    }
    s8bOrigCtx.resume().catch(() => {});

    s8bPlaying = true; s8bConcertStarted = false;
    s8bVocals.currentTime = 0; s8bVocals.muted = !s8bChecked('vocals');
    s8bMusic.currentTime  = 0; s8bMusic.muted  = !s8bChecked('music');
    s8bOrig.muted = !s8bChecked('orig');

    document.querySelectorAll('#step8bControls input[type=checkbox]').forEach(cb => { cb.onchange = s8bApplyMutes; });
    const btn = document.getElementById('step8bPlayBtn');
    if (btn) btn.textContent = '⏸ Pause';
    Promise.all([s8bVocals, s8bMusic].map(a => a.play().catch(() => {})));

    function tick() {
      if (!s8bPlaying) { s8bRaf = null; return; }
      const PAD = 16;
      const stemsDur = s8bMusic && isFinite(s8bMusic.duration) && s8bMusic.duration > 0 ? s8bMusic.duration : 59.77;
      const totalDur = S8B_STEMS_START + stemsDur;
      const mCt = s8bMusic ? s8bMusic.currentTime : 0; // master clock

      // Music: stop at MUSIC_END handle
      if (mCt >= MUSIC_END) { step8bStop(); return; }

      // Vocals: play only within [0, VOCALS_END] on stems timeline
      if (s8bVocals && !s8bVocals.paused && mCt >= VOCALS_END) s8bVocals.pause();

      // Concert: start when master clock reaches VOCALS_END, seek to S7_TRIM in silvertone
      if (!s8bConcertStarted && mCt >= VOCALS_END) {
        s8bConcertStarted = true;
        s8bOrig.currentTime = S7_TRIM;
        s8bOrig.play().catch(() => {});
      }
      // Concert: stop at OUT handle
      if (s8bOrig && !s8bOrig.paused && s8bConcertStarted && s8bOrig.currentTime >= OUT) s8bOrig.pause();

      // Track descriptors: all positions expressed as absolute time on the shared timeline
      const tracks = [
        {
          svgId: 's8bSvgV', phId: 's8bphV', key: 'vocals',
          // playhead travels [S8B_STEMS_START, S7_TRIM]; clamp at trim handle
          pos: S8B_STEMS_START + Math.min(mCt, VOCALS_END),
          visible: true,
        },
        {
          svgId: 's8bSvgM', phId: 's8bphM', key: 'music',
          // playhead travels [S8B_STEMS_START, S8B_STEMS_START + MUSIC_END]; clamp at right handle
          pos: S8B_STEMS_START + Math.min(mCt, MUSIC_END),
          visible: true,
        },
        {
          svgId: 's8bSvgO', phId: 's8bphO', key: 'orig',
          // playhead travels [S7_TRIM, OUT] in silvertone coords (same shared timeline)
          pos: s8bConcertStarted ? Math.min(Math.max(s8bOrig.currentTime, S7_TRIM), OUT) : S7_TRIM,
          visible: s8bConcertStarted,
        },
      ];

      tracks.forEach(({ svgId, phId, key, pos, visible }) => {
        const svg = document.getElementById(svgId);
        const ph  = document.getElementById(phId);
        if (!svg || !ph) return;
        const vb = svg.viewBox.baseVal, W = vb && vb.width > 0 ? vb.width : (svg.clientWidth || 240);
        const x = PAD + pos * (W - PAD * 2) / totalDur;
        ph.setAttribute('x1', x); ph.setAttribute('x2', x);
        ph.setAttribute('opacity', visible && s8bChecked(key) ? '1' : '0.15');
      });

      s8bRaf = requestAnimationFrame(tick);
    }
    s8bRaf = requestAnimationFrame(tick);
  }

  function buildStep8bViz() {
    const stemsDur  = s8bMusic && isFinite(s8bMusic.duration) && s8bMusic.duration > 0 ? s8bMusic.duration : 59.77;
    const silverDur = s8bOrig  && isFinite(s8bOrig.duration)  && s8bOrig.duration  > 0 ? s8bOrig.duration  : 46.5;
    const totalDur  = S8B_STEMS_START + stemsDur;
    const PAD = 16, RULER_H = 18, WAVE_H = 46;

    function srand(seed) {
      let s = seed >>> 0;
      return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 0xffffffff; };
    }

    // clipStart/clipEnd = full waveform extent; trimAt = vocals trim; stopAt = early right handle (waveform continues dimmed after)
    function drawRow(svgId, color, seed, label, phId, clipStart, clipEnd, trimAt, stopAt) {
      const svg = document.getElementById(svgId);
      if (!svg) return;
      const W = svg.parentElement.clientWidth || 240;
      const waveW = W - PAD * 2, sc = waveW / totalDur;
      const cx = PAD + clipStart * sc, cw = (clipEnd - clipStart) * sc;
      const midY = RULER_H + WAVE_H / 2;
      const o = [];

      const int = totalDur <= 40 ? 10 : 20;
      o.push(`<line x1="${PAD}" y1="${RULER_H}" x2="${PAD + waveW}" y2="${RULER_H}" stroke="${color}" stroke-width="0.5" opacity="0.25"/>`);
      for (let t = 0; t <= totalDur + 0.001; t += int) {
        const tx = (PAD + (t / totalDur) * waveW).toFixed(1);
        o.push(`<line x1="${tx}" y1="${(RULER_H - 4).toFixed(1)}" x2="${tx}" y2="${RULER_H}" stroke="${color}" stroke-width="0.8" opacity="0.35"/>`);
        o.push(`<text x="${tx}" y="${(RULER_H - 6).toFixed(1)}" text-anchor="middle" font-size="8" font-family=${MF} fill="${color}" opacity="0.5">${t}s</text>`);
      }

      o.push(`<rect x="${cx.toFixed(1)}" y="${RULER_H}" width="${cw.toFixed(1)}" height="${WAVE_H}" fill="${color}" opacity="0.07" rx="3"/>`);
      const rand = srand(seed), N = Math.ceil(cw / 2.4);
      for (let j = 0; j < N; j++) {
        const t = j / N, bx = cx + t * cw;
        const amp = 0.38 * Math.sin(t * Math.PI * 4.1) + 0.22 * Math.sin(t * Math.PI * 11.3 + 0.7)
                  + 0.18 * Math.sin(t * Math.PI * 2.2 + 1.8) + 0.12 * (rand() * 2 - 1);
        const bh = Math.max(2, Math.abs(amp) * WAVE_H * 0.44);
        o.push(`<rect x="${bx.toFixed(1)}" y="${(midY - bh).toFixed(1)}" width="1.8" height="${(bh * 2).toFixed(1)}" fill="${color}" opacity="0.6" rx=".9"/>`);
      }

      if (trimAt !== undefined) {
        // vocals: fade zone then dim to end
        const trimX    = PAD + trimAt * sc;
        const fadeEndX = trimX + S7_OVERLAP * sc;
        o.push(`<rect x="${trimX.toFixed(1)}" y="${RULER_H}" width="${(fadeEndX - trimX).toFixed(1)}" height="${WAVE_H}" fill="white" opacity="0.4"/>`);
        o.push(`<rect x="${fadeEndX.toFixed(1)}" y="${RULER_H}" width="${(cx + cw - fadeEndX).toFixed(1)}" height="${WAVE_H}" fill="white" opacity="0.65"/>`);
        [cx, trimX].forEach(hx => {
          o.push(`<line x1="${hx.toFixed(1)}" y1="${(RULER_H - 6).toFixed(1)}" x2="${hx.toFixed(1)}" y2="${(RULER_H + WAVE_H + 6).toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"/>`);
        });
      } else if (stopAt !== undefined) {
        // music: handle at stopAt, waveform continues dimmed beyond it
        const stopX = PAD + stopAt * sc;
        o.push(`<rect x="${stopX.toFixed(1)}" y="${RULER_H}" width="${(cx + cw - stopX).toFixed(1)}" height="${WAVE_H}" fill="white" opacity="0.6"/>`);
        [cx, stopX].forEach(hx => {
          o.push(`<line x1="${hx.toFixed(1)}" y1="${(RULER_H - 6).toFixed(1)}" x2="${hx.toFixed(1)}" y2="${(RULER_H + WAVE_H + 6).toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"/>`);
        });
      } else {
        [cx, cx + cw].forEach(hx => {
          o.push(`<line x1="${hx.toFixed(1)}" y1="${(RULER_H - 6).toFixed(1)}" x2="${hx.toFixed(1)}" y2="${(RULER_H + WAVE_H + 6).toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"/>`);
        });
      }

      o.push(`<text x="${(cx + cw + 8).toFixed(1)}" y="${(midY + 4).toFixed(1)}" text-anchor="start" font-size="9" font-weight="600" font-family=${MF} fill="${color}" opacity="0.7" letter-spacing=".06em">${label}</text>`);
      o.push(`<line id="${phId}" x1="${cx.toFixed(1)}" y1="${(RULER_H - 4).toFixed(1)}" x2="${cx.toFixed(1)}" y2="${(RULER_H + WAVE_H + 4).toFixed(1)}" stroke="${color}" stroke-width="1.5" opacity="0" stroke-linecap="round"/>`);
      svg.setAttribute('viewBox', `0 0 ${W} ${RULER_H + WAVE_H}`);
      svg.setAttribute('height', RULER_H + WAVE_H);
      svg.innerHTML = o.join('');
    }

    function drawConcertRow() {
      const color = '#5a6e82', svg = document.getElementById('s8bSvgO');
      if (!svg) return;
      const W = svg.parentElement.clientWidth || 240;
      const waveW = W - PAD * 2, sc = waveW / totalDur;
      const cx = PAD, cw = silverDur * sc;
      const trimX = PAD + S7_TRIM * sc, outX = PAD + OUT * sc;
      const midY  = RULER_H + WAVE_H / 2;
      const o = [];

      const int = totalDur <= 40 ? 10 : 20;
      o.push(`<line x1="${PAD}" y1="${RULER_H}" x2="${PAD + waveW}" y2="${RULER_H}" stroke="${color}" stroke-width="0.5" opacity="0.25"/>`);
      for (let t = 0; t <= totalDur + 0.001; t += int) {
        const tx = (PAD + (t / totalDur) * waveW).toFixed(1);
        o.push(`<line x1="${tx}" y1="${(RULER_H - 4).toFixed(1)}" x2="${tx}" y2="${RULER_H}" stroke="${color}" stroke-width="0.8" opacity="0.35"/>`);
        o.push(`<text x="${tx}" y="${(RULER_H - 6).toFixed(1)}" text-anchor="middle" font-size="8" font-family=${MF} fill="${color}" opacity="0.5">${t}s</text>`);
      }

      o.push(`<rect x="${cx.toFixed(1)}" y="${RULER_H}" width="${cw.toFixed(1)}" height="${WAVE_H}" fill="${color}" opacity="0.07" rx="3"/>`);
      o.push(`<rect x="${trimX.toFixed(1)}" y="${(RULER_H - 3).toFixed(1)}" width="${(outX - trimX).toFixed(1)}" height="${WAVE_H + 6}" fill="${color}" opacity="0.13" rx="2"/>`);
      const rand = srand(11), N = Math.ceil(cw / 2.4);
      for (let j = 0; j < N; j++) {
        const t = j / N, bx = cx + t * cw;
        const amp = 0.38 * Math.sin(t * Math.PI * 4.1) + 0.22 * Math.sin(t * Math.PI * 11.3 + 0.7)
                  + 0.18 * Math.sin(t * Math.PI * 2.2 + 1.8) + 0.12 * (rand() * 2 - 1);
        const bh = Math.max(2, Math.abs(amp) * WAVE_H * 0.44);
        o.push(`<rect x="${bx.toFixed(1)}" y="${(midY - bh).toFixed(1)}" width="1.8" height="${(bh * 2).toFixed(1)}" fill="${color}" opacity="0.6" rx=".9"/>`);
      }
      o.push(`<rect x="${PAD}" y="${RULER_H}" width="${(S7_TRIM * sc).toFixed(1)}" height="${WAVE_H}" fill="white" opacity="0.5"/>`);
      o.push(`<rect x="${outX.toFixed(1)}" y="${RULER_H}" width="${((silverDur - OUT) * sc).toFixed(1)}" height="${WAVE_H}" fill="white" opacity="0.5"/>`);
      [trimX, outX].forEach(hx => {
        o.push(`<line x1="${hx.toFixed(1)}" y1="${(RULER_H - 6).toFixed(1)}" x2="${hx.toFixed(1)}" y2="${(RULER_H + WAVE_H + 6).toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"/>`);
      });
      o.push(`<text x="${(PAD + silverDur * sc + 8).toFixed(1)}" y="${(midY + 4).toFixed(1)}" text-anchor="start" font-size="9" font-weight="600" font-family=${MF} fill="${color}" opacity="0.7" letter-spacing=".06em">CONCERT</text>`);
      o.push(`<line id="s8bphO" x1="${cx.toFixed(1)}" y1="${(RULER_H - 4).toFixed(1)}" x2="${cx.toFixed(1)}" y2="${(RULER_H + WAVE_H + 4).toFixed(1)}" stroke="${color}" stroke-width="1.5" opacity="0" stroke-linecap="round"/>`);
      svg.setAttribute('viewBox', `0 0 ${W} ${RULER_H + WAVE_H}`);
      svg.setAttribute('height', RULER_H + WAVE_H);
      svg.innerHTML = o.join('');
    }

    drawRow('s8bSvgV', '#3aaa68', 37, 'VOCALS', 's8bphV', S8B_STEMS_START, S8B_STEMS_START + stemsDur, S7_TRIM);
    drawRow('s8bSvgM', '#b85c26', 73, 'MUSIC',  's8bphM', S8B_STEMS_START, S8B_STEMS_START + stemsDur, undefined, S8B_STEMS_START + MUSIC_END);
    drawConcertRow();

    const btn = document.getElementById('step8bPlayBtn');
    if (btn) btn.onclick = step8bPlayPause;
    document.querySelectorAll('#step8bControls input[type=checkbox]').forEach(cb => { cb.onchange = s8bApplyMutes; });
  }

  // ── Step 7 audio-timeline visualization ────────────────────────────────
  let s7Audio1 = null, s7Audio2 = null, s7Playing = false, s7Phase = 0, s7Raf = null, s7XfadeStarted = false;

  function step7Stop() {
    [s7Audio1, s7Audio2].forEach(a => { if (a) { a.pause(); a.onended = null; a.volume = 1; } });
    s7Playing = false; s7Phase = 0; s7XfadeStarted = false;
    if (s7Raf) { cancelAnimationFrame(s7Raf); s7Raf = null; }
    const btn = document.getElementById('step7PlayBtn');
    if (btn) btn.textContent = '▶ Play';
    ['s7ph1','s7ph2'].forEach(id => { const el = document.getElementById(id); if (el) el.setAttribute('opacity','0'); });
  }

  function step7PlayPause() {
    if (s7Playing) { step7Stop(); return; }
    if (!s7Audio1) s7Audio1 = new Audio('/assets/prefix.mp4');
    if (!s7Audio2) s7Audio2 = new Audio('/assets/silvertone.mp4');
    s7Playing = true; s7Phase = 0; s7XfadeStarted = false;
    s7Audio1.currentTime = 0; s7Audio1.volume = 1;
    const btn = document.getElementById('step7PlayBtn');
    if (btn) btn.textContent = '⏸ Pause';

    s7Audio1.onended = () => {
      if (!s7Playing) return;
      s7Phase = 1; s7Audio1.volume = 1; s7Audio2.volume = 1;
      if (s7Audio2.paused) { s7Audio2.currentTime = S7_TRIM; s7Audio2.play().catch(() => {}); }
    };
    s7Audio2.onended = () => { step7Stop(); };
    s7Audio1.play().catch(() => step7Stop());

    function tick() {
      if (!s7Playing) { s7Raf = null; return; }
      const svg = document.getElementById('step7Svg');
      const ph1 = document.getElementById('s7ph1');
      const ph2 = document.getElementById('s7ph2');
      if (svg && ph1 && ph2) {
        const vb = svg.viewBox.baseVal;
        const W = vb && vb.width > 0 ? vb.width : (svg.clientWidth || 240);
        const PAD = 16;
        const silverDur = s7Audio2 && isFinite(s7Audio2.duration) && s7Audio2.duration > 0 ? s7Audio2.duration : 46.5;
        const prefDur   = s7Audio1 && isFinite(s7Audio1.duration) && s7Audio1.duration > 0 ? s7Audio1.duration : 3.4;
        const sc = (W - PAD * 2) / silverDur;
        const prefOffset = S7_TRIM - prefDur;

        if (s7Phase === 0) {
          const ct = s7Audio1.currentTime, dur = s7Audio1.duration;
          if (isFinite(dur) && !s7XfadeStarted && ct >= dur - S7_OVERLAP) {
            s7XfadeStarted = true;
            s7Audio2.volume = 0; s7Audio2.currentTime = S7_TRIM;
            s7Audio2.play().catch(() => {});
          }
          if (s7XfadeStarted && isFinite(dur)) {
            const prog = Math.max(0, Math.min(1, (ct - (dur - S7_OVERLAP)) / S7_OVERLAP));
            s7Audio1.volume = 1 - prog; s7Audio2.volume = prog;
          }
          const x1 = PAD + (prefOffset + ct) * sc;
          ph1.setAttribute('x1', x1); ph1.setAttribute('x2', x1);
          ph1.setAttribute('opacity', s7XfadeStarted ? String(s7Audio1.volume) : '1');
          if (s7XfadeStarted) {
            const x2 = PAD + s7Audio2.currentTime * sc;
            ph2.setAttribute('x1', x2); ph2.setAttribute('x2', x2);
            ph2.setAttribute('opacity', String(s7Audio2.volume));
          } else {
            ph2.setAttribute('opacity', '0');
          }
        } else {
          if (!s7Audio2.paused && s7Audio2.currentTime >= OUT) { s7Audio2.pause(); step7Stop(); }
          const x = PAD + Math.min(s7Audio2.currentTime, OUT) * sc;
          ph1.setAttribute('opacity', '0');
          ph2.setAttribute('x1', x); ph2.setAttribute('x2', x); ph2.setAttribute('opacity', '1');
        }
      }
      s7Raf = requestAnimationFrame(tick);
    }
    s7Raf = requestAnimationFrame(tick);
  }

  function buildStep7Viz() {
    const svg = document.getElementById('step7Svg');
    if (!svg) return;
    const W = svg.parentElement.clientWidth || 240;
    const PAD = 16, RULER_H = 18, WAVE_H = 50, GAP = 10;
    const totalH = (RULER_H + WAVE_H) * 2 + GAP + 18;

    const silverDur  = s7Audio2 && isFinite(s7Audio2.duration) && s7Audio2.duration > 0 ? s7Audio2.duration : 46.5;
    const prefDur    = s7Audio1 && isFinite(s7Audio1.duration) && s7Audio1.duration > 0 ? s7Audio1.duration : 3.4;
    const prefOffset = S7_TRIM - prefDur;
    const waveW = W - PAD * 2;
    const sc = waveW / silverDur;

    const GREEN = '#3aaa68', BROWN = '#9c7b65';
    const o = [];

    function srand(seed) {
      let s = seed >>> 0;
      return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 0xffffffff; };
    }

    function drawRuler(y, color) {
      const int = silverDur <= 30 ? 10 : 15;
      o.push(`<line x1="${PAD}" y1="${y + RULER_H}" x2="${PAD + waveW}" y2="${y + RULER_H}" stroke="${color}" stroke-width="0.5" opacity="0.25"/>`);
      for (let t = 0; t <= silverDur + 0.001; t += int) {
        const tx = (PAD + (t / silverDur) * waveW).toFixed(1);
        o.push(`<line x1="${tx}" y1="${(y + RULER_H - 4).toFixed(1)}" x2="${tx}" y2="${(y + RULER_H).toFixed(1)}" stroke="${color}" stroke-width="0.8" opacity="0.35"/>`);
        o.push(`<text x="${tx}" y="${(y + RULER_H - 6).toFixed(1)}" text-anchor="middle" font-size="8" font-family=${MF} fill="${color}" opacity="0.5">${t}s</text>`);
      }
    }

    function drawWave(cx, y, cw, seed, color, opac) {
      const rand = srand(seed), N = Math.ceil(cw / 2.4), midY = y + RULER_H + WAVE_H / 2;
      for (let i = 0; i < N; i++) {
        const t = i / N, bx = cx + t * cw;
        const amp = 0.38 * Math.sin(t * Math.PI * 4.1) + 0.22 * Math.sin(t * Math.PI * 11.3 + 0.7)
                  + 0.18 * Math.sin(t * Math.PI * 2.2 + 1.8) + 0.12 * (rand() * 2 - 1);
        const bh = Math.max(2, Math.abs(amp) * WAVE_H * 0.44);
        o.push(`<rect x="${bx.toFixed(1)}" y="${(midY - bh).toFixed(1)}" width="1.8" height="${(bh * 2).toFixed(1)}" fill="${color}" opacity="${opac}" rx=".9"/>`);
      }
    }

    function drawHandles(x1, x2, y, color) {
      [x1, x2].forEach(hx => {
        o.push(`<line x1="${hx.toFixed(1)}" y1="${(y + RULER_H - 6).toFixed(1)}" x2="${hx.toFixed(1)}" y2="${(y + RULER_H + WAVE_H + 6).toFixed(1)}" stroke="${color}" stroke-width="2" stroke-linecap="round"/>`);
      });
    }

    // ── Row 1: Prefix clip (green) — positioned on shared 0→silverDur scale ──
    const pX = PAD + prefOffset * sc, pW = prefDur * sc;
    drawRuler(0, GREEN);
    o.push(`<rect x="${pX.toFixed(1)}" y="${RULER_H}" width="${pW.toFixed(1)}" height="${WAVE_H}" fill="${GREEN}" opacity="0.09" rx="3"/>`);
    drawWave(pX, 0, pW, 42, GREEN, 0.72);
    drawHandles(pX, pX + pW, 0, GREEN);
    { const lx = (pX + pW + 8).toFixed(1), ly = (RULER_H + WAVE_H / 2 + 4).toFixed(1);
      o.push(`<text text-anchor="start" font-size="10" font-weight="600" font-family=${SF} fill="${GREEN}" opacity="0.85"><tspan x="${lx}" y="${ly}">beetei thei</tspan><tspan x="${lx}" dy="13">jo woh</tspan><tspan x="${lx}" dy="13">guzrei zamaanei</tspan></text>`); }
    o.push(`<line id="s7ph1" x1="${pX.toFixed(1)}" y1="${(RULER_H - 4).toFixed(1)}" x2="${pX.toFixed(1)}" y2="${(RULER_H + WAVE_H + 4).toFixed(1)}" stroke="${GREEN}" stroke-width="1.5" opacity="0" stroke-linecap="round"/>`);

    // ── Row 2: Silvertone clip (brown) — full duration on same shared scale ──
    const r2Y = RULER_H + WAVE_H + GAP;
    const trimX  = PAD + S7_TRIM * sc;
    const crossX = trimX - S7_OVERLAP * sc;
    const outX   = PAD + OUT * sc;
    drawRuler(r2Y, BROWN);
    o.push(`<rect x="${PAD}" y="${r2Y + RULER_H}" width="${waveW}" height="${WAVE_H}" fill="${BROWN}" opacity="0.07" rx="3"/>`);
    o.push(`<rect x="${crossX.toFixed(1)}" y="${(r2Y + RULER_H - 3).toFixed(1)}" width="${(outX - crossX).toFixed(1)}" height="${WAVE_H + 6}" fill="${BROWN}" opacity="0.13" rx="2"/>`);
    drawWave(PAD, r2Y, waveW, 73, BROWN, 0.6);
    o.push(`<rect x="${PAD}" y="${(r2Y + RULER_H).toFixed(1)}" width="${(S7_TRIM * sc).toFixed(1)}" height="${WAVE_H}" fill="white" opacity="0.5"/>`);
    drawHandles(crossX, outX, r2Y, BROWN);
    o.push(`<text x="${((crossX + outX) / 2).toFixed(1)}" y="${(r2Y + RULER_H + WAVE_H + 16).toFixed(1)}" text-anchor="middle" font-size="10" font-weight="600" font-family=${SF} fill="${BROWN}" opacity="0.85">yeh meiri hei kahani...</text>`);
    o.push(`<line id="s7ph2" x1="${crossX.toFixed(1)}" y1="${(r2Y + RULER_H - 4).toFixed(1)}" x2="${crossX.toFixed(1)}" y2="${(r2Y + RULER_H + WAVE_H + 4).toFixed(1)}" stroke="${BROWN}" stroke-width="1.5" opacity="0" stroke-linecap="round"/>`);

    svg.setAttribute('viewBox', `0 0 ${W} ${totalH}`);
    svg.setAttribute('height', totalH);
    svg.innerHTML = o.join('');

    const btn = document.getElementById('step7PlayBtn');
    if (btn) btn.onclick = step7PlayPause;
  }

  // ── Timeline ────────────────────────────────────────────────────────────
  const SEGMENTS = [
    { label:'Spotify & app transition', start:0,    end:IN,    color:P.spotify  },
    { label:'Original source',          start:IN,   end:OUT,   color:P.source   },
    { label:'Rolling credits',          start:OUT,  end:FINAL, color:P.credits  },
  ];

  function r(n) { return Math.round(n * 10) / 10; }

  function renderTimeline() {
    const svg = document.getElementById('tlSvg');
    const W   = svg.parentElement.clientWidth;
    if (W < 60) return;
    const lanes = STEPS.filter(s => !s.pending).flatMap(s => s.lanes);
    const L=0, R=0, T=0, RULER_H=16, BASE_H=40, LANE_H=24, GAP=4, B=0;
    const usableW = W - L - R, sc = usableW / FINAL;
    const xv = t => L + t * sc;
    const laneBlock = lanes.length ? GAP + lanes.length * (LANE_H + GAP) : 0;
    const H = T + RULER_H + 8 + BASE_H + laneBlock + B;
    const o = [], ry = T + RULER_H;

    o.push(`<line x1="${L}" y1="${ry}" x2="${L+usableW}" y2="${ry}" stroke="${P.hair}" stroke-width="1"/>`);
    for (let t = 0; t <= FINAL + .001; t += 10) {
      const tx = xv(Math.min(t, FINAL));
      o.push(`<line x1="${r(tx)}" y1="${ry-5}" x2="${r(tx)}" y2="${ry}" stroke="#c8c4bb" stroke-width="1"/>`);
      o.push(`<text x="${r(tx)}" y="${ry-8}" text-anchor="middle" font-size="9" font-family=${MF} fill="#c0bbb2">${Math.round(t)}s</text>`);
    }

    const by = ry + 8;
    SEGMENTS.forEach(seg => {
      const sx = xv(seg.start), sw = xv(seg.end) - xv(seg.start);
      o.push(`<rect x="${r(sx)}" y="${by}" width="${r(sw)}" height="${BASE_H}" fill="${seg.color}" rx="3"/>`);
      if (sw > 80) {
        o.push(`<text x="${r(sx+sw/2)}" y="${by+BASE_H/2-3}"  text-anchor="middle" font-size="9.5" font-weight="600" font-family=${SF} fill="white" letter-spacing=".01em">${seg.label}</text>`);
        o.push(`<text x="${r(sx+sw/2)}" y="${by+BASE_H/2+9}" text-anchor="middle" font-size="8"  font-family=${MF} fill="rgba(255,255,255,.55)" letter-spacing=".1em">${seg.start}s — ${seg.end}s</text>`);
      }
    });

    lanes.forEach((lane, i) => {
      const ly = by + BASE_H + GAP + i * (LANE_H + GAP);
      const lx = xv(lane.start), lw = xv(lane.end) - xv(lane.start);
      o.push(`<rect x="${L}" y="${ly}" width="${usableW}" height="${LANE_H}" fill="${P.track}" fill-opacity=".5" rx="3"/>`);
      o.push(`<rect x="${r(lx)}" y="${ly}" width="${r(lw)}" height="${LANE_H}" fill="${lane.color}" rx="3"/>`);
      if (lw > 50) o.push(`<text x="${r(lx+lw/2)}" y="${ly+LANE_H/2+4}" text-anchor="middle" font-size="9.5" font-family=${MF} fill="rgba(255,255,255,.9)" letter-spacing=".05em">${lane.label}</text>`);
    });

    svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
    svg.setAttribute('height', H);
    svg.innerHTML = o.join('\n');
  }

  // ── Steps ───────────────────────────────────────────────────────────────
  function buildSteps() {
    const list = document.getElementById('wtSteps');
    STEPS.forEach((step, i) => {
      const li = document.createElement('li');
      li.className = 'wt-step' + (step.pending ? ' is-pending' : '');
      li.dataset.stepId = step.id;
      li.innerHTML = `<div class="wt-gutter"><div class="wt-dot">${step.id}</div><div class="wt-line"></div></div><div class="wt-body"><p class="wt-step-label">${step.label}</p><h3 class="wt-step-title">${step.title}</h3><p class="wt-desc">${step.desc}</p></div>`;
      list.appendChild(li);
    });
  }

  buildSteps();
  buildSlider();
  renderTimeline();
  window.addEventListener('resize', () => { renderTimeline(); if (step7Panel.classList.contains('is-visible')) buildStep7Viz(); if (step8bPanel.classList.contains('is-visible')) buildStep8bViz(); if (step8Panel.classList.contains('is-visible')) buildStep8Viz(); });

  // ── Step hooks ───────────────────────────────────────────────────────────
  // Add enter/exit callbacks keyed by step id.
  const frame      = document.getElementById('phoneFrame');
  const cutout     = document.getElementById('phoneCutout');
  const diPill     = document.getElementById('diPill');
  const sliderWrap = document.getElementById('sliderWrap');
  const prefixPanel   = document.getElementById('prefixPanel');
  const prefixVid     = document.getElementById('prefixVid');
  const prefixReplay  = document.getElementById('prefixReplay');
  const afterFrameVid = document.getElementById('afterFrameVid');
  const afterOverlay    = document.getElementById('afterOverlay');
  const step6Panel      = document.getElementById('step6Panel');
  const step6PrefixVid  = document.getElementById('step6PrefixVid');
  const step6AfterFrame = document.getElementById('step6AfterFrame');
  const step6AfterVid   = document.getElementById('step6AfterVid');
  const step6Overlay    = document.getElementById('step6Overlay');
  const step6ReplayBtn  = document.getElementById('step6ReplayBtn');
  const step7Panel      = document.getElementById('step7Panel');
  const step8bPanel     = document.getElementById('step8bPanel');
  const step8Panel      = document.getElementById('step8Panel');
  const sliderHint    = document.querySelector('.slider-hint');

  afterFrameVid.addEventListener('loadedmetadata', () => { afterFrameVid.currentTime = 0; });
  step6AfterVid.addEventListener('loadedmetadata', () => { step6AfterVid.currentTime = 0; });

  prefixVid.addEventListener('play',  () => { prefixReplay.classList.remove('is-visible'); });
  prefixVid.addEventListener('ended', () => { prefixReplay.classList.add('is-visible'); });
  prefixReplay.addEventListener('click', () => {
    prefixVid.currentTime = 0;
    prefixVid.play().catch(() => { prefixReplay.classList.add('is-visible'); });
  });

  step6PrefixVid.addEventListener('play',  () => { step6ReplayBtn.classList.remove('is-visible'); });
  step6PrefixVid.addEventListener('ended', () => {
    step6Overlay.classList.add('is-gone');
    step6ReplayBtn.classList.add('is-visible');
  });
  step6ReplayBtn.addEventListener('click', () => {
    step6Overlay.classList.remove('is-gone');
    step6AfterVid.currentTime = 0;
    step6PrefixVid.currentTime = 0;
    step6PrefixVid.play().catch(() => { step6ReplayBtn.classList.add('is-visible'); });
  });

  // persistent: true  → effect stays from this step onward (cumulative)
  // persistent: false → effect only shows at exactly this step
  const STEP_HOOKS = {
    2: {
      enter: () => sliderWrap.classList.add('show-crop'),
      exit:  () => sliderWrap.classList.remove('show-crop'),
      persistent: false,
    },
    3: {
      enter: () => frame.classList.add('is-visible'),
      exit:  () => frame.classList.remove('is-visible'),
      persistent: true,
    },
    4: {
      enter: () => diPill.classList.add('is-visible'),
      exit:  () => diPill.classList.remove('is-visible'),
      persistent: true,
    },
    5: {
      enter: () => {
        prefixPanel.classList.add('is-visible');
        afterFrameVid.currentTime = 0;
        afterOverlay.classList.add('is-gone');
        prefixVid.currentTime = 0;
        prefixVid.play().catch(() => { prefixReplay.classList.add('is-visible'); });
      },
      exit: () => {
        prefixPanel.classList.remove('is-visible');
        afterOverlay.classList.remove('is-gone');
        prefixVid.pause();
        prefixVid.currentTime = 0;
      },
      persistent: false,
    },
    6: {
      enter: () => {
        step6Panel.classList.add('is-visible');
        step6Overlay.classList.remove('is-gone');
        step6AfterVid.currentTime = 0;
        step6PrefixVid.currentTime = 0;
        step6PrefixVid.play().catch(() => { step6ReplayBtn.classList.add('is-visible'); });
      },
      exit: () => {
        step6Panel.classList.remove('is-visible');
        step6Overlay.classList.remove('is-gone');
        step6ReplayBtn.classList.remove('is-visible');
        step6PrefixVid.pause();
        step6PrefixVid.currentTime = 0;
      },
      persistent: false,
    },
    7: {
      enter: () => {
        step7Panel.classList.add('is-visible');
        const redraw = () => { if (step7Panel.classList.contains('is-visible')) buildStep7Viz(); };
        if (!s7Audio1) { s7Audio1 = new Audio('/assets/prefix.mp4'); s7Audio1.addEventListener('loadedmetadata', redraw, { once: true }); }
        if (!s7Audio2) { s7Audio2 = new Audio('/assets/silvertone.mp4'); s7Audio2.addEventListener('loadedmetadata', redraw, { once: true }); }
        buildStep7Viz();
      },
      exit:  () => { step7Panel.classList.remove('is-visible'); step7Stop(); },
      persistent: false,
    },
    8: {
      enter: () => {
        step8bPanel.classList.add('is-visible');
        const redraw = () => { if (step8bPanel.classList.contains('is-visible')) buildStep8bViz(); };
        if (!s8bVocals) { s8bVocals = new Audio('/assets/stems-vocals.mp3'); s8bVocals.addEventListener('loadedmetadata', redraw, { once: true }); }
        if (!s8bMusic)  { s8bMusic  = new Audio('/assets/stems-music.mp3');  s8bMusic.addEventListener('loadedmetadata',  redraw, { once: true }); }
        if (!s8bOrig)   { s8bOrig   = new Audio('/assets/silvertone.mp3');   s8bOrig.addEventListener('loadedmetadata',   redraw, { once: true }); }
        buildStep8bViz();
      },
      exit: () => { step8bPanel.classList.remove('is-visible'); step8bStop(); },
      persistent: false,
    },
    9: {
      enter: () => {
        step8Panel.classList.add('is-visible');
        const redraw = () => { if (step8Panel.classList.contains('is-visible')) buildStep8Viz(); };
        if (!s8Before) { s8Before = new Audio('/assets/silvertone-trimmed.mp3'); s8Before.addEventListener('loadedmetadata', redraw, { once: true }); }
        if (!s8After)  { s8After  = new Audio('/assets/silvertone-restored.mp3'); s8After.addEventListener('loadedmetadata', redraw, { once: true }); }
        buildStep8Viz();
      },
      exit: () => { step8Panel.classList.remove('is-visible'); step8Stop(); },
      persistent: false,
    },
    10: {
      enter: () => {
        const bVid = document.getElementById('sliderBelowVid');
        const aVid = document.getElementById('sliderAboveVid');
        if (bVid) { bVid.src = '/assets/subtitles-before.mp4'; bVid.play().catch(() => {}); }
        if (aVid) { aVid.src = '/assets/subtitles-after.mp4';  aVid.play().catch(() => {}); }
        frame.classList.remove('is-visible');
        diPill.classList.remove('is-visible');
      },
      exit: () => {
        const bVid = document.getElementById('sliderBelowVid');
        const aVid = document.getElementById('sliderAboveVid');
        if (bVid) { bVid.src = VIDEOS.silvertone; bVid.play().catch(() => {}); }
        if (aVid) { aVid.src = VIDEOS.original;   aVid.play().catch(() => {}); }
        frame.classList.add('is-visible');
        diPill.classList.add('is-visible');
      },
      persistent: false,
    },
    11: {
      enter: () => {
        const overlay = document.getElementById('step11Overlay');
        const vid     = document.getElementById('step11Vid');
        const replay  = document.getElementById('step11Replay');
        if (overlay) overlay.style.display = 'block';
        if (replay)  replay.classList.remove('is-visible');
        frame.classList.remove('is-visible');
        diPill.classList.remove('is-visible');
        const s11Play = () => {
          replay && replay.classList.remove('is-visible');
          vid.currentTime = 28;
          vid.play().catch(() => replay && replay.classList.add('is-visible'));
        };
        if (vid) {
          vid.loop = false; vid.muted = false;
          vid.onended = () => replay && replay.classList.add('is-visible');
          s11Play();
        }
        if (replay) replay.onclick = s11Play;
      },
      exit: () => {
        const overlay = document.getElementById('step11Overlay');
        const vid     = document.getElementById('step11Vid');
        const replay  = document.getElementById('step11Replay');
        if (overlay) overlay.style.display = 'none';
        if (replay)  replay.classList.remove('is-visible');
        if (vid) { vid.pause(); vid.onended = null; vid.muted = true; vid.currentTime = 28; }
        frame.classList.add('is-visible');
        diPill.classList.add('is-visible');
      },
      persistent: false,
    },
    12: {
      enter: () => {
        const bVid  = document.getElementById('sliderBelowVid');
        const aVid  = document.getElementById('sliderAboveVid');
        const replay = document.getElementById('step12Replay');
        const s12Play = () => {
          replay && replay.classList.remove('is-visible');
          if (bVid) { bVid.currentTime = 0; bVid.play().catch(() => replay && replay.classList.add('is-visible')); }
          if (aVid) { aVid.currentTime = 0; aVid.play().catch(() => {}); }
        };
        if (bVid) { bVid.src = '/assets/hook-before.mp4'; bVid.onended = () => replay && replay.classList.add('is-visible'); }
        if (aVid) { aVid.src = '/assets/hook-after.mp4';  aVid.onended = null; }
        if (replay) replay.onclick = s12Play;
        frame.classList.remove('is-visible');
        diPill.classList.remove('is-visible');
        s12Play();
      },
      exit: () => {
        const bVid  = document.getElementById('sliderBelowVid');
        const aVid  = document.getElementById('sliderAboveVid');
        const replay = document.getElementById('step12Replay');
        if (replay) replay.classList.remove('is-visible');
        if (bVid) { bVid.onended = null; bVid.src = VIDEOS.silvertone; bVid.play().catch(() => {}); }
        if (aVid) { aVid.src = VIDEOS.original; aVid.play().catch(() => {}); }
        frame.classList.add('is-visible');
        diPill.classList.add('is-visible');
      },
      persistent: false,
    },
  };

  const hookedIds = Object.keys(STEP_HOOKS).map(Number).sort((a, b) => a - b);

  const stepEls = Array.from(document.querySelectorAll('[data-step-id]'));
  const stepChange = Object.fromEntries(STEPS.filter(s => s.change).map(s => [s.id, s.change]));
  const eyebrow = document.getElementById('sliderEyebrow');
  let activeEl = null;

  function updateActiveStep() {
    const viewH = window.innerHeight;
    const center = viewH / 2;
    let bestEl = null, bestDist = Infinity;

    stepEls.forEach(el => {
      const rect = el.getBoundingClientRect();
      if (rect.top < 0 || rect.bottom > viewH) return;
      const dist = Math.abs((rect.top + rect.bottom) / 2 - center);
      if (dist < bestDist) { bestDist = dist; bestEl = el; }
    });

    if (bestEl === activeEl) return;

    if (activeEl) activeEl.classList.remove('is-active');
    activeEl = bestEl;
    if (activeEl) {
      activeEl.classList.add('is-active');
      const change = stepChange[activeEl.dataset.stepId];
      if (eyebrow) eyebrow.textContent = change ? `${change} · Before / After` : 'Before / After';
    }

    // Apply hooks: persistent ones accumulate from their step onward;
    // non-persistent ones fire only at exactly their step.
    const activeId = activeEl ? Number(activeEl.dataset.stepId) : -Infinity;
    hookedIds.forEach(id => {
      const hooks = STEP_HOOKS[id];
      const on = hooks.persistent ? id <= activeId : id === activeId;
      if (on) hooks.enter?.();
      else    hooks.exit?.();
    });

    const anyPanel = prefixPanel.classList.contains('is-visible') || step6Panel.classList.contains('is-visible') || step7Panel.classList.contains('is-visible') || step8bPanel.classList.contains('is-visible') || step8Panel.classList.contains('is-visible');
    sliderWrap.style.display = anyPanel ? 'none' : '';
    sliderHint.classList.toggle('is-hidden', anyPanel);
  }

  let ticking = false;
  window.addEventListener('scroll', () => {
    if (!ticking) { requestAnimationFrame(() => { updateActiveStep(); ticking = false; }); ticking = true; }
  }, { passive: true });
  updateActiveStep();
})();
</script>

<p>I don’t know what to say after. I don’t have the energy to complete this article, so it’s time to publish and move on. Maybe later I will come back and tie together the pieces left untied.</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[“video-editing” is a very modern phenomena. “film” has existed for a long time, but it was by definition a series, any series, of pictures that conveyed a story. captured through camera, drawn through animation or whatever.]]></summary></entry><entry><title type="html">What to work on, and balancing spontaneity vs structure — jotbook page 7</title><link href="https://obaid.wtf/jotbook/2026/04/17/jotbook-page-7.html" rel="alternate" type="text/html" title="What to work on, and balancing spontaneity vs structure — jotbook page 7" /><published>2026-04-17T00:00:00+05:00</published><updated>2026-04-17T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/04/17/jotbook-page-7</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/04/17/jotbook-page-7.html"><![CDATA[<p><strong>1. This might sound</strong> truly self-presumptuous, especially coming from someone who has not proven himself the way I think I need to, to say these things: But I see so many people around me with incredible talent working on incredibly difficult tasks, but I see their work leading to a black hole in an AI-first future. My intuition on this has been built from extensive observation and careful self-scoring since the day GPT-3 launched.</p>

<p>I consider this intuition of what-deserves-your-effort-and-time and will survive or thrive in an AI age, both in the short-term, medium, and long my biggest edge.</p>

<p>Public self-flattery has a way of sometimes truly biting your loud mouth in the ass. Let’s hope this is not of those cases.</p>

<p><strong>2. Since I started</strong> working on things that made me money and honed my skills from the age of 17, I’ve never set yearly goals. I was always in a rush to solve the next week or month of problems, some because of the cards life played me, some self-created because they allowed me to hold a sense of urgency and importance.</p>

<p>The second half of last year and this was the first I intentionally stopped creating “urgent problems” and for the first time, actually, life matched that symphony and gave me greater peace.</p>

<p>So this year, it kind of made sense to have something: a structure, and some new years’ goals. But what I realized was, a lot of my life I couldn’t or didn’t want to fit into strict time boxes.</p>

<p>Two of my priorities in life are pursuing spontaneity (because many of my biggest wins and best ideas come from this) + deeper friendships, and caring for + prioritizing those connections.</p>

<p>These values are non-negotiable, and often at a perpendicular to strict time boxes and mapping my schedule out months in advance.</p>

<p>So I did something else instead: I mapped out loose goals and habits: gym 20x in a month, learn 3 songs on the piano every 3 months, stuff like that. It was my discretion to be carefully consistent, load it all up top, or rush and get it done last minute. And deciding to stay out at a party until 4am means I always know the tradeoffs. I like it this way, and 3 and a half months into 2026, I know it’s what works for me.</p>

<p><strong>3. One thing I’ve</strong> learnt from Huberman’s Lab that has helped me immensely over the last 2 months was… sitting in sunlight, looking directly or only tangentially apart into the sun. It’s been a life changer in solving fatigue and just the general feeling of feeling-drained, and I didn’t realize how painfully oblivious so many of us are to how necessary this is for us, our brains, and health.</p>

<p>Every day I’ve done that, my day has been better than baseline, and everyday I’ve woken up to do it earlier in the morning, even better so. But the way my internal conflict of priorities has been colliding has made that waking up early a rarity rather than the norm.</p>

<p>The other habits I started logging at the start of the year, I don’t anymore because I consider them learnt. They’re ingrained into me and my mind has learned the benefits of being regular with them and I no longer have to force myself to remember or practice them.</p>

<p>So I’m adding a new one. Waking up at 7:30am, 3x a week for now. Full 8 hours of sleep is now de-prioritized relative to waking up early, but I think 3 times is generous enough that it won’t need to come to that. Generous enough to fit Murtaza Qilbash this weekend and various other plans the next.</p>

<p>My productivity right now very much varies on a scale of 100x-0 depending on days, but I think just a guarantee of 3x productive days a week means I can achieve more than most dream of in their lifetime.</p>

<p>Of course, I know that isn’t my competition or bar though, it will be higher — but this is the first step. I remember the same thing Ben Affleck told Matt Damon in Good Will Hunting: “I’ll fucking kill you.”</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1. This might sound truly self-presumptuous, especially coming from someone who has not proven himself the way I think I need to, to say these things: But I see so many people around me with incredible talent working on incredibly difficult tasks, but I see their work leading to a black hole in an AI-first future. My intuition on this has been built from extensive observation and careful self-scoring since the day GPT-3 launched.]]></summary></entry><entry><title type="html">Aesthetic, art, and what the “overton window” means — jotbook page 6</title><link href="https://obaid.wtf/jotbook/2026/03/05/jotbook-page-6.html" rel="alternate" type="text/html" title="Aesthetic, art, and what the “overton window” means — jotbook page 6" /><published>2026-03-05T00:00:00+05:00</published><updated>2026-03-05T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/03/05/jotbook-page-6</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/03/05/jotbook-page-6.html"><![CDATA[<p><strong>1. I feel like</strong> we’re at the edge of the cliff, entering into a artistic renaissance. Already the world is seeing so many hand-drawn and “imperfect” aesthetics popping up as all other kinds of traditional ones seem to have been mastered and the market flooded.</p>

<p>Though this aesthetic centers around rejecting this new-found mastery of craft by a non-human, there will be another enabled by the now-lifted ceiling of limitation. The one that sticks will be one that cannot be one-shotted, or have enough variance in minor prompt variation that makes it a unique expression.</p>

<p><strong>2. The power you</strong> wield with AI can even be utilized for original creations, but that power is gradually diminished as you approach originality. All original creations have elements of the been-done, some more, some less, and it is that % of the been-done that determines how capable AI will be at helping you.</p>

<p><strong>3. There is immense</strong> mastery in the craft of communication. Not just the conventional way we view it through speech but through all our senses and branched into all the ways those senses are interpreted by our intelligence: through music that evokes emotion, switches of association with culture we trip in people’s minds, and the mirror neurons fired when we see a performance.</p>

<p>I have a very surface-level understanding of what each of those are, but that is what I am a student of when I seek to understand the “overton window” and culture. It is delusional perhaps, even in a way that I consider it to be so, to expect myself to master each of those crafts when many incredible humans made it the purpose of their entire lives to master just one.
But I think there’s a certain magic to be found in the interspersion of those, or optimistically, a common symphony I might find that lets me understand, express and judge those crafts comparable to those with a much-higher mastery of the individuals.</p>

<p>[final note: various versions of this jotbook have been lying in my drafts for a while, this is but a short clip from those as I’ve rushed to post in an effort to prevent myself from further procrastination-perfection. god hopes they come out soon.]</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1. I feel like we’re at the edge of the cliff, entering into a artistic renaissance. Already the world is seeing so many hand-drawn and “imperfect” aesthetics popping up as all other kinds of traditional ones seem to have been mastered and the market flooded.]]></summary></entry><entry><title type="html">The Arts Council of Pakistan has a database of 20k+ attendees and full write access completely exposed. Right now.</title><link href="https://obaid.wtf/jotbook/2026/02/22/arts-council-database-20k-attendees-exposed.html" rel="alternate" type="text/html" title="The Arts Council of Pakistan has a database of 20k+ attendees and full write access completely exposed. Right now." /><published>2026-02-22T00:00:00+05:00</published><updated>2026-02-22T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/02/22/arts-council-database-20k-attendees-exposed</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/02/22/arts-council-database-20k-attendees-exposed.html"><![CDATA[<p><strong>Update:</strong> <i><mark>Around 2pm on February 23, the old API keys were discovered revoked and the database locked down. Some time on February 24, the database was opened up again with Row-Level Security (RLS) enabled</mark>, although some vulnerabilities were as I found still present: ones that could make it very easy to crash or upset their systems, but not any that expose their 20,000+ attendees; so I’m ending this story here. On to new projects.</i></p>

<hr style="border:none;border-top:3px dotted #c2255c;margin-top:4px;" />

<p><em>And as I click publish on this post, the database is still, publicly exposed and has <u>not been patched</u>.</em></p>

<p>At exactly 4:32pm, 16.02.2026, I discovered something I think was absolutely insane.</p>

<p>Most anyone in the city’s heard of them: The Arts Council of Pakistan Karachi has a school that now offers full 4-year diploma courses and also happens to be one of the biggest organizers of events in Karachi today.</p>

<p>They also have a website to list their events and that website, as I discovered, is powered by a Supabase database with disabled security controls, and an API Key being used publicly from the web app. In. Raw. Text.</p>

<p>Not only that, but this events database is being used for their entire offline ticketing and attendee management, exposing 20,000+ people’s personal information: names, emails, phone numbers, order QRs, payment amounts, and much, much more.</p>

<p>If I were to draw an analogy for non-technical people, this <u>data breach</u> is not me finding a crevice in the wall I could use to slip a hook in and open the window. This is leaving the door to your most valuable safe wide-open, and then leaving a trail of breadcrumbs and carefully placed cardboard signs to it screaming “I’m exposed and vulnerable.”.</p>

<blockquote>
  <p>This is leaving the door to your most valuable safe wide-open, and then leaving a trail of breadcrumbs and carefully placed cardboard signs to it screaming “I’m exposed and vulnerable.”.</p>
</blockquote>

<p>As a thought experiment, let’s examine a few possibilities of what could have happened (or perhaps has been secretly happening) because of this leak:</p>

<p><strong>1. Someone could sell</strong> this personal data dump on some online anonymous forums, for idk, some bitcoin.
<strong>2. Scammers could pose</strong> as the Arts Council, building trust as they knew people’s exact details, exactly where they were on given dates down to how much they paid for each event.
<strong>3. Use it to</strong> issue themselves free tickets for events, without any pre-screening or payment: Perhaps even set up a third-party market for Arts Council events selling tickets for 100Rs each.</p>

<p>And what’s worse, this vulnerability couldn’t possibly be just an oversight.</p>

<p>Before you are <em>allowed</em> to disable Supabase’s default security settings, you must confirm repeatedly you are aware of the dangers and consequences of doing so, and not only that, but while it is disabled you are repeatedly sent notifications, emails, and reminders telling you to re-enable it.</p>

<p>Some irresponsible and reckless developer, somewhere, chose to intentionally ignore all that. The API key was also not exposed recently: I’ve found traces of it in web backups going all the way back to September 2025.</p>

<p>Many of own friends, regular attendees of Arts Council events’ who trusted the administration with their names, numbers, and contact information, and personal details have their data publicly exposed within, so this is personal to me.</p>

<p>Since I discovered this vulnerability, I have been worried sick about how to reach out to the Arts Council <em>without</em> alerting any bad actors, and worried whether I could even communicate to them the sensitivity of what they have wrought, because let’s be frank, someone who exposed their entire system and the personal information of 20,000+ people has to be an icon of irresponsibility and callousness.</p>

<p>Right now, the website and database are still exposed as they were. But I’m making this announcement publicly and alerting the Arts Council because before doing so because I’ve replaced data of the 20,000+ attendees with a non-sensitive, synthetic dataset that both protects the original attendees and doesn’t break the Arts Council’s systems.</p>

<p>Beyond my need to protect these innocent people (including my friends) who have been put at risk due to no fault of their own, I do not have the tools to do anything else.</p>

<p>I’ve also sent an email to the Arts Council at <code class="language-plaintext highlighter-rouge">info@acpkhi.com</code>, with a link to this article and informing them of this bug. I hope they fix this quickly before their system crashes, and I wish them the best of luck if on top of this recklessness, they have made no backups and had their financial accounting also, solely dependent on this database.</p>

<p>But I see an educational moment in this. The next part of this post is targeted specifically at developers, engineers, data scientists, and people interested in binary sciences… for whom I will use this breach as a practical example of what <strong>NOT</strong> to do, demonstrate the dangers of ignoring security, and break down all the attack vectors I can think of leveraging against an exposed database like this… and how to in the future, protect against them.</p>

<hr />

<h3 id="the-vulnerability">The vulnerability</h3>

<p>was that the Row-Level-Security of the database was purposefully, intentionally, disabled. Supabase warns you repeatedly of the consequences before letting you do that, and those warnings were ignored. Its “security adviser” sends you notifications and follow-up emails regularly if RLS is left disabled for too long, and those warnings too, presumably, were ignored.</p>

<p>This was not a temporary security relaxation for whatever reason, I traced its history through the Wayback Machine, and although the site isn’t archived frequently, I was able to use the wildcard URL feature to discover <em>some</em> JS bundles have been saved  periodically. And what did I discover? The first time this exact API Key (not even refreshed!) was seen in the bundle on September 2025: almost 5 months now.</p>

<p><img src="/assets/wayback.webp" style="width:100%;border-radius:11px;margin:1rem 0;" /></p>

<h3 id="producing-the-synthetic-dataset">Producing the synthetic dataset</h3>

<p>I needed to protect the attendee’s (including my friends!) data before announcing, but also make sure I didn’t break the site. So after being inspired by some discussions with a friend, I had an interesting idea: replacing the original attendees with a synthetic dataset.</p>

<p>Now, any random dataset with the same schema would do, but I wanted to do a little experiment: How close could LLMs and matching statistical distribution get the synthetic to the original dataset?</p>

<p>And you can judge, because this is what our produced final-dataset looks like:</p>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@31.3.2/styles/ag-grid.css" />

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ag-grid-community@31.3.2/styles/ag-theme-alpine.css" />

<style>
  .walkin-grid-wrap {
    margin-bottom: 1em;
    border-radius: 2px;
    border: 1px solid #ddd8ce;
    overflow: hidden;
  }
  #walkin-grid {
    height: 480px;
    width: 100%;

    --ag-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --ag-font-size: 12px;
    --ag-cell-horizontal-padding: 8px;

    --ag-background-color: #ffffff;
    --ag-foreground-color: #1c1c1c;
    --ag-secondary-foreground-color: #555;

    --ag-header-background-color: #1c1c1c;
    --ag-header-foreground-color: #f4f0e8;
    --ag-header-column-separator-color: #383838;
    --ag-header-column-separator-display: block;
    --ag-header-column-separator-height: 60%;

    --ag-odd-row-background-color: #f5f5f5;
    --ag-row-hover-color: #ebebeb;
    --ag-selected-row-background-color: rgba(194,37,92,0.1);

    --ag-border-color: transparent;
    --ag-pinned-column-border-color: #aaa;

    --ag-row-border-color: #eeeeee;
    --ag-cell-widget-spacing: 4px;
  }
</style>

<div class="walkin-grid-wrap">
  <div id="walkin-grid" class="ag-theme-alpine"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/ag-grid-community@31.3.2/dist/ag-grid-community.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>

<script>
(function () {
  const pkr = v => v != null && v !== '' ? `₨${Number(v).toLocaleString()}` : '';
  const cap = v => v ? v[0].toUpperCase() + v.slice(1) : '';

  const columnDefs = [
    { field: 'order_number',        headerName: 'Order #',      width: 105, pinned: 'left' },
    { field: 'customer_name',       headerName: 'Customer',     width: 150 },
    { field: 'customer_mobile',     headerName: 'Mobile',       width: 125 },
    { field: 'customer_email',      headerName: 'Email',        width: 210 },
    { field: 'ticket_type',         headerName: 'Ticket Type',  width: 115 },
    { field: 'buyer_category',      headerName: 'Category',     width: 100, valueFormatter: p => cap(p.value) },
    { field: 'quantity',            headerName: 'Qty',          width: 65,  type: 'numericColumn' },
    { field: 'unit_price',          headerName: 'Price',        width: 90,  type: 'numericColumn', valueFormatter: p => pkr(p.value) },
    { field: 'discount_per_ticket', headerName: 'Discount',     width: 90,  type: 'numericColumn', valueFormatter: p => pkr(p.value) },
    { field: 'total_amount',        headerName: 'Total',        width: 90,  type: 'numericColumn', valueFormatter: p => pkr(p.value) },
    { field: 'holder_names',        headerName: 'Holder(s)',    width: 150 },
    { field: 'membership_no',       headerName: 'Membership #', width: 125 },
    { field: 'student_id',          headerName: 'Student ID',   width: 100 },
    { field: 'printed',             headerName: 'Printed',      width: 80,  valueFormatter: p => p.value === 'TRUE' || p.value === true ? 'Yes' : 'No' },
    { field: 'refund',              headerName: 'Refund',       width: 85 },
    { field: 'remarks',             headerName: 'Remarks',      width: 150 },
    { field: 'created_at',          headerName: 'Created At',   width: 160, valueFormatter: p => p.value ? new Date(p.value).toLocaleString() : '' },
  ];

  const grid = agGrid.createGrid(document.getElementById('walkin-grid'), {
    columnDefs,
    defaultColDef: { resizable: true, sortable: true },
    rowHeight: 24,
    headerHeight: 26,
    rowModelType: 'clientSide',
    rowData: [],
    overlayLoadingTemplate: '<span>Loading synthetic dataset...</span>',
  });

  grid.showLoadingOverlay();

  Papa.parse('/assets/walkin_ticket_orders_synthetic.csv', {
    download: true,
    header: true,
    skipEmptyLines: true,
    complete({ data }) {
      grid.setGridOption('rowData', data);
    },
  });
})();
</script>

<p><em>Since RLS was re-enabled, the synthetic dataset now loads from a CSV file instead. Download the full dataset (with QR codes) <a href="/assets/walkin_ticket_orders_synthetic_ALL_COLUMNS.csv">here</a>.</em></p>

<p><span style="color:gray"><del><em>btw, the sheet above is loading <strong>directly from</strong> the Arts Council’s Supabase database, now filled with our synthetic attendees. You can see exactly <strong>HOW</strong> bad the situation is</em></del>.</span></p>

<p>So how did I go about producing it? I analyzed, with help from Claude ofc, the statistical distribution and patterns in the original dataset and discovered some hyper-specific and extremely niche patterns, for example:</p>

<ul>
  <li>
    <p>Order numbers are almost always sequential, except for 18 gaps in the middle, so I simulated the original dataset by producing randomly, between 9 and 27 total gaps (+- 50% displacement).</p>

    <p>Each gap was of random size, between 1-5 numbers long, and then there was always ONE gap which was between 30 and 70. The +-50% random displacement from the original dataset’s distributions was a general rule across patterns.</p>
  </li>
  <li>Membership numbers were
    <ul>
      <li><code class="language-plaintext highlighter-rouge">MN-NNNN</code>, 63% of the time.</li>
      <li><code class="language-plaintext highlighter-rouge">L-NNNN</code>, 35% of the time.</li>
      <li><code class="language-plaintext highlighter-rouge">PM-</code>, <code class="language-plaintext highlighter-rouge">E-</code>, or literal <code class="language-plaintext highlighter-rouge">Member/M/MEMBER</code>, 2% of the time.</li>
    </ul>

    <p>That, was also matched to look like the original dataset :)</p>
  </li>
  <li>
    <p>Attendees usually purchased tickets together in groups (of at least 2), but how big of a group was and what % of the attendees were in groups vs by themselves was heavily dependent on the event, so we calculated the per-event distributions individually and matched that in the synthetic (with +- 50% random displacement again, of course).</p>
  </li>
  <li>
    <p>Mobiles and emails were almost always recorded for attendees except for the free events, in which case were mostly not.</p>

    <p>But this heavy correlation didn’t just extend to events but also to specific ticket types. VIPs for example, almost never had their name, email or anything recorded. Which makes sense. How <em>many</em> of the attendees would have their information recorded was also matched hyper-specifically to the original dataset, <em>per-event-per-ticket-type</em>, then displaced by the same +-50% random displacement.</p>
  </li>
</ul>

<p>All this extremely niche statistical programming was done to produce a dataset near-unrecognizable from the original, but even this wouldn’t be enough to achieve that perfect result.</p>

<h3 id="using-llms-as-a-name-generator">Using LLMs as a name-generator</h3>

<p>The names in our original dataset were Pakistani, riddled with typos, spelling variations, and had characters and numbers in the emails that made them look very uniquely real.</p>

<p>This, was not a job for statistical rules in a Python script. For this, I had to invoke our society’s greatest accomplishment in statistical engineering: SOTA LLMs.</p>

<p>Using Sonnet 4.6, I first generated, at random, triplets of Pakistani names (both two-word and one), emails and mobiles, in the same format, all riddled with similar typos and patterns as the original dataset.</p>

<p>Since I needed to generate thousands of rows and Claude was expensive/slow… I experimented with other APIs: ChatGPT was bad, and so was GLM-5. Amusingly, they both leaned towards famous Pakistani personalites even when I made it explicity clear I didn’t want that.</p>

<p>GLM-5 though, did prove to give survivable results after a few rounds of prompt engineering when I generated the first 100 synthetics from Claude and used those as an example for GLM-5 to copy.</p>

<p>Probabilistically, the script had already calculated which fields need to be filled in vs. left empty. Once this LLM-generated CSV of triplets was ready, all I needed to do was row-by-row, fill them in.</p>

<p><img src="/assets/probabilistic_programming.webp" style="width:100%;border-radius:11px;margin:1rem 0;" /></p>

<p>* As a final note on this topic, people who depend on AI for agentic behaviour, this is sometimes what it lacks. This idea of statistically copying patterns of the database with some randomization… even after I gave Claude the hint I wanted to replace the database and prompted it for ideas, it didn’t suggest.</p>

<p>Only when I explicitly mentioned it did it go “oh, that’s an amazing idea”. LLMs still very much lack in creative imagination.</p>

<h4 id="qr-code-tickets">QR code tickets</h4>

<p>One of the fields was <code class="language-plaintext highlighter-rouge">qr_codes</code>, a base64 png image directly stored as a text field and presumably used at the ticketing counter. Decoding it revealed the pattern <code class="language-plaintext highlighter-rouge">{order_number}-{index_in_a_group_buying_tickets_together}</code>.</p>

<p>After the synthetic data was produced, I simply followed the same pattern to encode it back and produce a QR code ticket for all synthetic attendees.</p>

<h2 id="image-as-an-attack-vector">Image as an attack vector</h2>

<p>While investigating the events table to see what would be vulnerable to an XSS attack, I noticed nearly all fields were text and the app was built with Vue. Because modern PWAs protect very well against such injections by default, I looked for alternatives.</p>

<p>One field caught my eye: the image URL.</p>

<p>Now, I could probably store and fetch excessive amounts of data on load and possibly overload the database, make the arts council’s egress bill skyrocket, or prevent the site from loading for visitors, but that would be immediately obvious.</p>

<p>So, I set up an edge function. With a simple script that would masquerade as an image (with .jpeg extension), capture information about the visitor, and quietly redirect to the real image.</p>

<p>To a visitor, there would be no difference. They’re still seeing the image.</p>

<p>But even for anyone looking at the source: I used the original Supabase project’s id in my edge function so the URL difference was almost imperceptible.</p>

<p><img src="/assets/image_attack.webp" style="width:100%;border-radius:11px;margin:1rem 0;" /></p>

<p><code class="language-plaintext highlighter-rouge">https://dciofyiyfblqtlozqbzw.</code><mark>supabase.co</mark><code class="language-plaintext highlighter-rouge">/storage/v1/object/public/events/event-images/ycvtq5s8kn_1770818564643.jpeg</code></p>

<p>➡ <mark>up.railway.app</mark></p>

<p>Because of just this <em>tiny change</em> to one image URL, I (and anyone else could) have been collecting the exact location (through IP address), time, device (user agent), and other extensive analytics on every single person who’s visited the Arts Council’s website for the past 5 days.</p>

<p>and an actor with bad intentions wouldn’t be interested in just researching this.</p>

<video autoplay="" muted="" loop="" style="width:100%;max-width:800px;margin:1rem 0 0;border-radius:11px;">
  <source src="/assets/visitors_location-compressed.mp4" type="video/mp4" />
</video>

<div style="display:flex;gap:1rem;margin:1rem 0;">
  <video autoplay="" muted="" loop="" style="flex:1;min-width:0;border-radius:11px;">
    <source src="/assets/piechart-compressed.mp4" type="video/mp4" />
  </video>
  <img src="/assets/visits_per_hour.webp" style="flex:1;min-width:0;border-radius:11px;object-fit:cover;" />
</div>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[Update: Around 2pm on February 23, the old API keys were discovered revoked and the database locked down. Some time on February 24, the database was opened up again with Row-Level Security (RLS) enabled, although some vulnerabilities were as I found still present: ones that could make it very easy to crash or upset their systems, but not any that expose their 20,000+ attendees; so I’m ending this story here. On to new projects.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://obaid.wtf/assets/probabilistic_programming.webp" /><media:content medium="image" url="https://obaid.wtf/assets/probabilistic_programming.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Leash your dopamine intake, and how ego == insecurity — jotbook page 5</title><link href="https://obaid.wtf/jotbook/2026/01/06/jotbook-page-5.html" rel="alternate" type="text/html" title="Leash your dopamine intake, and how ego == insecurity — jotbook page 5" /><published>2026-01-06T00:00:00+05:00</published><updated>2026-01-06T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2026/01/06/jotbook-page-5</id><content type="html" xml:base="https://obaid.wtf/jotbook/2026/01/06/jotbook-page-5.html"><![CDATA[<p><strong>1. I think the</strong> most valuable upskill a highly-ambitious person from my generation can make is understanding the role of dopamine, the receptors, and what causes its release in our brains. Training that to align with our goals is the most important challenge of our time.</p>

<ul>
  <li>
    <p>We have all already started to realize just how harmful smartphones and especially infinite-scrolling apps like TikTok, IG Reels, and YT Shorts are, but they have their benefits and are perhaps the only form of social communities which is a human necessity, for the globally connected world that exists today. Going cold turkey is not a solution.</p>
  </li>
  <li>
    <p>I’ve been learning as much as I can for the past 8 months from Huberman, from my new book, and various other research papers and studies Claude summarizes for me. I cannot stress how incredibly life-changing it has been for me to understand those patterns and easily predict how my body and mind will function in the next few days after I feed it junk (attention-invading content).</p>
  </li>
</ul>

<p><strong>2. I have never</strong> seen two apparently distinct attributes found more commonly together than ego and insecurity, and likewise on the flip-side, humility and confidence. Intelligent, talented and resourceful people are often detached from this curse, and are nearly always better people to have around simply because their self of identity comes from their own achievements. They realize that shrinking the pie’s size might give them get them a wider slice, but never actually, a greater chunk of pie.</p>

<p><strong>3. Forgiveness, I’m learning,</strong> is valuable. I always used to believe in the proportional relation between discipline of training to the exceptionality of outcomes, but funnily, Duolingo over the past year has made me think differently. I think, their forgiveness of inconsistent behaviour over time has made me more disciplined with practice, and overall increased the amount I would have learnt otherwise.</p>

<p>This was empirically reinforced when diving down the rabbit hole of simulation studies around the prisoner’s dilemma. Molander (1985) found that noisy, uncertain environments like the world we find ourselves in need forgiveness. We self-sabotage ourselves by punishing each and every mistake without leaving margin for uncertainty.</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1. I think the most valuable upskill a highly-ambitious person from my generation can make is understanding the role of dopamine, the receptors, and what causes its release in our brains. Training that to align with our goals is the most important challenge of our time.]]></summary></entry><entry><title type="html">Spotify has been scraped — jotbook page 4</title><link href="https://obaid.wtf/jotbook/2025/12/30/jotbook-page-4.html" rel="alternate" type="text/html" title="Spotify has been scraped — jotbook page 4" /><published>2025-12-30T00:00:00+05:00</published><updated>2025-12-30T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2025/12/30/jotbook-page-4</id><content type="html" xml:base="https://obaid.wtf/jotbook/2025/12/30/jotbook-page-4.html"><![CDATA[<p><strong>1. We are going</strong> through the great unlearning. App developers make an app today only to realize their moat is nothing but a context-retrieved .md file, and their app as a wrapper on top is nothing but an annoyance that turns people away because they have to use “yet another app”.</p>

<p><strong>2. You might have</strong> seen the news that Anna’s Archive has archived, through scraping, the near-entirety of Spotify’s songs catalogue, equivalent to 99.6% of its (ambiguous, but presumably all-time) listens.</p>

<ul>
  <li>I have followed and been a supporter of Anna’s Archive for a long time, they have a very noble mission to preserve human knowledge, and generally try to provide a catalogue of all books and research papers on the planet, for free. They have without a doubt, the most comprehensive website in the world for this.</li>
</ul>

<p>This is also a significant contribution to where I consider the arc of humanity should bend towards: removing all barriers of entry to knowledge and education, making it accessible to all.</p>

<ul>
  <li>
    <p>The technical difficulty of going completely undetected by Spotify while scraping 300! terabytes from their platform is a herculean task, and the anonymous people who made this happen could easily be earning upwards of a $quarter-million (or maybe even are) at a Silicon Valley big-tech, but they chose to dedicate their time, and effort instead into moving this goalpost for humanity forward. I think their effort should be celebrated more.</p>
  </li>
  <li>
    <p>I don’t think this will cause any noticeable increase in piracy at all. Spotify is one of the few success stories in history that coalesced a HEAVILY pirated market back into the mainstream through their incredible user experience and extremely affordable pricing. Piracy has an inconvenience attached to it, and even in lower-income markets like Pakistan, their regional pricing makes them the best, wallet-friendly choice.</p>
  </li>
</ul>

<p>Whether they are “fair” in the sense of paying more per-stream to the biggest artists compared to the rest is a debate for another time.</p>

<ul>
  <li>
    <p>Splitting hairs (a lot of numbers): Though the archive catalogues 99.9% of all songs’ metadata (name, artist, genre etc.) which is important, the actual music archived is just 33.6% of Spotify’s library… this represents 99.6% of what people listen to because popular songs are listened to a lot more, of course: two-thirds of all songs on Spotify are only listened to by 0.4% of the people.</p>
  </li>
  <li>
    <p>Since AI and platforms like Suno, the number of albums released per year have exploded and a lot might be wholly or partially AI-generated. We might realize many years later how important it was to have a cultural archive of all music pre-AI.</p>
  </li>
</ul>

<p>Number of albums released per year:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2024 ╢████████████████████ (11.07m)
2023 ╢███████████████
2022 ╢████████████
2021 ╢█████████
2020 ╢████████
2019 ╢█████
2018 ╢███
2017 ╢██
2016 ╢██
2015 ╢██
2014 ╢██
</code></pre></div></div>

<ul>
  <li>
    <p>The most popular Pakistani song by no. of all-time listens is Jhol, coming in at 792nd on the list overall. Since Spotify is by far the biggest platform and loosely follows the rankings of others, we can pretty much be certain… Jhol is the 792nd most overall, and most popular Pakistani song in the history of music. Congrats to Maanu and Annural Khalid?!</p>
  </li>
  <li>
    <p>Is Anna’s Archive “Russian-backed”? I don’t know. But I do think even if this is somewhat true, this is very immature fear-mongering.</p>
  </li>
</ul>

<p>The talented engineers, and I suspect the vast majority of funders and incredibly intelligent people who came together and volunteered everything to make this happen, didn’t do it because they personally knew the leadership or funders, they were in it for its mission, and if that mission is achieved it will be an incredible step forward for humanity, whoever is at the helm of it.</p>

<p>And secondly, perhaps its something to think about whether Western jurisdictions are long-overdue an overhaul in terms of how they balance intellectual property vs. accessibility to knowledge and culture and use actual studies of why piracy occurs instead of relying on Cold-War-era “communism” fear-mongering to define their policies.</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1. We are going through the great unlearning. App developers make an app today only to realize their moat is nothing but a context-retrieved .md file, and their app as a wrapper on top is nothing but an annoyance that turns people away because they have to use “yet another app”.]]></summary></entry><entry><title type="html">Benchmarks seem fake. also, Libya has two governments — jotbook page 3</title><link href="https://obaid.wtf/jotbook/2025/12/25/jotbook-page-3.html" rel="alternate" type="text/html" title="Benchmarks seem fake. also, Libya has two governments — jotbook page 3" /><published>2025-12-25T00:00:00+05:00</published><updated>2025-12-25T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2025/12/25/jotbook-page-3</id><content type="html" xml:base="https://obaid.wtf/jotbook/2025/12/25/jotbook-page-3.html"><![CDATA[<p><strong>1. Context for what</strong> I’m about to say: I’ve been using AI-coding models, agents and tools since they have existed, starting with the Github Copilot. I was one of the first waitlist-signups to Cursor after Aman just posted a link on his Twitter, and Claude Code I tested for a bit back when it was really bad. Majority of the last 3 years I’ve spent copy-pasting code from the Claude web app until this year when I deemed Cursor/Claude Code good enough to replace that workflow. Cline was still the best, but very expensive as it had to use Claude’s web API.</p>

<p>My perception of a models’ coding ability and its benchmarks used to be pretty similar but in this last year both have started diverging to the point where I’ve stopped paying attention to them (data leakage, or benchmarks testing for the wrong things).</p>

<p>Opus and Sonnet are absolute elite-tier models for coding, far above competition, and Gemini or any OpenAI model do not come close. Distilled models like GLM and others seem to have weird bugs and inconsistent behaviour. Benchmarks do not convince me otherwise.</p>

<p><strong>2. Libya’s army</strong> chief was mysteriously killed in a plane crash a day after news broke that it had a signed $4b arms deal with Pakistan. Naturally, I was curious.</p>

<p>Turns out Libya is actually controlled by two different factions.</p>

<ul>
  <li>
    <p>One is Tripoli-based, UN-recognized, and backed by Turkey (who even stepped in militarily to defend it in 2019) and Qatar. Is a coalition-based “national government”, controls most of Western Libya.</p>
  </li>
  <li>
    <p>And the anti-islamist Haftar faction, supported mainly by Egypt+UAE and loosely by Russia (used Wagner to support Haftar in 2019 until Turkey intervened, at which point they withdrew) controls the East and South.</p>
  </li>
  <li>
    <p>Tripoli has been subject to infighting over recent years while Haftar relatively stable so even huge backers like Turkey have begun hedging their bets by making some contacts w Haftar.</p>
  </li>
  <li>
    <p>I guess the army chief flew to Turkey seeking their support after seeing Pakistan signing an arms deal with their rival. Plane crashed within Turkey’s airspace on the way back.</p>
  </li>
  <li>
    <p>Even Haftar declared three days of national mourning in honour of the killed army chief, I imagine to dispel any rumour of their involvement.</p>
  </li>
</ul>

<p>Super interesting story, had no idea about Libya’s situation before this rabbit hole.</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1. Context for what I’m about to say: I’ve been using AI-coding models, agents and tools since they have existed, starting with the Github Copilot. I was one of the first waitlist-signups to Cursor after Aman just posted a link on his Twitter, and Claude Code I tested for a bit back when it was really bad. Majority of the last 3 years I’ve spent copy-pasting code from the Claude web app until this year when I deemed Cursor/Claude Code good enough to replace that workflow. Cline was still the best, but very expensive as it had to use Claude’s web API.]]></summary></entry><entry><title type="html">Contempt for linkedin and why forgiveness is game-theory optimal — jotbook page 1</title><link href="https://obaid.wtf/jotbook/2025/12/21/jotbook-page-1.html" rel="alternate" type="text/html" title="Contempt for linkedin and why forgiveness is game-theory optimal — jotbook page 1" /><published>2025-12-21T00:00:00+05:00</published><updated>2025-12-21T00:00:00+05:00</updated><id>https://obaid.wtf/jotbook/2025/12/21/jotbook-page-1</id><content type="html" xml:base="https://obaid.wtf/jotbook/2025/12/21/jotbook-page-1.html"><![CDATA[<p><strong>1) My contempt for</strong> LinkedIn is so visceral, so deep, that honestly, the rational reasons I’ve thought of so far don’t really justify it. And I think I’ve done a reasonable amount of introspection over it.</p>

<p>Maybe it’s the unholy pretentiousness of so many of the posters I see on here that triggers this or the absolute aversion of my nature to the “corporate” culture and identities we’ve collectively built around them, I don’t know. But I do know I will stop myself before I fall down this rabbit hole that might take anything from a few hours to weeks of my life from existential burnout.</p>

<p>Most top researchers and discoveries, I’ve noticed, have quite a bias for “good” and “kind” things. Just look at the Nobel Prizes for the last couple of years in which there weren’t really any earth-shattering discoveries. You’ll find that aplenty. Which is probably a better thing for a less antagonistic world, but intriguing nonetheless. Do they reflect the general distribution of humanity, or is this some by-product of the academia they surround themselves in that pushes them towards that bias?</p>

<p>All of this as a tangent to say, yes, there is a counter-point to the acceptability of the 92’ Nowak &amp; Sigmund paper GTFT paper so I am not accused of being a propagandist for “goodness” at the compromise of truth… but the paper essentially found that forgiveness was a necessary part of all successful strategies when playing a multi-round, noisy like the real world, Prisoners Dilemma.
(I would hate to gate-keep behind jargon, you can research each of these individual terms if you want to read more - you will find them).</p>

<p>I’m going to give LinkedIn one more chance, basically. And starting today, I’m running an experiment: I’m unfollowing everyone, and all new connections will be unfollowed by default. I will build out a new “following” list from scratch based on posts I’m interested in, and will see how the new feed treats me.</p>

<p><strong>2) I will post,</strong> maybe, I think. But I will essentially treat it as a public jotbook, I will write for myself and perhaps often not be that coherent. It will be thoughts about Claude Code vs Cursor (as I have one thought now), or it could be a random shower thought I had about Apple’s long-term lead in desktops because of solid early decisions, despite a now-lack of innovation (one I shared with a friend earlier today). It could be more philosophical too.</p>

<p>The point is, don’t expect much. I’m certain if I feel anyone does I will be more hesitant to post anything until I polish it a million times over. So I’d like to think you don’t expect much, but I do hope it becomes something that makes you start to.</p>]]></content><author><name>obaid</name></author><category term="jotbook" /><summary type="html"><![CDATA[1) My contempt for LinkedIn is so visceral, so deep, that honestly, the rational reasons I’ve thought of so far don’t really justify it. And I think I’ve done a reasonable amount of introspection over it.]]></summary></entry></feed>