daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

commit ddd84af895b5ee1faf7aa2d979c1fab3365d85f2
parent 1ed96a19cee00c4874b7d72ca7b6e1e0c89c7097
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Wed,  7 Oct 2026 13:41:06 +0100

Bring over daemon-sec's dot field, hero card icons, cursor pill and fades

The same effects as daemon-sec.xyz, as framework-free modules in
src/scripts/dmn (shaders and timing unchanged):
- the dot field replaces the connection-node field in the home hero and
  every index banner; the sheet headers keep their fuzz field
- the hero card's random dot grid becomes the card's own dots, which gather
  into the hovered shelf's icon (lucide outlines) in the shelf's accent, with
  the copy kept clear by a gradient mask
- the hero boot plays once and the shelf tour then keeps cycling instead of
  fading out and re-entering; the timecode counts real elapsed time
- a cursor pill: Read over sheets, Browse over shelves and categories, View
  image over sheet images (with a small native-dialog viewer), Open PDF over
  PDF links; none on the masthead and footer
- scroll reveals on daemon-sec's longer, softer curve; index rows now reveal
  (the selector pointed at a class that no longer exists)

Structure: a skip link, and the hero wordmark is the home page's h1.
The field test is updated for sheet headers using the fuzz field (it had been
failing since that change) and a test for the h1 and skip link is added.
Removes leftover TSV/JSON/PNG files at the root, tracked .pyc files (now
ignored), an unused Hero component, old planning specs and a prompt.

Tested: npm test (build + 9/9), and Playwright on the preview: fields live on
home and a category banner, icon forms on hovering a shelf row, pills on hero
rows, shelf cards, article rows and a sheet image, viewer opens and closes,
no pill on the masthead, no page errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Diffstat:
M.gitignore | 4++++
Dcandidates-assets.tsv | 45---------------------------------------------
Dcandidates-summary.tsv | 234-------------------------------------------------------------------------------
Dcontent-manifest-missing.json | 577-------------------------------------------------------------------------------
Ddocs/superpowers/specs/2026-08-09-daemon-sec-cheatsheet-site-design.md | 137-------------------------------------------------------------------------------
Ddocs/superpowers/specs/2026-09-28-ired-team-integration-design.md | 367-------------------------------------------------------------------------------
Ddocs/superpowers/specs/2026-10-04-signal-field-banners-design.md | 305------------------------------------------------------------------------------
Dlive-root.png | 0
Dprompts/daemon-sec-content-expansion.md | 272-------------------------------------------------------------------------------
Dscripts/__pycache__/build-candidates.cpython-314.pyc | 0
Dscripts/__pycache__/port-missing.cpython-314.pyc | 0
Dscripts/__pycache__/port-vault.cpython-314.pyc | 0
Dscripts/__pycache__/sync-payloads.cpython-314.pyc | 0
Dsrc/components/Hero.astro | 253-------------------------------------------------------------------------------
Msrc/components/HeroLoop.astro | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/components/SignalField.astro | 13+++++++++++--
Msrc/layouts/Base.astro | 9+++++++++
Msrc/scripts/app.ts | 4++--
Asrc/scripts/dmn/cursor.ts | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/scripts/dmn/icon-targets.ts | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/scripts/dmn/init.ts | 31+++++++++++++++++++++++++++++++
Asrc/scripts/dmn/particle-field.ts | 1036+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/scripts/dmn/poisson.ts | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/scripts/dmn/shelf-icons.ts | 20++++++++++++++++++++
Asrc/scripts/dmn/viewer.ts | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/scripts/signal-field.ts | 726-------------------------------------------------------------------------------
Msrc/styles/app.css | 1+
Asrc/styles/dmn.css | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/styles/global.css | 13++++++-------
Msrc/styles/tokens.css | 4++++
Mtest/hero-and-shelves.test.mjs | 20++++++++++++++++++--
31 files changed, 1858 insertions(+), 2956 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -17,3 +17,7 @@ candidates.json # wrangler dev scratch state .wrangler/ + +# Python bytecode from the scripts/ helpers +__pycache__/ +*.pyc diff --git a/candidates-assets.tsv b/candidates-assets.tsv @@ -1,44 +0,0 @@ -repo pdf Active-Directory/rbcd-cheatsheet.pdf -repo pdf Cryptography/GPG-Cheat_sheet.pdf -repo pdf Cryptography/GPG_Cheat_Sheet.pdf -repo pdf Enumeration/FFUF_Cheat_Sheet__2_.pdf -repo pdf Enumeration/netexec.pdf -repo pdf Enumeration/RustScan.pdf -repo pdf Exploitation/metasploit_cheatsheet_source__1_.pdf -repo pdf HTB/cheatsheet-file-inclusion.pdf -repo pdf HTB/cheatsheet-file-transfers.pdf -repo pdf HTB/cheatsheet-getting-started.pdf -repo pdf HTB/cheatsheet-intro-to-network-traffic-analysis.pdf -repo pdf HTB/cheatsheet-introduction-to-active-directory.pdf -repo pdf HTB/cheatsheet-javascript-deobfuscation.pdf -repo pdf HTB/cheatsheet-network-enumeration-with-nmap.pdf -repo pdf HTB/cheatsheet-sql-injection-fundamentals.pdf -repo pdf HTB/cheatsheet-stack-based-buffer-overflows-on-windows-x86.pdf -repo pdf HTB/cheatsheet-using-crackmapexec.pdf -repo pdf HTB/cheatsheet-using-the-metasploit-framework.pdf -repo pdf HTB/cheatsheet-web-requests.pdf -repo pdf HTB/cheatsheet-windows-fundamentals.pdf -repo pdf Linux/chmod-cheatsheet.pdf -repo pdf Linux/tty_shell_cheatsheet.pdf -repo pdf Misc/openssl-cheatsheet.pdf -repo pdf Misc/Overleaf Archive.pdf -repo pdf Password-Attacks/john-the-ripper-cheatsheet.pdf -repo pdf Password-Attacks/Password_Attacks_Cheat_Sheet.pdf -repo pdf Purple-Team/PTFM_ Purple Team Field Manual -- By Tim Bryant -- First Edition, United States, October 20, 2020 -- Independently Published; Pragma LLC -- 9781736526712 -- 588002f85367ddf0bd0e674980d8433c -- Anna’s Archive.pdf -repo pdf Web/Curk Document.pdf -vault html bloodhound_ce_cypher_cheatsheet.html -vault pdf Ligolo-ng Double Tunnel Guide.pdf -vault pdf Cryptography/GPG-Cheat_sheet.pdf -vault pdf Cryptography/GPG_Cheat_Sheet.pdf -vault pdf Enumeration/FFUF_Cheat_Sheet__2_.pdf -vault pdf Enumeration/netexec.pdf -vault pdf Enumeration/RustScan.pdf -vault html HashingAndEncrypting/argon2-cheatsheet.html -vault pdf Linux/chmod-cheatsheet.pdf -vault pdf Linux/tty_shell_cheatsheet.pdf -vault pdf Misc/Overleaf Archive.pdf -vault pdf PasswordAttacks/Password_Attacks_Cheat_Sheet.pdf -vault pdf TunnelingAndPivoting/Ligolo-ng - multi-hop tunneling setup and enumeration.pdf -vault pdf Web/Curl Document.pdf -vault pdf Web/phishing-identification-cheatsheet.pdf -vault pdf Web/phishing-identification-guide.pdf -\ No newline at end of file diff --git a/candidates-summary.tsv b/candidates-summary.tsv @@ -1,233 +0,0 @@ -IDX SOURCE TOPDIR BYTES LINES H2 CODE RELPATH TITLE -0 repo (root) 13487 427 15 17 README.md README -1 repo Active-Directory 25728 845 17 53 Active-Directory/BloodHound-Python_Cheatsheet.md Usually pre-installed on Kali -2 repo Active-Directory 26655 705 16 41 Active-Directory/BloodyAD.md Clone repository -3 repo Active-Directory 22394 698 11 39 Active-Directory/Certipy-ad.md Install via pip -4 repo Active-Directory 22834 806 13 84 Active-Directory/Impacket_Cheatsheet.md Can also use like: -5 repo Active-Directory 23044 739 15 51 Active-Directory/SharpHound_Cheatsheet.md Start SMB server in directory containing SharpHound.exe -6 repo Enumeration 44259 2064 29 264 Enumeration/Shodan_Cheatsheet.md Install via pip -7 repo Misc 17140 563 9 49 Misc/smbserver-cheatsheet.md Verify Impacket is installed -8 repo Misc 14930 550 15 53 Misc/sponge-cheatsheet.md Debian/Ubuntu -9 vault (root) 23902 550 15 27 Attack-Flow-Guide.md Attack-Flow-Guide -10 vault (root) 22716 464 19 23 CPTS-Exam-Attack-Flow.md CPTS-Exam-Attack-Flow -11 vault (root) 10231 287 17 19 CPTS-Exam-Most-Used-Commands.md CPTS-Exam-Most-Used-Commands -12 vault (root) 48303 1210 31 26 Digital_Forensics-Roadmap.md ForensicsLab_S1 Forensic Investigation Roadmap -13 vault (root) 30335 716 15 18 Git β€” Complete Branch & Vault Management Guide.md Initialise the repo if you haven't already -14 vault (root) 22150 793 23 40 macOS Terminal Tweaks Cheat Sheet.md 🍎 macOS Hidden Terminal Tweaks β€” Cheat Sheet -15 vault (root) 12671 319 10 10 macOS-ISO-to-USB-Cheatsheet.md πŸ’Ώ Burning ISOs to USB on macOS (CLI) -16 vault (root) 27271 718 24 53 Most-Used-Commands.md Most-Used-Commands -17 vault ActiveDirectory 14145 272 11 9 ActiveDirectory/ACL-ESC-Techniques/_ADCS Attack Methodology Guide.md ADCS Attack Methodology Guide -18 vault ActiveDirectory 5146 131 4 3 ActiveDirectory/ACL-ESC-Techniques/_ADCS Dashboard.md _ADCS Dashboard -19 vault ActiveDirectory 9394 199 6 3 ActiveDirectory/ACL-ESC-Techniques/_ADCS ESC Attack Index.md ADCS ESC Attack Index -20 vault ActiveDirectory 15845 418 15 20 ActiveDirectory/ACL-ESC-Techniques/Certificate Persistence β€” Certifried (CVE-2022-26923).md Certificate Persistence β€” Certifried (CVE-2022-26923) -21 vault ActiveDirectory 4534 127 8 7 ActiveDirectory/ACL-ESC-Techniques/DPERSIST2 β€” Rogue CA Certificate (NTAuth Injection).md DPERSIST2 β€” Rogue CA Certificate (NTAuth Injection) -22 vault ActiveDirectory 4367 112 8 5 ActiveDirectory/ACL-ESC-Techniques/DPERSIST3 β€” Malicious Misconfiguration (ACL Backdoor).md DPERSIST3 β€” Malicious Misconfiguration (ACL Backdoor) -23 vault ActiveDirectory 14009 310 15 12 ActiveDirectory/ACL-ESC-Techniques/ESC1 β€” SAN Specification in Template.md ESC1 β€” SAN Specification in Template -24 vault ActiveDirectory 7640 225 11 8 ActiveDirectory/ACL-ESC-Techniques/ESC10 β€” Weak Certificate Mapping.md ESC10 β€” Weak Certificate Mapping -25 vault ActiveDirectory 16848 378 11 17 ActiveDirectory/ACL-ESC-Techniques/ESC11 β€” NTLM Relay to ADCS RPC (ICPR).md ESC11 β€” NTLM Relay to ADCS RPC (ICPR) -26 vault ActiveDirectory 7421 199 10 8 ActiveDirectory/ACL-ESC-Techniques/ESC12 β€” Shell Access to CA with YubiHSM.md ESC12 β€” Shell Access to CA with YubiHSM -27 vault ActiveDirectory 9076 234 11 9 ActiveDirectory/ACL-ESC-Techniques/ESC13 β€” Issuance Policy OID Group Link.md ESC13 β€” Issuance Policy OID Group Link -28 vault ActiveDirectory 9115 256 10 11 ActiveDirectory/ACL-ESC-Techniques/ESC14 β€” Weak Explicit Certificate Mapping.md ESC14 β€” Weak Explicit Certificate Mapping -29 vault ActiveDirectory 11327 285 14 10 ActiveDirectory/ACL-ESC-Techniques/ESC15 β€” EKUwu (CVE-2024-49019).md ESC15 β€” EKUwu (CVE-2024-49019) -30 vault ActiveDirectory 12959 286 12 14 ActiveDirectory/ACL-ESC-Techniques/ESC16 β€” Security Extension Disabled on CA (Globally).md ESC16 β€” Security Extension Disabled on CA (Globally) -31 vault ActiveDirectory 12091 278 12 10 ActiveDirectory/ACL-ESC-Techniques/ESC17 β€” ADCS Certificate Spoofing to Attack HTTPS-Enabled WSUS Clients.md ESC17 β€” ADCS Certificate Spoofing to Attack HTTPS-Enabled WSUS Clients -32 vault ActiveDirectory 12576 289 13 12 ActiveDirectory/ACL-ESC-Techniques/ESC2 β€” Any Purpose EKU No EKU (The Swiss Certificate).md ESC2 β€” Any Purpose EKU / No EKU ("The Swiss Certificate") -33 vault ActiveDirectory 14513 336 12 14 ActiveDirectory/ACL-ESC-Techniques/ESC3 β€” Misconfigured Enrollment Agent Templates.md ESC3 β€” Misconfigured Enrollment Agent Templates -34 vault ActiveDirectory 15922 344 12 12 ActiveDirectory/ACL-ESC-Techniques/ESC4 β€” Vulnerable Certificate Template Access Control.md ESC4 β€” Vulnerable Certificate Template Access Control -35 vault ActiveDirectory 8671 229 11 8 ActiveDirectory/ACL-ESC-Techniques/ESC5 β€” Vulnerable PKI Object Access Control.md ESC5 β€” Vulnerable PKI Object Access Control -36 vault ActiveDirectory 13436 336 14 13 ActiveDirectory/ACL-ESC-Techniques/ESC6 β€” EDITF_ATTRIBUTESUBJECTALTNAME2 Flag.md ESC6 β€” EDITF_ATTRIBUTESUBJECTALTNAME2 Flag -37 vault ActiveDirectory 16883 448 14 19 ActiveDirectory/ACL-ESC-Techniques/ESC7 β€” Vulnerable CA Access Control (ManageCA ManageCertificates).md ESC7 β€” Vulnerable CA Access Control (ManageCA / ManageCertificates) -38 vault ActiveDirectory 17490 376 11 15 ActiveDirectory/ACL-ESC-Techniques/ESC8 β€” NTLM Relay to ADCS HTTP Web Enrollment.md ESC8 β€” NTLM Relay to ADCS HTTP Web Enrollment -39 vault ActiveDirectory 20390 498 19 19 ActiveDirectory/ACL-ESC-Techniques/ESC9 β€” No Security Extension (Template-Level).md ESC9 β€” No Security Extension (Template-Level) -40 vault ActiveDirectory 12710 328 17 14 ActiveDirectory/ACL-ESC-Techniques/Golden Certificate Attack β€” DPERSIST1.md Golden Certificate Attack β€” DPERSIST1 -41 vault ActiveDirectory 3518 101 8 4 ActiveDirectory/ACL-ESC-Techniques/PERSIST1 β€” Active User Credential Theft via Certificates.md PERSIST1 β€” Active User Credential Theft via Certificates -42 vault ActiveDirectory 3280 98 8 4 ActiveDirectory/ACL-ESC-Techniques/PERSIST2 β€” Machine Account Persistence via Certificates.md PERSIST2 β€” Machine Account Persistence via Certificates -43 vault ActiveDirectory 3580 102 7 5 ActiveDirectory/ACL-ESC-Techniques/PERSIST3 β€” Account Persistence via Certificate Renewal.md PERSIST3 β€” Account Persistence via Certificate Renewal -44 vault ActiveDirectory 7508 190 11 12 ActiveDirectory/ACL-ESC-Techniques/Shadow Credentials β€” msDS-KeyCredentialLink Abuse.md Shadow Credentials β€” msDS-KeyCredentialLink Abuse -45 vault ActiveDirectory 4647 134 9 7 ActiveDirectory/ACL-ESC-Techniques/THEFT1 β€” Exporting Certificates and Keys.md THEFT1 β€” Exporting Certificates and Keys -46 vault ActiveDirectory 4040 121 8 7 ActiveDirectory/ACL-ESC-Techniques/THEFT2 β€” User Certificate Theft via DPAPI.md THEFT2 β€” User Certificate Theft via DPAPI -47 vault ActiveDirectory 3686 110 8 5 ActiveDirectory/ACL-ESC-Techniques/THEFT3 β€” Machine Certificate Theft via DPAPI.md THEFT3 β€” Machine Certificate Theft via DPAPI -48 vault ActiveDirectory 3481 120 8 6 ActiveDirectory/ACL-ESC-Techniques/THEFT4 β€” Finding Certificate Files.md THEFT4 β€” Finding Certificate Files -49 vault ActiveDirectory 4350 133 9 7 ActiveDirectory/ACL-ESC-Techniques/THEFT5 β€” NTLM Theft via PKINIT (UnPAC-the-Hash).md THEFT5 β€” NTLM Theft via PKINIT (UnPAC-the-Hash) -50 vault ActiveDirectory 53345 1369 10 75 ActiveDirectory/Active-Directory_cheat_sheet.md Active Directory Exploitation Cheat Sheet -51 vault ActiveDirectory 2985 79 6 3 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #61 β€” Skeleton Key Attack.md 🟀 Attack #61 β€” Skeleton Key Attack -52 vault ActiveDirectory 2858 73 5 3 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #62 β€” DSRM Backdoor Abuse.md 🟀 Attack #62 β€” DSRM Backdoor Abuse -53 vault ActiveDirectory 2553 69 5 2 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #63 β€” SID History Injection.md 🟀 Attack #63 β€” SID History Injection -54 vault ActiveDirectory 2499 68 5 2 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #64 β€” Golden Ticket Persistence.md 🟀 Attack #64 β€” Golden Ticket Persistence -55 vault ActiveDirectory 2881 72 5 3 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #65 β€” ACL Backdooring (Persistence via DCSync ACE).md 🟀 Attack #65 β€” ACL Backdooring (Persistence via DCSync ACE) -56 vault ActiveDirectory 3112 73 5 3 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #66 β€” Malicious GPO Creation.md 🟀 Attack #66 β€” Malicious GPO Creation -57 vault ActiveDirectory 2922 73 5 3 ActiveDirectory/AD-Attack/Category-Eight/🟀 Attack #67 β€” ADCS Certificate-Based Persistence.md 🟀 Attack #67 β€” ADCS Certificate-Based Persistence -58 vault ActiveDirectory 42345 726 13 22 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #37 β€” DCSync Attack.md πŸ”΅ Attack #37 β€” DCSync Attack -59 vault ActiveDirectory 25501 408 12 9 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #38 β€” DCShadow Attack.md πŸ”΅ Attack #38 β€” DCShadow Attack -60 vault ActiveDirectory 26964 448 12 15 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #39 β€” NTDS.dit Extraction and Dumping.md πŸ”΅ Attack #39 β€” NTDS.dit Extraction & Dumping -61 vault ActiveDirectory 21999 371 12 9 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #40 β€” Zerologon (CVE-2020-1472).md πŸ”΅ Attack #40 β€” Zerologon (CVE-2020-1472) -62 vault ActiveDirectory 22224 396 12 8 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #41 β€” PetitPotam (CVE-2021-36942).md πŸ”΅ Attack #41 β€” PetitPotam (CVE-2021-36942) β€” NTLM Coercion -63 vault ActiveDirectory 21754 380 12 13 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #42 β€” PrinterBug SpoolSample.md πŸ”΅ Attack #42 β€” PrinterBug / SpoolSample β€” Print Spooler Coercion -64 vault ActiveDirectory 22175 381 12 10 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #43 β€” PrintNightmare (CVE-2021-34527).md πŸ”΅ Attack #43 β€” PrintNightmare (CVE-2021-34527) -65 vault ActiveDirectory 24537 414 12 10 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack #44 β€” noPAC Sam-the-Admin (CVE-2021-42278 42287).md πŸ”΅ Attack #44 β€” noPAC / Sam-the-Admin (CVE-2021-42278/42287) -66 vault ActiveDirectory 0 1 0 0 ActiveDirectory/AD-Attack/Category-Five/πŸ”΅ Attack.md πŸ”΅ Attack -67 vault ActiveDirectory 16456 319 7 10 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #27 β€” ESC1 SAN Specification in Template.md 🟒 Attack #27 β€” ESC1: SAN Specification in Certificate Template -68 vault ActiveDirectory 3933 101 6 5 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #28 β€” ESC2 Any Purpose EKU No EKU.md 🟒 Attack #28 β€” ESC2: Any Purpose EKU / No EKU -69 vault ActiveDirectory 3221 78 5 4 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #29 β€” ESC3 Certificate Request Agent.md 🟒 Attack #29 β€” ESC3: Certificate Request Agent -70 vault ActiveDirectory 4364 111 5 6 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #30 β€” ESC4 Template Write Permissions.md 🟒 Attack #30 β€” ESC4: Template Write Permissions -71 vault ActiveDirectory 3326 84 6 5 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #31 β€” ESC6 EDITF_ATTRIBUTESUBJECTALTNAME2 Flag.md 🟒 Attack #31 β€” ESC6: EDITF_ATTRIBUTESUBJECTALTNAME2 Flag -72 vault ActiveDirectory 4291 111 5 4 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #32 β€” ESC7 Vulnerable CA Officer Permissions.md 🟒 Attack #32 β€” ESC7: Vulnerable CA Officer Permissions -73 vault ActiveDirectory 4975 126 7 4 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #33 β€” ESC8 NTLM Relay to ADCS HTTP Endpoint.md 🟒 Attack #33 β€” ESC8: NTLM Relay to ADCS HTTP Endpoint -74 vault ActiveDirectory 2844 71 5 3 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #34 β€” ESC11 NTLM Relay to ADCS RPC.md 🟒 Attack #34 β€” ESC11: NTLM Relay to ADCS RPC -75 vault ActiveDirectory 5210 126 7 5 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #35 β€” Golden Certificate Attack.md 🟒 Attack #35 β€” Golden Certificate Attack -76 vault ActiveDirectory 3833 93 5 3 ActiveDirectory/AD-Attack/Category-Four/🟒 Attack #36 β€” Certifried (CVE-2022-26923).md 🟒 Attack #36 β€” Certificate Persistence (Certifried / CVE-2022-26923) -77 vault ActiveDirectory 3361 81 5 3 ActiveDirectory/AD-Attack/Category-Nine/πŸ”Ά Attack #68 β€” Cross-Domain Trust Abuse (SID History).md πŸ”Ά Attack #68 β€” Cross-Domain Trust Abuse (SID History) -78 vault ActiveDirectory 3318 78 5 3 ActiveDirectory/AD-Attack/Category-Nine/πŸ”Ά Attack #69 β€” Forest Trust Abuse Cross-Forest Ticket Forging.md πŸ”Ά Attack #69 β€” Forest Trust Abuse / Cross-Forest Ticket Forging -79 vault ActiveDirectory 2430 61 5 2 ActiveDirectory/AD-Attack/Category-Nine/πŸ”Ά Attack #70 β€” ADCS Cross-Domain Enrollment.md πŸ”Ά Attack #70 β€” ADCS Cross-Domain Enrollment -80 vault ActiveDirectory 2861 70 5 2 ActiveDirectory/AD-Attack/Category-Nine/πŸ”Ά Attack #71 β€” PAM Trust Abuse (Bastion Forest).md πŸ”Ά Attack #71 β€” PAM Trust Abuse (Bastion Forest) -81 vault ActiveDirectory 25318 397 9 10 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #1 β€” Password Spraying.md πŸ”΄ Attack #1 β€” Password Spraying -82 vault ActiveDirectory 34773 610 9 18 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #10 β€” Credential Hunting in Shares GPP Passwords.md πŸ”΄ Attack #10 β€” Credential Hunting in Shares / GPP Passwords -83 vault ActiveDirectory 25641 430 9 10 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #2 β€” Kerberoasting.md πŸ”΄ Attack #2 β€” Kerberoasting -84 vault ActiveDirectory 21874 410 9 13 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #3 β€” AS-REP Roasting.md πŸ”΄ Attack #3 β€” AS-REP Roasting -85 vault ActiveDirectory 27608 458 9 13 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #4 β€” Pass-the-Hash (PtH).md πŸ”΄ Attack #4 β€” Pass-the-Hash (PtH) -86 vault ActiveDirectory 24968 462 10 11 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #5 β€” Pass-the-Ticket (PtT).md πŸ”΄ Attack #5 β€” Pass-the-Ticket (PtT) -87 vault ActiveDirectory 25099 446 10 10 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #6 β€” Overpass-the-Hash (Pass-the-Key).md πŸ”΄ Attack #6 β€” Overpass-the-Hash (Pass-the-Key) -88 vault ActiveDirectory 29681 521 9 12 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #7 β€” NTLM Relay Attacks.md πŸ”΄ Attack #7 β€” NTLM Relay Attacks -89 vault ActiveDirectory 27884 513 9 14 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #8 β€” LLMNR NBT-NS mDNS Poisoning.md πŸ”΄ Attack #8 β€” LLMNR / NBT-NS / mDNS Poisoning -90 vault ActiveDirectory 27947 516 9 15 ActiveDirectory/AD-Attack/Category-One/πŸ”΄ Attack #9 β€” mitm6 (IPv6 DNS Spoofing DHCPv6 Takeover).md πŸ”΄ Attack #9 β€” mitm6 (IPv6 DNS Spoofing / DHCPv6 Takeover) -91 vault ActiveDirectory 18485 311 7 9 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #54 β€” PsExec Remote Execution via SMB.md ⚫ Attack #54 β€” PsExec / Remote Execution via SMB -92 vault ActiveDirectory 2488 56 4 2 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #55 β€” WinRM Evil-WinRM Lateral Movement.md ⚫ Attack #55 β€” WinRM / Evil-WinRM Lateral Movement -93 vault ActiveDirectory 2605 67 4 2 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #56 β€” RDP Lateral Movement and Hijacking.md ⚫ Attack #56 β€” RDP Lateral Movement & Hijacking -94 vault ActiveDirectory 2715 56 4 2 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #57 β€” DCOM Lateral Movement.md ⚫ Attack #57 β€” DCOM Lateral Movement -95 vault ActiveDirectory 2414 55 4 2 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #58 β€” WMI Lateral Movement.md ⚫ Attack #58 β€” WMI Lateral Movement -96 vault ActiveDirectory 2146 56 4 2 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #59 β€” SCM Service Manager Lateral Movement.md ⚫ Attack #59 β€” SCM / Service Manager Lateral Movement -97 vault ActiveDirectory 2947 81 5 4 ActiveDirectory/AD-Attack/Category-Seven/⚫ Attack #60 β€” Token Stealing and Impersonation.md ⚫ Attack #60 β€” Token Stealing & Impersonation (Lateral) -98 vault ActiveDirectory 16940 319 7 10 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #45 β€” Token Impersonation (SeImpersonatePrivilege).md 🟣 Attack #45 β€” Token Impersonation (SeImpersonatePrivilege) -99 vault ActiveDirectory 2847 73 5 3 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #46 β€” DNSAdmins DLL Injection.md 🟣 Attack #46 β€” DNSAdmins DLL Injection -100 vault ActiveDirectory 2807 72 5 3 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #47 β€” MachineAccountQuota (MAQ) Abuse.md 🟣 Attack #47 β€” MachineAccountQuota (MAQ) Abuse -101 vault ActiveDirectory 2743 69 5 3 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #48 β€” GPP Password Decryption.md 🟣 Attack #48 β€” GPP Password Decryption -102 vault ActiveDirectory 3513 83 5 3 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #49 β€” Abusing Backup Operators Group.md 🟣 Attack #49 β€” Abusing Backup Operators Group -103 vault ActiveDirectory 2669 67 5 2 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #50 β€” Abusing Account Operators Group.md 🟣 Attack #50 β€” Abusing Account Operators Group -104 vault ActiveDirectory 2251 62 5 2 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #51 β€” Abusing Server Operators Group.md 🟣 Attack #51 β€” Abusing Server Operators Group -105 vault ActiveDirectory 1995 60 5 2 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #52 β€” Abusing Print Operators Group.md 🟣 Attack #52 β€” Abusing Print Operators Group -106 vault ActiveDirectory 2864 74 5 3 ActiveDirectory/AD-Attack/Category-Six/🟣 Attack #53 β€” Exchange Windows Permissions (WriteDACL to DCSync).md 🟣 Attack #53 β€” Exchange Windows Permissions (WriteDACL β†’ DCSync) -107 vault ActiveDirectory 3531 75 5 3 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #72 β€” LAPS Password Extraction.md πŸ”· Attack #72 β€” LAPS Password Extraction -108 vault ActiveDirectory 3509 75 5 3 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #73 β€” gMSA Password Extraction.md πŸ”· Attack #73 β€” gMSA Password Extraction -109 vault ActiveDirectory 2577 70 5 3 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #74 β€” Azure AD Connect Credential Extraction.md πŸ”· Attack #74 β€” Azure AD Connect Credential Extraction -110 vault ActiveDirectory 2896 73 5 3 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #75 β€” SCCM MECM Exploitation.md πŸ”· Attack #75 β€” SCCM / MECM Exploitation -111 vault ActiveDirectory 4082 100 5 6 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #76 β€” MSSQL Server and Linked Server Abuse.md πŸ”· Attack #76 β€” MSSQL Server & Linked Server Abuse -112 vault ActiveDirectory 2399 62 5 2 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #77 β€” DFSCoerce MS-DFSNM Coercion.md πŸ”· Attack #77 β€” Coercion via DFSCoerce / MS-DFSNM -113 vault ActiveDirectory 4081 90 5 3 ActiveDirectory/AD-Attack/Category-Ten/πŸ”· Attack #78 β€” AD Recycle Bin Object Abuse.md πŸ”· Attack #78 β€” AD Recycle Bin Object Abuse -114 vault ActiveDirectory 18341 326 7 11 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #19 β€” GenericAll Abuse.md 🟑 Attack #19 β€” GenericAll Abuse -115 vault ActiveDirectory 7185 159 7 7 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #20 β€” GenericWrite Abuse.md 🟑 Attack #20 β€” GenericWrite Abuse -116 vault ActiveDirectory 6586 151 7 9 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #21 β€” WriteDACL Abuse.md 🟑 Attack #21 β€” WriteDACL Abuse -117 vault ActiveDirectory 4570 111 6 4 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #22 β€” WriteOwner Abuse.md 🟑 Attack #22 β€” WriteOwner Abuse -118 vault ActiveDirectory 4523 100 7 3 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #23 β€” ForceChangePassword Abuse.md 🟑 Attack #23 β€” ForceChangePassword Abuse -119 vault ActiveDirectory 3728 94 5 5 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #24 β€” AllExtendedRights DCSync ACE Abuse.md 🟑 Attack #24 β€” AllExtendedRights / DCSync ACE Abuse -120 vault ActiveDirectory 7451 164 7 5 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #25 β€” Shadow Credentials Attack (msDS-KeyCredentialLink).md 🟑 Attack #25 β€” Shadow Credentials Attack (msDS-KeyCredentialLink) -121 vault ActiveDirectory 7879 181 7 8 ActiveDirectory/AD-Attack/Category-Three/🟑 Attack #26 β€” AdminSDHolder Persistence via ACL.md 🟑 Attack #26 β€” AdminSDHolder Persistence via ACL -122 vault ActiveDirectory 21429 413 7 12 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #11 β€” Golden Ticket Attack.md 🟠 Attack #11 β€” Golden Ticket Attack -123 vault ActiveDirectory 20466 430 7 11 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #12 β€” Silver Ticket Attack.md 🟠 Attack #12 β€” Silver Ticket Attack -124 vault ActiveDirectory 7530 172 7 5 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #13 β€” Diamond Ticket Attack.md 🟠 Attack #13 β€” Diamond Ticket Attack -125 vault ActiveDirectory 6762 145 7 4 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #14 β€” Sapphire Ticket Attack.md 🟠 Attack #14 β€” Sapphire Ticket Attack -126 vault ActiveDirectory 9622 188 7 9 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #15 β€” Unconstrained Delegation Abuse.md 🟠 Attack #15 β€” Unconstrained Delegation Abuse -127 vault ActiveDirectory 8889 184 7 6 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #16 β€” Constrained Delegation Abuse (S4U2Proxy).md 🟠 Attack #16 β€” Constrained Delegation Abuse (S4U2Proxy) -128 vault ActiveDirectory 9955 206 7 11 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #17 β€” Resource-Based Constrained Delegation (RBCD).md 🟠 Attack #17 β€” Resource-Based Constrained Delegation (RBCD) -129 vault ActiveDirectory 6073 135 7 2 ActiveDirectory/AD-Attack/Category-Two/🟠 Attack #18 β€” Bronze Bit Attack (CVE-2020-17049).md 🟠 Attack #18 β€” Bronze Bit Attack (CVE-2020-17049) -130 vault ActiveDirectory 36667 992 19 45 ActiveDirectory/AD_Pentest_Tools_Cheat_Sheet.md Add before touching any target -131 vault ActiveDirectory 8818 256 10 13 ActiveDirectory/bloodhound-ce-python-cheatsheet.md BloodHound CE Python Cheat Sheet -132 vault ActiveDirectory 22611 795 17 45 ActiveDirectory/BloodHound-Python_Cheatsheet.md 🐍 BloodHound-Python Cheatsheet -133 vault ActiveDirectory 30854 984 17 106 ActiveDirectory/BloodyAD.md 🩸 BloodyAD Cheat Sheet -134 vault ActiveDirectory 22065 698 10 32 ActiveDirectory/Certipy-ad.md πŸ” Certipy-AD Cheat Sheet -135 vault ActiveDirectory 8133 243 9 15 ActiveDirectory/faketime-cheatsheet.md Faketime Cheat Sheet β€” Beating Kerberos Clock Skew -136 vault ActiveDirectory 8848 245 9 10 ActiveDirectory/Kerberos/Kerberoasting Cheatsheet.md 🎟️ Kerberoasting Cheatsheet -137 vault ActiveDirectory 9651 314 9 18 ActiveDirectory/Kerberos/Kerberoasting β€” Local On-Host Cheatsheet.md 🎟️ Kerberoasting β€” Local / On-Host Cheatsheet -138 vault ActiveDirectory 13162 351 10 15 ActiveDirectory/Kerberos/πŸ₯ˆ Silver Ticket Attack Cheatsheet.md πŸ₯ˆ Silver Ticket Attack Cheatsheet -139 vault ActiveDirectory 15068 329 10 7 ActiveDirectory/Kerbrute.md Pre-built binary (Linux) -140 vault ActiveDirectory 11437 398 12 27 ActiveDirectory/LDAP Search.md LDAP Enumeration Cheat Sheet for HTB Support -141 vault ActiveDirectory 15657 568 17 30 ActiveDirectory/ldapdomaindump.md Complete ldapdomaindump Cheat Sheet for HTB Support -142 vault ActiveDirectory 25568 811 24 33 ActiveDirectory/Rubeus.md 🎟️ Rubeus β€” Complete Cheat Sheet -143 vault ActiveDirectory 18407 672 15 45 ActiveDirectory/SharpHound_Cheatsheet.md 🩸 SharpHound.exe Cheatsheet -144 vault ActiveDirectory 28291 848 16 31 ActiveDirectory/SharpSploit.md πŸ—‘οΈ SharpSploit β€” Complete Cheat Sheet -145 vault ActiveDirectory 28010 801 18 38 ActiveDirectory/Snaffler.md πŸ” Snaffler β€” Complete Cheat Sheet -146 vault Cryptography 0 1 0 0 Cryptography/gog.md gog -147 vault Cryptography 75000 2457 33 154 Cryptography/GPG - Cheatsheet markdown.md Replace every placeholder with a full 40-hex-character OpenPGP fingerprint. -148 vault DFIR 73874 1915 14 60 DFIR/Forensics Cheatsheet.md Linux/macOS -149 vault DFIR 34346 852 11 30 DFIR/RECmd - FullGuide.md RECmd: Registry Forensics Operational Guide -150 vault DFIR 29512 849 8 38 DFIR/REDmd - Quick Cheat sheet.md RECmd: Registry Forensics Cheat Sheet & Workflow Guide -151 vault DFIR 13167 592 7 42 DFIR/Volitility3 .md **VOLATILITY 3 - WINDOWS MEMORY FORENSICS CHEAT SHEET** -152 vault Enumeration 37698 1066 3 80 Enumeration/Anonymous Null Testing.md Test null session -153 vault Enumeration 8206 271 7 14 Enumeration/Awesome NMAP grep.md awesome-nmap-grep πŸ’₯ -154 vault Enumeration 27932 674 12 23 Enumeration/Cheatsheet - Infrastructure Enumeration Tools 1.md Basic query β€” good starting point -155 vault Enumeration 31136 839 11 19 Enumeration/Credential Hunting.md Basic recursive search (case-insensitive, whole word, suppress errors) -156 vault Enumeration 37153 1417 20 84 Enumeration/ffuf_cheat_sheet.md ffuf Cheat Sheet -157 vault Enumeration 18961 476 10 18 Enumeration/GitHub-Enum/2.4 - Cheatsheet - Gitleaks.md macOS β€” Homebrew -158 vault Enumeration 28585 640 10 17 Enumeration/GitHub-Enum/2.5 - Cheatsheet - TruffleHog.md macOS β€” Homebrew -159 vault Enumeration 4854 77 4 0 Enumeration/LFI - Cheat Sheet.md LFI - Cheat Sheet -160 vault Enumeration 36779 994 14 21 Enumeration/Nikto & Nuclei - Web Scanner Cheatsheets.md Nikto -161 vault Enumeration 18012 368 11 14 Enumeration/Nmap Cheatsheet 2026.md Then just tap spacebar anytime to force an immediate progress line -162 vault Enumeration 70320 1627 33 45 Enumeration/NSE Guide.md Safe FTP enumeration -163 vault Enumeration 3445 159 7 13 Enumeration/rustscan-usage.md Runs: rustscan -a 192.168.1.1 -b 65535 -t 3000 -164 vault Enumeration 22440 858 17 73 Enumeration/rustscan.md RustScan Comprehensive Cheat Sheet -165 vault Enumeration 18034 589 17 60 Enumeration/RustScan_Cheatsheet.md RustScan Cheat Sheet -166 vault Enumeration 18061 416 7 12 Enumeration/SMBMAP.md Null session (anonymous) -167 vault Enumeration 15579 549 15 28 Enumeration/Windows Emumeration.md Windows Enumeration Cheat Sheet -168 vault Enumeration 24486 773 15 59 Enumeration/WPScan 1.md WPScan 1 -169 vault Enumeration 43195 866 13 25 Enumeration/WPScan.md Basic scan with API token -170 vault Exploitation 87715 1921 13 53 Exploitation/Implementation Roadmap Strategic Workflow for Windows Privilege Escalation.md Enable ALL disabled privileges (requires scripts) -171 vault Exploitation 57693 2244 59 247 Exploitation/Jailbreak - TTY Upgrade.md TTY Upgrades, Shell Stabilisation & Restricted Shell Escapes -172 vault Exploitation 19951 541 4 38 Exploitation/sqlmap Cheat Sheet - SQL Injection Testing & Data Extraction.md Basic GET parameter test -173 vault Exploitation 29330 659 10 20 Exploitation/sqlmap.md Ultimate SQLMap Cheat Sheet -174 vault Exploitation 17715 800 8 74 Exploitation/TTY and Escaping Restricted Env.md Interactive Shells & Restricted Shell Escapes β€” Cheat Sheet -175 vault Git 13574 365 9 14 Git/Branches Expanded.md Initialise the repo if you haven't already -176 vault Git 19298 497 1 21 Git/Branches.md Full default log (opens pager) -177 vault Git 6039 159 5 4 Git/Git β€” Move Existing Edits to a New Branch (Railway Site).md 1. Create a new branch AND switch to it immediately -178 vault Git 31689 820 10 46 Git/git-cheatsheet.md Git Cheat Sheet -179 vault Git 9121 241 6 6 Git/Resetting.md Check current state of working tree and staging area -180 vault HashingAndEncrypting 19786 703 14 37 HashingAndEncrypting/Hashing cheat sheet .md Manual Hashing Cheat Sheet -181 vault Linux 22933 549 6 30 Linux/Find Command.md Find files by name (case-insensitive) -182 vault Linux 15537 804 9 37 Linux/Linux File & Directory Search Cheat Sheet.md Linux File & Directory Search Cheat Sheet -183 vault Linux 13147 347 10 15 Linux/RDP CheatSheet.md macOS: native SDL client (recommended) -184 vault Misc 19693 776 9 25 Misc/Pivoting and Tunnelling .md Pivoting & Port Forwarding Cheat Sheet -185 vault Misc 3491 89 5 9 Misc/smbshare.md Impacket SMB Share Cheat Sheet -186 vault Misc 10501 476 12 50 Misc/sponge-cheatsheet.md 🧽 Sponge Command Cheat Sheet -187 vault Misc 11572 451 13 26 Misc/SSH Portfwding with metasploit .md SSH Tunneling with Metasploit: A Complete Guide (Pandora HTB Edition) -188 vault Misc 10884 404 21 6 Misc/tmux.md πŸ‰ Complete Tmux Cheat Sheet for Your Config -189 vault Misc 35728 1471 15 93 Misc/Tunneling.md Tunneling Tools Cheatsheet -190 vault PasswordAttacks 24928 97 0 0 PasswordAttacks/hashcat modes.md hashcat modes -191 vault PasswordAttacks 9709 262 11 12 PasswordAttacks/hashcat-cheatsheet.md Hashcat Cheat Sheet -192 vault PasswordAttacks 13058 339 11 13 PasswordAttacks/john-cheatsheet.md John the Ripper Cheat Sheet -193 vault PrivEsc 5336 37 0 0 PrivEsc/Linux PrivEsc Cheat Sheet.md Linux PrivEsc Cheat Sheet -194 vault PrivEsc 71700 2273 31 118 PrivEsc/PrivEsc - Windows.md Windows Privilege Escalation Master Guide (December 2025 Edition) -195 vault PrivEsc 80395 1794 22 72 PrivEsc/Windows PrivEsc.md 1. Enable the privilege (disabled by default) -196 vault Tools 10780 289 12 6 Tools/BloodHound-Cheatsheet.md BloodHound Cheatsheet -197 vault Tools 8396 225 12 5 Tools/Certipy-ADCS-Cheatsheet.md Certipy AD CS Cheatsheet -198 vault Tools 71280 2541 29 161 Tools/CMD-Powershell Cheat Sheet.md Windows Penetration Testing Cheat Sheet -199 vault Tools 7121 174 10 3 Tools/Cobalt-Strike-Cheatsheet.md Cobalt Strike Cheatsheet -200 vault Tools 15845 640 18 45 Tools/dd tool.md Linux `dd` Command: Complete Guide and Cheat Sheet -201 vault Tools 12434 360 15 10 Tools/EyeWitness-Cheatsheet.md EyeWitness Cheatsheet -202 vault Tools 8633 264 12 6 Tools/Ffuf-Cheatsheet.md ffuf Cheatsheet -203 vault Tools 43028 1013 16 28 Tools/fscan.md Linux (raw ICMP, requires root or CAP_NET_RAW) -204 vault Tools 26557 676 17 20 Tools/gobuster.md Gobuster Cheat Sheet -205 vault Tools 10573 305 14 8 Tools/Hashcat-Cheatsheet.md Hashcat Cheatsheet -206 vault Tools 8736 284 14 11 Tools/Impacket-Cheatsheet.md Impacket Cheatsheet -207 vault Tools 17216 640 12 64 Tools/Impacket.md Impacket Field Manual: Core Tools Deep Dive -208 vault Tools 34051 809 15 30 Tools/Internet-Archival-Guide/Internet Archival Guide.md Linux -209 vault Tools 2034 54 1 1 Tools/Internet-Archival-Guide/scripts/README.md Archival Scripts -210 vault Tools 55540 2110 28 74 Tools/Ligolo-ng Cheat sheet.md Installation (Kali) -211 vault Tools 48422 988 12 21 Tools/meterpreter.md In msfconsole -212 vault Tools 6170 196 11 6 Tools/Mimikatz-Cheatsheet.md Mimikatz Cheatsheet -213 vault Tools 26928 968 21 50 Tools/Netexec (nxc) Cheat Sheet.md Netexec (nxc) Cheat Sheet - 2025 Edition -214 vault Tools 15397 558 13 30 Tools/NetExec - SpiderPlus.md NetExec Spider Module Guide - Downloading Files from SMB Shares -215 vault Tools 10537 322 14 9 Tools/NetExec-Cheatsheet.md NetExec (nxc) Cheatsheet -216 vault Tools 9111 237 12 6 Tools/NTLM-Kerberos-Relay-Cheatsheet.md NTLM / Kerberos Relay Cheatsheet -217 vault Tools 27772 748 19 21 Tools/Nuclei-Cheatsheet.md Nuclei Cheatsheet (v3.x) -218 vault Tools 11024 372 10 21 Tools/pcap-credential-extraction-cheatsheet.md PCAP Credential Extraction Cheat Sheet -219 vault Tools 38989 1173 1 73 Tools/Powershell.md Or use the alias: -220 vault Tools 8868 240 11 5 Tools/Rubeus-Cheatsheet.md Rubeus Cheatsheet -221 vault Tools 3697 120 4 10 Tools/smbserver.py.md πŸ“‚ Impacket smbserver.py Usage Guide -222 vault Tools 33133 1077 19 48 Tools/TAR.md The Ultimate `tar` Cheat Sheet -223 vault Tools 11344 403 14 42 Tools/Tshark.md TShark Cheat Sheet -224 vault Tools 0 1 0 0 Tools/Username Anarchy.md Username Anarchy -225 vault Tools 15382 334 14 11 Tools/webfuzz.md Requirements: ffuf, python3, curl (all present on the working box). SecLists optional. -226 vault Web 16261 420 9 13 Web/Blind XSS Tool - ezXSS.md Blind XSS Tool β€” ezXSS `fas:ClipboardList` -227 vault Web 18380 478 9 17 Web/Blind XSS Tool - Interactsh.md Blind XSS Tool β€” Interactsh `fas:ClipboardList` -228 vault Web 15354 379 8 13 Web/Blind XSS Tool - XSS Hunter.md Blind XSS Tool β€” XSS Hunter `fas:ClipboardList` -229 vault Web 22932 426 10 8 Web/Cross-Site Scripting (XSS) - HTB Cheat Sheet.md Cross-Site Scripting (XSS) β€” HTB Cheat Sheet `fas:ClipboardList` -230 vault Web 22408 487 18 26 Web/Phishing Site & Link Identification - Cheat Sheet.md Phishing Site & Link Identification β€” Cheat Sheet -231 vault macOS 22150 793 23 40 macOS/macOS Terminal Tweaks Cheat Sheet.md 🍎 macOS Hidden Terminal Tweaks β€” Cheat Sheet -\ No newline at end of file diff --git a/content-manifest-missing.json b/content-manifest-missing.json @@ -1,576 +0,0 @@ -{ - "sheets": [ - { - "source": "vault", - "rel": "ActiveDirectory/Rubeus.md", - "category": "active-directory", - "slug": "rubeus", - "title": "Rubeus", - "description": "Rubeus Kerberos abuse: kerberoast, asreproast, ticket forging, S4U, pass-the-ticket, overpass-the-hash.", - "tools": [ - "Rubeus" - ], - "tags": [ - "active-directory", - "kerberos", - "tickets" - ], - "difficulty": "advanced", - "action": "port", - "note": "Vault version (25KB/24H2/33code) far richer than Tools/Rubeus-Cheatsheet (8.8KB); single canonical Rubeus." - }, - { - "source": "vault", - "rel": "ActiveDirectory/BloodyAD.md", - "category": "active-directory", - "slug": "bloodyad", - "title": "BloodyAD", - "description": "BloodyAD LDAP privilege-abuse toolkit: RBCD, shadow creds, DACL edits, password/attribute writes.", - "tools": [ - "BloodyAD" - ], - "tags": [ - "active-directory", - "ldap", - "acl-abuse" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Vault (30KB/106code) beats repo copy (26KB/41code); dedupe winner." - }, - { - "source": "vault", - "rel": "Enumeration/WPScan.md", - "category": "enumeration", - "slug": "wpscan", - "title": "WPScan", - "description": "WPScan WordPress enumeration: plugins/themes/users, vuln API tokens and password attacks.", - "tools": [ - "WPScan" - ], - "tags": [ - "enumeration", - "web", - "wordpress" - ], - "difficulty": "intermediate", - "action": "port", - "note": "WPScan.md (43KB) richer than WPScan 1 (24KB); dedupe winner." - }, - { - "source": "vault", - "rel": "Enumeration/SMBMAP.md", - "category": "enumeration", - "slug": "smbmap", - "title": "SMBMap", - "description": "SMBMap share enumeration, permissions mapping, file download/upload and command execution over SMB.", - "tools": [ - "SMBMap" - ], - "tags": [ - "enumeration", - "smb", - "shares" - ], - "difficulty": "beginner", - "action": "port", - "note": "Canonical SMB share-enum tool page." - }, - { - "source": "repo", - "rel": "Enumeration/Shodan_Cheatsheet.md", - "category": "enumeration", - "slug": "shodan", - "title": "Shodan", - "description": "Shodan search filters, dorks, CLI and API workflows for internet-wide asset and service discovery.", - "tools": [ - "Shodan" - ], - "tags": [ - "enumeration", - "osint", - "recon" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Huge OSINT sheet (44KB/264code); only Shodan reference and a distinct recon angle." - }, - { - "source": "vault", - "rel": "Exploitation/sqlmap.md", - "category": "exploitation", - "slug": "sqlmap", - "title": "SQLMap", - "description": "SQLMap automated SQL injection: target flags, techniques, enumeration, dumping and tamper scripts.", - "tools": [ - "SQLMap" - ], - "tags": [ - "exploitation", - "sql-injection", - "web" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Ultimate SQLMap (29KB) chosen over the 20KB SQLi-testing variant; single canonical sqlmap." - }, - { - "source": "vault", - "rel": "Exploitation/Jailbreak - TTY Upgrade.md", - "category": "exploitation", - "slug": "shell-stabilization", - "title": "Shell Stabilization & TTY Upgrades", - "description": "Upgrade dumb shells to full TTYs and escape restricted shells (rbash, jails) with many techniques.", - "tools": [ - "python", - "socat", - "stty" - ], - "tags": [ - "exploitation", - "shells", - "post-exploitation" - ], - "difficulty": "intermediate", - "action": "port", - "note": "57KB/247code sheet beats the 17KB TTY variant and the tty_shell PDF; PDF rewrite unnecessary." - }, - { - "source": "vault", - "rel": "Tools/meterpreter.md", - "category": "exploitation", - "slug": "metasploit", - "title": "Metasploit Framework", - "description": "msfconsole and Meterpreter workflow: search, exploits, payloads, sessions, post modules, pivoting.", - "tools": [ - "Metasploit", - "msfconsole", - "Meterpreter" - ], - "tags": [ - "exploitation", - "framework", - "post-exploitation" - ], - "difficulty": "intermediate", - "action": "port", - "note": "48KB markdown covers msfconsole+meterpreter, so the two metasploit PDFs are not rewritten." - }, - { - "source": "repo", - "rel": "HTB/cheatsheet-stack-based-buffer-overflows-on-windows-x86.pdf", - "category": "exploitation", - "slug": "buffer-overflow", - "title": "Stack-Based Buffer Overflow (Win x86)", - "description": "Classic Windows x86 stack overflow: fuzzing, EIP control, bad chars, JMP ESP, shellcode.", - "tools": [ - "Immunity Debugger", - "mona.py", - "msfvenom" - ], - "tags": [ - "exploitation", - "buffer-overflow", - "windows", - "binary" - ], - "difficulty": "advanced", - "action": "rewrite_from_pdf", - "note": "PDF-only classic binary-exploitation topic with no markdown equivalent; worth converting." - }, - { - "source": "vault", - "rel": "PrivEsc/PrivEsc - Windows.md", - "category": "privilege-escalation", - "slug": "windows-privesc", - "title": "Windows Privilege Escalation", - "description": "Windows privesc master guide: token/privilege abuse, services, registry, AlwaysInstallElevated, potatoes.", - "tools": [ - "winPEAS", - "PowerUp", - "JuicyPotato" - ], - "tags": [ - "privilege-escalation", - "windows", - "post-exploitation" - ], - "difficulty": "advanced", - "action": "port", - "note": "118-code master guide chosen over the two other Windows privesc dumps; single canonical page." - }, - { - "source": "vault", - "rel": "PrivEsc/Linux PrivEsc Cheat Sheet.md", - "category": "privilege-escalation", - "slug": "linux-privesc", - "title": "Linux Privilege Escalation", - "description": "Linux privesc quick-reference: sudo, SUID/SGID, cron, LD_PRELOAD, LXD, NFS, capabilities and kernel.", - "tools": [ - "linPEAS", - "pspy", - "GTFOBins" - ], - "tags": [ - "privilege-escalation", - "linux", - "post-exploitation" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Only Linux privesc candidate; dense command-table checklist (0 fenced code because it is a table)." - }, - { - "source": "vault", - "rel": "PasswordAttacks/hashcat-cheatsheet.md", - "category": "password-attacks", - "slug": "hashcat", - "title": "Hashcat", - "description": "Hashcat cracking: attack modes, hash-mode selection, rules, masks, wordlists and performance tuning.", - "tools": [ - "Hashcat" - ], - "tags": [ - "password-attacks", - "cracking", - "hashes" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Structured sheet (11 H2/12 code) preferred over the Tools copy and the raw hashcat-modes table." - }, - { - "source": "vault", - "rel": "PasswordAttacks/john-cheatsheet.md", - "category": "password-attacks", - "slug": "john-the-ripper", - "title": "John the Ripper", - "description": "John the Ripper: 2john extractors, formats, wordlist/incremental/rules modes and session control.", - "tools": [ - "John the Ripper" - ], - "tags": [ - "password-attacks", - "cracking", - "hashes" - ], - "difficulty": "intermediate", - "action": "port", - "note": "13KB markdown beats the john PDF; PDF rewrite unnecessary." - }, - { - "source": "repo", - "rel": "Password-Attacks/Password_Attacks_Cheat_Sheet.pdf", - "category": "password-attacks", - "slug": "password-attacks", - "title": "Password Attacks & Brute Forcing", - "description": "Online/offline password attacks: Hydra/Medusa service brute-forcing, spraying, mutations and defaults.", - "tools": [ - "Hydra", - "Medusa", - "CrackMapExec" - ], - "tags": [ - "password-attacks", - "brute-force", - "spraying" - ], - "difficulty": "intermediate", - "action": "rewrite_from_pdf", - "note": "PDF-only; covers network brute-forcing/spraying not in the hashcat/john offline-cracking sheets." - }, - { - "source": "vault", - "rel": "Tools/Ligolo-ng Cheat sheet.md", - "category": "tunneling-pivoting", - "slug": "ligolo-ng", - "title": "Ligolo-ng", - "description": "Ligolo-ng tunneling: agent/proxy setup, interface routing, double/multi-hop pivots and listeners.", - "tools": [ - "Ligolo-ng" - ], - "tags": [ - "pivoting", - "tunneling", - "network" - ], - "difficulty": "intermediate", - "action": "port", - "note": "55KB markdown supersedes the two Ligolo PDFs; PDF rewrite unnecessary." - }, - { - "source": "vault", - "rel": "Misc/Tunneling.md", - "category": "tunneling-pivoting", - "slug": "tunneling-tools", - "title": "Tunneling Tools", - "description": "Chisel, socat, plink and SSH tunneling patterns for port forwarding and pivoting through hosts.", - "tools": [ - "Chisel", - "socat", - "plink", - "SSH" - ], - "tags": [ - "pivoting", - "tunneling", - "port-forwarding" - ], - "difficulty": "advanced", - "action": "port", - "note": "Comprehensive multi-tool tunneling (35KB/93code); chosen over the narrower Pivoting & Tunnelling sheet." - }, - { - "source": "vault", - "rel": "Misc/SSH Portfwding with metasploit .md", - "category": "tunneling-pivoting", - "slug": "ssh-tunneling", - "title": "SSH Tunneling & Port Forwarding", - "description": "SSH local/remote/dynamic forwarding and Metasploit route/portfwd pivoting, worked end to end.", - "tools": [ - "SSH", - "Metasploit" - ], - "tags": [ - "pivoting", - "ssh", - "port-forwarding" - ], - "difficulty": "intermediate", - "action": "port", - "note": "SSH+Metasploit focus distinct from the general tunneling and Ligolo pages." - }, - { - "source": "vault", - "rel": "Cryptography/GPG - Cheatsheet markdown.md", - "category": "cryptography", - "slug": "gpg", - "title": "GPG", - "description": "GnuPG keys, encryption/decryption, signing/verification, keyservers, trust and revocation.", - "tools": [ - "GPG", - "GnuPG" - ], - "tags": [ - "cryptography", - "encryption", - "pgp" - ], - "difficulty": "intermediate", - "action": "port", - "note": "75KB/154code markdown beats both GPG PDFs; PDF rewrite unnecessary." - }, - { - "source": "repo", - "rel": "Misc/openssl-cheatsheet.pdf", - "category": "cryptography", - "slug": "openssl", - "title": "OpenSSL", - "description": "OpenSSL: keys, CSRs, certs, x509 inspection, PEM/DER conversion, s_client and encryption.", - "tools": [ - "OpenSSL" - ], - "tags": [ - "cryptography", - "tls", - "certificates" - ], - "difficulty": "intermediate", - "action": "rewrite_from_pdf", - "note": "PDF-only crypto staple with no markdown equivalent." - }, - { - "source": "vault", - "rel": "DFIR/Forensics Cheatsheet.md", - "category": "dfir", - "slug": "forensics", - "title": "Digital Forensics", - "description": "Cross-platform DFIR reference: acquisition, triage, artifacts, timelines and analysis commands.", - "tools": [ - "Autopsy", - "Sleuth Kit", - "plaso" - ], - "tags": [ - "dfir", - "forensics", - "incident-response" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Broadest DFIR sheet (73KB/60code); general forensics anchor." - }, - { - "source": "vault", - "rel": "Tools/CMD-Powershell Cheat Sheet.md", - "category": "tools", - "slug": "windows-cmd-powershell", - "title": "Windows CMD & PowerShell", - "description": "Windows pentest command reference: recon, users/groups, networking, downloads and PowerShell one-liners.", - "tools": [ - "cmd", - "PowerShell" - ], - "tags": [ - "windows", - "post-exploitation", - "commands" - ], - "difficulty": "intermediate", - "action": "port", - "note": "71KB/161code superset; dedupes the standalone Powershell.md." - }, - { - "source": "vault", - "rel": "Tools/fscan.md", - "category": "tools", - "slug": "fscan", - "title": "fscan", - "description": "fscan all-in-one intranet scanner: host/port discovery, service brute-forcing and vuln checks.", - "tools": [ - "fscan" - ], - "tags": [ - "scanning", - "enumeration", - "internal" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Distinct all-in-one internal scanner (43KB)." - }, - { - "source": "vault", - "rel": "Tools/EyeWitness-Cheatsheet.md", - "category": "tools", - "slug": "eyewitness", - "title": "EyeWitness", - "description": "EyeWitness bulk web/RDP/VNC screenshotting and reporting for rapid visual recon.", - "tools": [ - "EyeWitness" - ], - "tags": [ - "recon", - "screenshots", - "web" - ], - "difficulty": "beginner", - "action": "port", - "note": "Only screenshot-recon tool sheet." - }, - { - "source": "vault", - "rel": "ActiveDirectory/Snaffler.md", - "category": "tools", - "slug": "snaffler", - "title": "Snaffler", - "description": "Snaffler share-crawling for credentials, keys and sensitive files across SMB with tuning rules.", - "tools": [ - "Snaffler" - ], - "tags": [ - "credentials", - "shares", - "enumeration" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Rich share/credential hunting tool (28KB/38code)." - }, - { - "source": "vault", - "rel": "ActiveDirectory/SharpSploit.md", - "category": "tools", - "slug": "sharpsploit", - "title": "SharpSploit", - "description": "SharpSploit .NET post-exploitation library: execution, credentials, enumeration and evasion APIs.", - "tools": [ - "SharpSploit" - ], - "tags": [ - "post-exploitation", - "dotnet", - "offensive" - ], - "difficulty": "advanced", - "action": "port", - "note": "Rich .NET tradecraft sheet (28KB); distinct from GUI/CLI tools." - }, - { - "source": "vault", - "rel": "Misc/tmux.md", - "category": "linux-it", - "slug": "tmux", - "title": "tmux", - "description": "tmux sessions, windows, panes, copy mode and config bindings for terminal multiplexing.", - "tools": [ - "tmux" - ], - "tags": [ - "linux", - "terminal", - "productivity" - ], - "difficulty": "beginner", - "action": "port", - "note": "IT staple named in the brief." - }, - { - "source": "vault", - "rel": "Git/git-cheatsheet.md", - "category": "git-workflow", - "slug": "git", - "title": "Git", - "description": "Everyday Git: staging, commits, remotes, log/diff, stash, merge/rebase and recovery.", - "tools": [ - "git" - ], - "tags": [ - "git", - "version-control", - "workflow" - ], - "difficulty": "beginner", - "action": "port", - "note": "Most comprehensive Git sheet (31KB/46code); core Git reference." - }, - { - "source": "vault", - "rel": "Git/Branches Expanded.md", - "category": "git-workflow", - "slug": "git-branching", - "title": "Git Branching", - "description": "Branch workflows: create/switch, track remotes, merge vs rebase, and moving edits between branches.", - "tools": [ - "git" - ], - "tags": [ - "git", - "branching", - "workflow" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Best-structured branching deep-dive (9 H2); dedupes Branches.md and the Vault-management branch guide." - }, - { - "source": "vault", - "rel": "Git/Resetting.md", - "category": "git-workflow", - "slug": "git-reset", - "title": "Git Reset & Undo", - "description": "Undo work safely: reset soft/mixed/hard, restore, revert, reflog recovery and stash rescue.", - "tools": [ - "git" - ], - "tags": [ - "git", - "undo", - "recovery" - ], - "difficulty": "intermediate", - "action": "port", - "note": "Distinct undo/recovery focus not covered by the main Git sheet." - } - ] -} -\ No newline at end of file diff --git a/docs/superpowers/specs/2026-08-09-daemon-sec-cheatsheet-site-design.md b/docs/superpowers/specs/2026-08-09-daemon-sec-cheatsheet-site-design.md @@ -1,137 +0,0 @@ -# DΓ†MON//SEC β€” Cyber & IT Cheatsheet Vault β€” Design Spec - -**Date:** 2026-08-09 -**Status:** Approved -**Owner:** DAΞMON (Daemon-AT4) - -## Purpose - -A GitHub Pages website that is a curated, high-polish public vault of the best IT -and cybersecurity cheatsheets. Sourced from the local NetrunnerVault cheatsheet -collection (`/Volumes/bmdrbeKUVgvV/NetrunnerVault/02Cybersecurity/Cheatsheets`, -224 markdown + 14 PDF + 6 SVG) plus the `Ethical_Hacking-Cheatsheets` working -repo. Curated to ~60 canonical pages, each modernized before first deploy. - -## Success criteria - -- Live at `https://daemon-at4.github.io/daemon-sec` via GitHub Actions. -- ~60 curated cheatsheets, deduplicated, each with normalized frontmatter and - reviewed/modernized content (current flags, versions, CVE refs). -- Distinctive visual identity: Rose Pine (dark, default) + Rose Pine Dawn (light), - Apple-glass frosted panels, restrained cyberpunk neon accents. -- Instant offline fuzzy search (Pagefind), keyboard shortcut `/`. -- Fully responsive, WCAG AA contrast both themes, respects reduced-motion. - -## Stack - -- **Astro 5** static site (TypeScript, content collections). -- **Pagefind** for build-time search index (offline, no server). -- **Shiki** syntax highlighting with a rose-pine code theme. -- Self-hosted fonts (Chakra Petch / Inter / JetBrains Mono). -- **GitHub Actions β†’ Pages**. Project page, base path `/daemon-sec/`. - -## Design system - -### Palette β€” Rose Pine + Rose Pine Dawn -Dark (default, Rose Pine): -`base #191724, surface #1f1d2e, overlay #26233a, muted #6e6a86, subtle #908caa, -text #e0def4, love #eb6f92, gold #f6c177, rose #ebbcba, pine #31748f, -foam #9ccfd8, iris #c4a7e7, highlight #403d52` - -Light (Rose Pine Dawn): -`base #faf4ed, surface #fffaf3, overlay #f2e9e1, muted #9893a5, subtle #797593, -text #575279, love #b4637a, gold #ea9d34, rose #d7827e, pine #286983, -foam #56949f, iris #907aa9, highlight #dfdad9` - -Delivered as CSS custom properties on `:root` (light) and `:root[data-theme]` -overrides. Toggle persisted to `localStorage`, honors `prefers-color-scheme`. -Dark is the default. - -### Apple glass -Frosted translucent panels: `backdrop-filter: blur(16px) saturate(140%)`, -semi-transparent surface fill, hairline 1px border with low-alpha highlight, -layered box-shadow for depth, subtle inner top highlight. Applied to header, -nav rail, cards, search modal. - -### Cyberpunk accents (restrained) -- Neon edge-glow on hover using palette accents (rose/iris/foam/gold). -- Faint scanline + grid texture on the hero only. -- Animated gradient borders on featured/hover cards. -- Subtle noise grain overlay at very low opacity. -- Monospace command blocks with a terminal-style top bar + copy button. -All motion gated behind `prefers-reduced-motion`. - -### Typography -- Display: Chakra Petch (cyber geometric). Body: Inter. Code: JetBrains Mono. -- Self-hosted woff2 in `public/fonts`, `font-display: swap`. - -## Content pipeline - -1. **Curate** (~60): score all 224 vault sheets, select one canonical page per - topic, resolve duplicates (keep the richer copy), include IT staples - (git, tmux, linux find, chmod, macOS terminal). Output: selection manifest. -2. **Ingest**: copy selected markdown into `src/content/sheets/<category>/`, - normalize frontmatter: `title, description, category, tags, tools, difficulty, - updated, source`. Strip Obsidian-only syntax; rewrite wikilinks/attachment - embeds to site-relative or drop. -3. **PDF-only best content** (netexec, rustscan, ffuf, HTB sheets): rewrite into - native themed markdown pages (searchable, on-theme). Any that cannot be - cleanly extracted β†’ embedded PDF viewer + download link in `public/pdfs`. -4. **Modernize**: one pass per selected sheet β€” update stale flags, tool - versions, CVE references, fix broken commands, add common missing usage. - **Guardrails:** never fabricate flags/CVEs; if unsure keep the original and - flag it; preserve author voice. Legal note: content is for authorized - testing / education (matches existing repo framing). - -## Taxonomy & pages - -Categories (from folder tree, ~12): Active Directory, Enumeration, Exploitation, -Privilege Escalation, Password Attacks, Web, Tunneling & Pivoting, Cryptography, -DFIR, Tools, Linux & IT, Git & Workflow. - -- **Landing**: hero (glass + scanline) β†’ search β†’ glass category grid β†’ featured. -- **Category page**: filtered card grid, count, description. -- **Sheet page**: sticky TOC rail, copy-code buttons, updated/source/difficulty - badges, tag pills, prev/next, breadcrumb. -- **Tag index** + **Tool index** cross-reference pages. -- Themed **404**. Global search modal (`/`). Theme toggle in header. - -## Repo structure - -``` -daemon-sec-vault/ # local; GitHub repo = Daemon-AT4/daemon-sec - .github/workflows/deploy.yml - astro.config.mjs Β· package.json Β· tsconfig.json - scripts/ingest.mjs # copy + normalize curated files from vault - content-manifest.json # curation selection + dedupe decisions - src/ - content/sheets/<cat>/*.md # curated, modernized content - content/config.ts # collection schema (zod) - components/ (GlassCard, CodeBlock, ThemeToggle, SearchModal, TOC, NavRail, - CategoryBadge, TagPill, Hero, Footer, Prose) - layouts/ (Base.astro, Sheet.astro) - pages/ (index, [category]/index, sheets/[...slug], tags, tools, 404) - styles/ (tokens.css, glass.css, global.css, prose.css) - lib/ (nav.ts, taxonomy.ts) - public/ fonts/ pdfs/ svgs/ favicon.svg og image -``` - -## Execution phases (multi-agent) - -- **A β€” Curate**: agents score & select ~60 β†’ `content-manifest.json`. -- **B β€” Modernize**: pipeline, ~1 agent per selected sheet β†’ cleaned + updated - markdown with normalized frontmatter. Human-reviewed batch. -- **C β€” Scaffold**: build Astro app, theme, components, pages (parallel with A/B). -- **D β€” Integrate & deploy**: assemble content, `astro build` + Pagefind, create - `Daemon-AT4/daemon-sec`, push, enable Pages, verify live. - -## Decisions locked - -- Engine: Astro + Pagefind. Scope: curated ~60 tight. Deploy: new public repo - `Daemon-AT4/daemon-sec`. Content updates: enhance-during-build. -- Default theme: dark Rose Pine. PDFs: rewrite to markdown where feasible. - -## Out of scope (v1) - -- Custom domain. Comments/analytics. Auth. Non-cyber IT beyond staples. -- Full 224-page ingest (deferred; can expand later from manifest). diff --git a/docs/superpowers/specs/2026-09-28-ired-team-integration-design.md b/docs/superpowers/specs/2026-09-28-ired-team-integration-design.md @@ -1,367 +0,0 @@ -# ired.team Integration β€” Design - -**Date:** 2026-09-28 -**Upstream:** [`mantvydasb/RedTeaming-Tactics-and-Techniques`](https://github.com/mantvydasb/RedTeaming-Tactics-and-Techniques) β€” the source repository behind [ired.team](https://ired.team) -**Author:** Mantvydas Baranauskas ([@mantvydasb](https://github.com/mantvydasb)) -**Pinned at:** `8cdbdd60eb4a8997e689649f3911f7c893e59ed9` (2026-06-13) -**Licence:** **none published.** The GitHub API returns `"license": null`; there is no `LICENSE` -file and `/license` returns 404. - ---- - -## 1. Intent - -Bring the technique coverage of ired.team onto DΓ†MON//SEC **without giving it a section of its -own**. ired.team material is either folded into the sheet that already covers the topic, or becomes -a new sheet filed under an existing category. No entry is added to `taxonomy.ts`; no `/ired` route -exists; the site's 13 tabs stay 13 tabs. - -Success looks like: an operator searching the site for "process hollowing" or "AMSI bypass" finds a -DΓ†MON//SEC sheet in the house voice, with the screenshots that make the technique legible, and a -credit line that sends them to ired.team for the full lab write-up. - -### What the user asked for, verbatim - -> "so hacktricks has been added can you add this as well https://www.ired.team dont make it have its -> own sections just read the guides and add them to the site or add to the owns that it goes over." - -> "use the images as well and credit the images" - -### Decisions taken by the user during brainstorming - -| Question | Decision | -|---|---| -| Placement | **Hybrid** β€” merge into sheets that already cover the topic; new sheets under existing categories where nothing does | -| Text fidelity | **Rewritten** in the DΓ†MON//SEC sheet voice, credited as `derived` | -| First-pass scope | **Curated, ~40–50 topics**, delivered in reviewable batches | -| Images | **Hotlinked** at the pinned SHA, per-image credit, nothing copied into the repo | - -### Assumptions - -- "Don't make it have its own sections" governs site navigation, so `subcategory` frontmatter is the - organising tool rather than a new category. Existing category pages already render subcategories - (Active Directory uses 11 of them), so this needs no template work. -- The curated ~45 are chosen for *capability the site does not currently have*, not for upstream - page length. - ---- - -## 2. Licence position - -This is the constraint that shapes everything else, and the repo has already ruled on the -identical situation twice. - -`src/content.config.ts` on the `upstreamLicense` enum: - -> `none` means the upstream publishes no licence at all β€” that grants no right to copy, so such a -> sheet must be a link-only stub, not a mirror. - -`docs/provenance-audit.md` on `enumeration/awesome-nmap-grep.md`, an unlicensed upstream: - -> No grant of rights exists, so republication is not permitted by any licence. […] Adding a credit -> line alone does **not** make this compliant. - -Therefore, for ired.team: - -| | Handling | -|---|---| -| Prose | **Not copied.** Written fresh in the house voice. ired.team's guides are first-person lab narrative ("This lab is my attempt to…", shout-outs to collaborators); a command-first cheatsheet is a different work, not a reformat of that one. | -| Commands, API sequences, struct layouts, registry paths | Reproduced as the technical facts they are. These are not the upstream's creative expression, and most are not the upstream's inventions either β€” the README says so: "Most of these techniques are discovered by other security researchers and I do not claim their ownership." | -| Screenshots | **Hotlinked** at the pinned SHA. Referenced, never reproduced on our domain. 3–6 per sheet, each captioned and credited. | -| Credit | Mandatory on every sheet that draws on ired.team, plus a provenance plate on `/credits`. | - -The HackTricks approach β€” adapted mirror under CC BY-NC β€” is **not available here**, because -HackTricks publishes a licence and ired.team does not. - -### What this does not claim - -Hotlinking is chosen because it avoids reproduction, not because it is a licence. If Mantvydas -Baranauskas asks for the screenshots or the derived sheets to come down, they come down. The -`/credits` plate names him and links the upstream so that request is easy to make. - ---- - -## 3. Provenance model - -### 3.1 Problem - -`SheetCredit` renders one upstream per sheet, driven by six flat frontmatter fields. That is -sufficient for a new sheet derived from one ired.team guide, but it cannot express a *merged* sheet: -`active-directory/active-directory-cheat-sheet.md` must credit S1ckB0y1337 under MIT (per the -provenance audit) **and** ired.team with no licence. One slot, two upstreams. - -### 3.2 Schema change - -Add an optional `references` array to the `sheets` collection. Generic by design β€” merging any -future third-party material reuses it. - -```ts -references: z - .array( - z.object({ - name: z.string(), - url: z.string().url(), - author: z.string().optional(), - license: z - .enum(['MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', 'CC-BY-NC-4.0', 'none', 'proprietary']) - .optional(), - relation: z.enum(['verbatim', 'derived', 'inspired', 'link-only']).optional(), - // What this source contributed, so the credit is specific rather than decorative. - note: z.string().optional(), - }), - ) - .default([]), -``` - -No existing field changes and no existing sheet is touched by the schema edit, so the 264 sheets on -disk keep validating. - -### 3.3 Which mechanism a sheet uses - -| Sheet kind | Credit mechanism | -|---|---| -| New sheet, substance from one ired.team guide | Existing flat fields: `upstreamName: "ired.team"`, `upstreamUrl`, `upstreamAuthor: "Mantvydas Baranauskas"`, `upstreamLicense: none`, `upstreamRelation: derived`. `SheetCredit` already renders this as "Derived from ired.team by **Mantvydas Baranauskas** Β· no licence published Β· credits". | -| New sheet drawing on several guides | Flat fields naming ired.team, plus `references` entries for the specific guides. | -| Existing sheet gaining an ired.team-derived section | `references` entry only. Flat fields stay as they are, so prior attribution is preserved. | - -### 3.4 New component β€” `src/components/SheetReferences.astro` - -Renders a **Sources & further reading** plate at the foot of the article when `references` is -non-empty. Reuses the `.patt-credit` visual language already shared by `SheetCredit`, -`PayloadCredit` and `HackTricksCredit`: square plate, corner brackets, left accent bar, JetBrains -Mono micro-labels, cream-on-night inversion. One list row per reference β€” relation badge, linked -name, author, licence label, `note`. - -Licence wording follows `SheetCredit` exactly, including `none` β†’ "no licence published", so the two -panels cannot disagree about the same upstream. - -Mounted in `src/pages/sheets/[...slug].astro` after `<Content />`, inside the `<article>`. - ---- - -## 4. Images - -### 4.1 URL form - -``` -https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/<percent-encoded filename> -``` - -Pinning to the SHA rather than `master` means an upstream reorganisation cannot silently swap or -break the image a caption describes. Asset filenames routinely contain spaces and parentheses -(`Screenshot from 2019-04-28 16-28-59.png`), so every path segment is percent-encoded; -`fetch-ired.py` emits the finished URL so this is never done by hand. - -### 4.2 Markup - -Sheets are plain `.md`, so an Astro component is not available inline. Raw HTML passes through -Markdown, so each screenshot is a `<figure>`: - -```html -<figure class="shot"> - <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Screenshot%20from%202019-04-28%2016-36-33.png" - alt="WinDBG showing the host process PEB at 0100e000 with ImageBaseAddress eight bytes in" - loading="lazy" referrerpolicy="no-referrer"> - <figcaption>Reading <code>ImageBaseAddress</code> from the host PEB in WinDBG. - <span class="shot-credit">ired.team Β· Mantvydas Baranauskas</span> - </figcaption> -</figure> -``` - -Rules: -- `alt` describes what the shot *shows*, for screen readers and for the case where the hotlink dies. -- `loading="lazy"` β€” a sheet with six remote screenshots must not block first paint. -- `referrerpolicy="no-referrer"` β€” don't leak reader browsing to GitHub. -- `.shot-credit` on **every** figure. Credit is per-image, as requested, not one blanket line. - -### 4.3 CSS - -Add `.shot`, `.shot img`, `.shot figcaption`, `.shot-credit` to `src/styles/prose.css`, matching the -existing `figure.code-pane` treatment: square corners, 1px rule, caption in the terminal font at -micro-label size, `.shot-credit` in `--fg-faint`. Both themes, since `prose.css` already carries the -cream/night pairs. - -### 4.4 Selection - -3–6 screenshots per sheet β€” the ones that carry information the text cannot (debugger state, a -detection artefact in Process Hacker, a SysInternals view). Upstream pages with 30+ near-identical -shots get the decisive few. This keeps pages light and keeps the use clearly illustrative. - ---- - -## 5. Placement map - -No new `taxonomy.ts` entries. `subcategory` does the grouping. - -| ired.team cluster | Guides | Category | Subcategory | Mode | -|---|---|---|---|---| -| `code-injection-process-injection` | 41 | `exploitation` | Process Injection | new sheets | -| `defense-evasion` | 32 | `exploitation` | Defense Evasion | new sheets | -| `persistence` | 25 | `exploitation` | Persistence | new sheets | -| `code-execution` | 21 | `exploitation` | Code Execution | new sheets | -| `initial-access` | 16 | `exploitation` | Initial Access | new sheets | -| `credential-access-and-credential-dumping` | 22 | `password-attacks` | Credential Dumping | new sheets + merge into the two credential-hunting sheets | -| `active-directory-kerberos-abuse` | 29 | `active-directory` | Kerberos & Delegation *(existing)* | merge β€” AD is already 136 sheets deep here | -| `lateral-movement` | 22 | `active-directory` | Lateral Movement *(existing)* | new sheets + merge | -| `privilege-escalation` | 10 | `privilege-escalation` | β€” | merge into the two existing Windows/Linux sheets | -| `red-team-infrastructure` | 8 | `tunneling-pivoting` | Red Team Infrastructure | new sheets | -| `enumeration-and-discovery` | 10 | `enumeration` | β€” | merge into `windows-enumeration.md` | -| `windows-kernel-internals`, `windows-kernel`, ETW, PEB, `get-injectedthread` | 42 | `dfir` | Windows Internals | new sheets β€” detection-side framing | - -`exploitation` grows from 4 sheets to roughly 30 across batches 1–3, which is the single biggest gain: the category's -own blurb promises "injection, upload, and shell delivery" and currently ships four sheets, none of -which is about injection. - -### Why not a new category - -A "Post-Exploitation" or "Red Team" tab would be the natural taxonomy for injection + evasion + -persistence. The user ruled it out explicitly. `subcategory` gives the same reading experience on -the category page without adding a tab, so the constraint costs nothing structurally. Noting it here -because if the user later changes their mind, the move is a frontmatter rename, not a rewrite. - ---- - -## 6. Tooling - -### 6.1 `scripts/fetch-ired.py` - -Read-only research helper. **It never writes to `src/`.** Content is written by hand; this script -only makes reading the upstream cheap and makes image URLs correct. - -``` -usage: fetch-ired.py [--list] [--tree] [--get PATH ...] [--images PATH] - [--out DIR] [--refresh-sha] -``` - -- `--list` / `--tree` β€” the 287 guide paths, grouped by cluster. -- `--get PATH` β€” fetch a guide at the pinned SHA into `--out` (default: the session scratchpad), - with GitBook macros translated so the text is readable: `{% hint style=X %}` β†’ a blockquote, - `{% content-ref %}` β†’ a plain link, `{% code %}` / `{% endcode %}` stripped, `{% embed %}` β†’ link. -- `--images PATH` β€” print every image referenced by that guide as a finished, percent-encoded, - SHA-pinned URL ready to paste into a `<figure>`. -- Pins from `src/data/ired-source.json`; `--refresh-sha` re-pins and rewrites that file. - -Placed in `scripts/` beside `sync-hacktricks.py`, but deliberately *not* named `sync-*`: the -`sync-*` scripts generate content collections, and this one must never be mistaken for one. - -### 6.2 `src/data/ired-source.json` - -```json -{ - "repo": "mantvydasb/RedTeaming-Tactics-and-Techniques", - "site": "https://ired.team", - "author": "Mantvydas Baranauskas", - "sha": "8cdbdd60eb4a8997e689649f3911f7c893e59ed9", - "fetched": "2026-09-28", - "license": null -} -``` - -One place to bump the pin. `credits.astro` reads it so the displayed commit cannot drift from the -commit the image URLs use. - -### 6.3 `src/pages/credits.astro` - -New ired.team section, placed after HackTricks, following the existing `.slab.corners.provenance` -pattern. It must state plainly: - -- Upstream repo and site, author named. -- **Licence: none published.** No rights are granted to copy, so nothing is mirrored. -- Prose on DΓ†MON//SEC is original; commands and technique facts are reproduced as facts. -- Screenshots load from the upstream repository at a pinned commit and are **not** redistributed. -- A takedown sentence: if the author objects, the material is removed. - -### 6.4 `docs/provenance-audit.md` - -Append an ired.team row so the audit stays the single record of third-party content. It currently -says 75 sheets never entered triage; adding ~45 sheets without recording them would make that -honesty statement wrong. - ---- - -## 7. Sheet anatomy - -```markdown ---- -title: "Process Hollowing" -description: "Carving a suspended process's image out of memory and running a replacement PE in its place, plus the relocation fixups that make it work." -category: exploitation -subcategory: "Process Injection" -tags: [process-injection, evasion, windows, maldev] -tools: [windbg, visual-studio, process-hacker] -difficulty: advanced -updated: 2026-09-28 -upstreamName: "ired.team" -upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations" -upstreamAuthor: "Mantvydas Baranauskas" -upstreamLicense: none -upstreamRelation: derived -references: - - name: "Process-Hollowing" - url: "https://github.com/m0n0ph1/Process-Hollowing" - author: "m0n0ph1" - relation: inspired - note: "The reference implementation ired.team's lab works from." ---- - -## What it does -## Prerequisites -## Walkthrough ← commands / API sequence + the 3–6 credited figures -## Detection ← what defenders see; DΓ†MON//SEC's own addition -## References -``` - -House-style requirements: -- Every code fence carries a language. `validate-content.py` warns on bare fences and the baseline - already has 100+ such warnings; this work must not add to them. -- A **Detection** section on every offensive sheet. ired.team is attacker-side notes; the detection - framing is original DΓ†MON//SEC value and is also what makes these derived works rather than - restatements. -- Internal cross-links use `/sheets/<category>/<slug>` β€” `validate-content.py` resolves these - against files on disk and fails on a broken or self-referential one. -- No `[[wikilinks]]`, no `![[embeds]]`, no `%%comments%%`. - ---- - -## 8. Verification - -| Gate | Command | Pass condition | -|---|---|---| -| Content validator | `python3 scripts/validate-content.py` | **No new `ISSUES`** vs. the recorded baseline. Baseline on `main` is already non-zero: `MISSING: exploitation/shell-stabilization` plus 201 `EXTRA` and ~113 fence warnings. New sheets appear under `EXTRA`, which is informational (`ok = not missing and not issues`). | -| Build + tests | `npm run test` (`astro build && node --test`) | Green, including `test/internal-links.test.mjs`. | -| Image spot check | `curl -sIL <figure src>` on a sample from each batch | `200` and an `image/*` content type. | -| Rendered page | Build, then read one new sheet and one merged sheet | Credit panel, references plate and figures all render; captions carry credit; both themes. | - -The pre-existing baseline failure is recorded rather than fixed: `content-manifest.json` is stale by -200 sheets and repairing it is unrelated to this work. It is called out so a reviewer does not read -a red validator as regression. - ---- - -## 9. Delivery - -~45 topics in batches of 8–10. Each batch is one jj change on its own bookmark off `main` β€” per the -user's standing rule, nothing is pushed to `main` directly. - -| Batch | Content | Why this order | -|---|---|---| -| 0 | Schema `references`, `SheetReferences.astro`, `prose.css` figure styles, `fetch-ired.py`, `ired-source.json`, `credits.astro` plate | Plumbing first, so batch 1 proves the whole pipeline end to end | -| 1 | Process injection β€” ~9 sheets | Biggest gap on the site; exercises figures hardest (debugger-heavy) | -| 2 | Defense evasion β€” ~9 sheets | AMSI, ETW, unhooking | -| 3 | Persistence + code execution β€” ~10 sheets | | -| 4 | Credential dumping + privesc β€” ~9 sheets, includes the first merges | Merge path and `references`-only credit get exercised | -| 5 | Lateral movement, red team infra, Windows internals, enumeration merges β€” ~9 sheets | | - -Batch 0 ships with batch 1 so there is something to look at. - ---- - -## 10. Out of scope - -- The other ~240 upstream guides. The tooling and credit plumbing handle them; only the writing - remains, and it can continue in later batches. -- Repairing the stale `content-manifest.json` or the ~113 pre-existing bare-fence warnings. -- The four open items in `docs/provenance-audit.md` Β§2 (awesome-nmap-grep, the two HTB sheets, - the S1ckB0y1337 MIT notice). Unrelated to ired.team and each needs a decision from the user. -- The OSINT section requested mid-session (Bellingcat toolkit + tools.osintnewsletter.com). That one - *does* want its own tab, so it gets its own design pass after this work lands. diff --git a/docs/superpowers/specs/2026-10-04-signal-field-banners-design.md b/docs/superpowers/specs/2026-10-04-signal-field-banners-design.md @@ -1,305 +0,0 @@ -# Signal field on the section banners β€” Design - -**Date:** 2026-10-04 -**Source:** `/Users/daemon1/git/daemon-sec/client/src/components/daemon/ambient-signal.tsx` (716 lines, React + WebGL) -**Prior audit:** [`docs/design-pass-findings.md`](../../design-pass-findings.md) Β§1 -**Bookmark:** `signal-field-banners`, off `main` - ---- - -## 1. Intent - -Bring daemon-sec's interactive dot field β€” `ambient-signal.tsx` at `DEFAULT_STYLE = 2`, the -"constellation" β€” onto the cheatsheet, which is Astro 7 with no React and a contract -(`DESIGN.md`) forbidding new dependencies. - -The field is a network map: hosts on an irregular lattice that drift on their own and are pushed -away from the pointer. Hosts within the cursor's reach lock to the archive's REC red and the links -between them light, so pointing at the page draws an attack path across it. In style 2 the dots are -dense and plain, links appear *only* near the cursor, and the cursor reaches out to them. - -Success looks like: landing on any category page, the dark banner plate carries a lattice of faint -dots, and moving the pointer across it draws lit paths that settle when the pointer leaves β€” with -the banner title no less legible than it is today, and no regression to the fuzz field already -painting that plate. - -### What the user asked for, verbatim - -> "Port daemon-sec's interactive dot field to the daemon-sec-cheatsheet Astro site." - -> "Open question to settle with the user: dots on every page, or only the home hero and category -> banners?" - -On the composition question, asked during brainstorming: - -> "i dunno you choose whatever looks cooler" - -### Decisions taken during brainstorming - -| Question | Decision | -|---|---| -| Where it mounts | **Section banners only** β€” every route below home. Home page untouched. | -| Relationship to the existing fuzz field | **Both**, constellation behind, fuzz gain reduced on the banner so the lattice reads | -| Palette source | **Theme-relative tokens** read off the canvas (`--text`, `--*-soft`), not the absolute `--color-night-*` set | -| `tone` prop | **Dropped** β€” `.plate` already pins night for the subtree | -| Shader | **Lifted verbatim**, all four styles retained, `?field=N` override kept | -| Fallback | **No canvas** β€” the fuzz field carries the plate alone. `ambient-field.tsx` is not ported. | - -### Premises in the original task brief that did not survive contact with the repo - -Recorded because each one changed the design: - -1. **"`--color-dawn-*` / `--color-night-*` do not exist in the cheatsheet."** They do β€” - `src/styles/tokens.css` carries the whole "DΓ†MON main-site palette" block, extracted verbatim - from daemon-sec, and `HeroLoop.astro` already reads it. Only the three dawn `-soft` variants are - absent, and the original's `SOURCES` table already falls through to full strength when a `-soft` - is missing. The mapping problem the brief anticipated does not exist. Β§4 takes a different route - again, for reasons of its own. -2. **"`src/scripts/app.ts` drives SectionBanner, Marquee and Hero."** It does not. Each of those - components carries its own inline `<script>` with its own `astro:page-load` / `astro:before-swap` - pair; `app.ts` has no `before-swap` teardown at all. The pattern to follow is - `SectionBanner.astro:240-289`, not `app.ts`. -3. **"The home hero."** The home page uses `HeroLoop`, not `Hero`, and `HeroLoop` is already a - heavy canvas animation β€” an authored 1920Γ—1080 bitmap on a 21.4s rAF clock filling the whole - band. This is the main reason the scope question resolved to banners only. - -### Assumptions - -- The banner is near the top of every page it appears on, so the original's `u_scroll` term - (`window.scrollY / box.height`, written for a hero at page top) carries over unchanged. -- Nothing in the banner needs the `data-index-zone` clearing; see Β§6. -- No ambient switch exists on the cheatsheet (`motion-settings` is not ported), so - `prefers-reduced-motion` is the only motion gate. This satisfies DESIGN.md's "all motion gated on - `prefers-reduced-motion`". - ---- - -## 2. Why both fields, rather than a replacement - -The banner plate is already painted by the fuzz field (`SectionBanner.astro:79-290`): six -pixel-sorted streak bands plus 54 bright filaments on a seamless 9-second loop, composited -`lighter` on dark at `--fuzz-gain: 1`. - -Layered naively the two fight, and the arithmetic says so. Fuzz streak alpha reaches -`(0.06 + 0.34) Γ— env Γ— hx Γ— 1.7 Γ— gain` β€” around 0.6 at peak, *additive* β€” while the constellation -draws its dots at `hostA * 0.38` and its resting links at `linkA * 0.03`. The streaks would eat the -lattice, and the cursor lock, which is the piece's entire gesture, needs quiet ground to read -against. - -The fix is not to retire the fuzz field. DESIGN.md lists "`SectionBanner` canvas animation reading -`--fuzz-*` tokens" under **behaviour that must keep working (do not regress)**. Instead: -`--fuzz-gain` is already a CSS custom property the fuzz canvas re-reads at draw time -(`SectionBanner.astro:136`), so lowering it on `.banner` in CSS alone rebalances the two with no -JavaScript change and no edit to the fuzz code. The plate then reads as two depths β€” dots as the -substrate, streaks as interference drifting across them. - -A pointer-driven cross-fade was considered and rejected on a substantive point rather than cost: the -constellation has an idle wanderer (`IDLE_MS = 2800`) that walks the cursor on its own precisely so -touch screens still see hosts being locked. Gating the field on pointer-enter would suppress exactly -that behaviour. - ---- - -## 3. Module β€” `src/scripts/signal-field.ts` - -```ts -export function mountSignalField(canvas: HTMLCanvasElement): () => void -``` - -Returns its own teardown. Everything the React `useEffect` body did, minus React. - -`VERT` and `FRAG` are lifted **byte-identical** from `ambient-signal.tsx:75-340`. All four styles -stay in the fragment shader even though only style 2 ships, and `styleFromUrl()` (`?field=N`) comes -with them. Two reasons: the `st()` branches are uniform-constant, so they are coherent across every -fragment and cost effectively nothing on the GPU; and a byte-identical shader stays diffable against -the original when daemon-sec moves. Stripping three styles would save no measurable frame time and -would cost the port its provenance. - -Carried over unchanged from the original: - -- **DPR-1 backing store.** Lines are one pixel wide and fragment cost is per pixel. -- **No layout reads inside a frame.** The canvas box is measured by `ResizeObserver` into a `box` - record; pointer events are converted using that record, never a fresh `getBoundingClientRect`. -- **Pointer listened for on `window`**, not the canvas, so content in front keeps its hover. The - canvas itself takes `pointer-events: none`. -- **The 2800ms idle wanderer**, so touch screens see hosts being locked. -- **Eased cursor** at a 0.07 step per frame β€” weight, not lag. -- **`HELD_TIME = 31.0`** as the still-frame phase. - -Deliberately dropped: the `tone` prop and its `SOURCES` table (Β§4), the React `failed` state (Β§7), -and `AmbientField` (Β§7). - ---- - -## 4. Palette - -The field reads four colours off its own canvas with `getComputedStyle`, re-reading on -`daemonmodechange`, because a GPU uniform does not inherit a CSS variable. - -| Uniform | Token | Resolves to inside `.banner.plate` | -|---|---|---| -| `u_ink` | `--text` | `#e0def4` | -| `u_love` | `--love-soft` | `#eb6f92` | -| `u_foam` | `--foam-soft` | `#9ccfd8` | -| `u_iris` | `--iris-soft` | `#c4a7e7` | - -These are the **theme-relative** tokens, not the absolute `--color-night-*` set, and that is the -design decision this section exists to record. The banner carries `.plate`, which pins the night -palette for its whole subtree in both modes β€” so the cascade has already answered the question and -the canvas only has to ask the right element. Three consequences: - -- The `tone: "page" | "plate"` prop disappears. There is one tone here, and CSS decides it. -- No new tokens are needed. The three missing `--color-dawn-*-soft` variants stay missing. -- If the plate is ever retuned, or a banner is ever placed in a `.plate-dawn` scope, the field - follows without a code change. - -This is also exactly how the fuzz field already resolves its palette (`SectionBanner.astro:129-151` -and its comment), so the two canvases on the plate answer the colour question the same way. - -All four resolve to plain `#rrggbb`, which the original's `hexToRgb` requires β€” no `color-mix()` -values are involved. The fallbacks are the original's `FALLBACK` table, whose values are already -these exact night hexes. - ---- - -## 5. Mount β€” `SectionBanner.astro` - -Markup gains one element, as the **first** child so it sits at the bottom of the stack: - -```astro -<header class="banner plate bleed" style={`--acc: var(--${accent});`}> - <canvas class="banner__signal" data-signal aria-hidden="true"></canvas> <!-- NEW --> - <canvas class="banner__field" data-fuzz aria-hidden="true"></canvas> - <span class="banner__scrim" aria-hidden="true"></span> - <span class="grain grain--local" aria-hidden="true"></span> - <span class="scanlines" aria-hidden="true"></span> - <div class="wrap banner__body"> … </div> -``` - -Both canvases sit under the existing `.banner__scrim`, so the title's two-layer legibility story β€” -the fuzz canvas erasing its own top and bottom bands by `destination-out`, **and** the scrim -gradient over it β€” is untouched. The new field adds no third layer to that story and takes no part -in it. - -The component's existing `<style>` block gains: - -```css -/* The constellation is the substrate; the streaks are interference across - it. At full gain the additive streaks eat a lattice drawn at 0.38 alpha, - so the fuzz field steps back on the banner. Read at draw time by the - fuzz canvas, so this is the whole adjustment β€” no JS change. */ -.banner { --fuzz-gain: 0.45; } -.banner__signal { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; } -``` - -The `0.45` is a starting value to be confirmed against the rendered plate during implementation, in -both modes. It is the one number in this design arrived at by reasoning rather than by reading the -original. - -The component's `<script>` imports the module and wraps it in the lifecycle the fuzz script already -uses β€” `data-bound` guard, `astro:page-load` init, `astro:before-swap` teardown, plus the -`DOMContentLoaded` / `readyState` pair for the first load: - -```ts -import { mountSignalField } from '../scripts/signal-field'; - -function initSignal(): void { - document.querySelectorAll<HTMLCanvasElement>('[data-signal]:not([data-bound])').forEach((cv) => { - cv.dataset.bound = '1'; - const teardown = mountSignalField(cv); - document.addEventListener('astro:before-swap', teardown, { once: true }); - }); -} -``` - -Routes affected, via the seven pages that use `SectionBanner`: `/[category]`, `/payloads`, -`/internal`, `/tags`, `/tags/[tag]`, `/hacktricks`, `/404`. - ---- - -## 6. The clearing - -The `data-index-zone` mechanism β€” hosts inside a marked box pushed out through the nearest edge, -links and locks faded to nothing inside β€” is **kept in the module and left unused**. - -Nothing in the banner carries the attribute, so `u_zone` stays at its empty sentinel -`(0, 0, -1, -1)`, `zoneSd` returns `1e3` on its first line, and the keep-out costs one uniform and -one early return per fragment. - -It is kept rather than stripped for two reasons. It is load-bearing behaviour in the original, so -removing it would make the port no longer a port. And the home page already has a -`<section id="index" class="dsection" data-index>` that is the structural analogue of daemon-sec's -index rows β€” if the field is ever extended there (the "banners + home bands" option considered and -set aside during brainstorming), the clearing is what it will need, and it should not have to be -re-derived. - -In the banner itself the scrim already does this job, better and more cheaply. - ---- - -## 7. Motion, pausing and degradation - -**Reduced motion.** `prefers-reduced-motion: reduce` means the loop never starts. One frame is drawn -at `HELD_TIME = 31.0` β€” probes spread, hosts settled β€” and redrawn only on resize and on -`daemonmodechange`. A still frame has to be redrawn by hand on a mode flip, or the plate keeps the -old palette until something else resizes it. - -**Pausing.** The loop stops when the tab is hidden (`visibilitychange`) and when the banner is -scrolled out of view (`IntersectionObserver`), and restarts on either returning. Both route through -one `sync()`, as in the original. - -**Degradation.** No WebGL, a shader that will not compile, a program that will not link, or a -`webglcontextlost` event all end the same way: tear down and remove the canvas from the DOM. The -fuzz field is still painting the plate, so the banner degrades to exactly what ships today. - -This is the one place the port deliberately diverges in structure rather than only in idiom. The -original falls back to `ambient-field.tsx` (141 lines of CSS orbs, rings and drifting dots) because -on daemon-sec the field is the *only* thing on that ground and removing it would leave the hero -bare. Here the ground is already occupied. Porting a second fallback component to sit behind a -working animation would add 141 lines to render something nobody sees. - -**Context teardown.** The context is left to the GC rather than lost by hand, carrying over the -original's closing comment: a forced loss fires `webglcontextlost` *after* cleanup has run, and the -next mount of the same canvas would hear it as a real loss and degrade for no reason. This matters -more here than in the original, because view-transition navigation between two banner pages mounts -and unmounts in quick succession. - ---- - -## 8. Testing - -No test framework runs against components in this repo (`test/` holds content checks), so -verification is by inspection, and the spec names what to inspect rather than pretending otherwise. - -Type checking: `npx tsc --noEmit`. Permitted by DESIGN.md; `npm run build` is **not**, while other -agents share the working copy. - -Checked by hand on a category page, in both modes: - -1. Dots visible on the plate at rest; the lattice reads *through* the fuzz streaks rather than - under them. Tune `--fuzz-gain` here if not. -2. Pointer across the banner lights links and locks hosts to love; the field settles when it leaves. -3. Banner title legibility unchanged against `main` β€” the failure mode to look for is the title on a - bright filament, which is what the two-layer scrim exists to prevent. -4. Theme toggle repaints the field rather than leaving it on the previous palette, both while - animating and while held still under reduced motion. -5. Reduced motion (`Emulate CSS prefers-reduced-motion` in devtools) shows a still frame, not a - blank plate, and that frame repaints on a theme toggle. -6. Navigating between two banner pages under view transitions leaves exactly one rAF loop running β€” - no accumulation, no context-loss fallback triggered by the swap. -7. Scrolling the banner out of view and back, and backgrounding the tab, stop and restart the loop. -8. Touch: the idle wanderer walks the cursor and hosts lock without a pointer. -9. WebGL disabled (`chrome://flags`, or forcing the context to fail): the canvas is removed and the - banner looks like `main`. - ---- - -## 9. Files - -| File | Change | -|---|---| -| `src/scripts/signal-field.ts` | **New.** Shader, uniform wiring, lifecycle, `mountSignalField`. | -| `src/components/SectionBanner.astro` | Canvas element, two CSS rules, an import and the init block. | - -No token changes. No new dependencies. No edits to `src/content/`, `scripts/`, the manifests or -`astro.config.mjs`. diff --git a/live-root.png b/live-root.png Binary files differ. diff --git a/prompts/daemon-sec-content-expansion.md b/prompts/daemon-sec-content-expansion.md @@ -1,272 +0,0 @@ -<identity> -A red-team operator who maintains DΓ†MON//SEC and writes its sheets: command-first, no -narrative warm-up, the detection half included because a technique you cannot see is a -technique you cannot defend. Also an OSINT practitioner who has actually run the tools -being documented, and so writes the flag that matters and the failure mode that wastes -an afternoon, not the tool's own marketing line. -</identity> - -<purpose> -Two content passes on the DΓ†MON//SEC cheatsheet vault at -`/Users/daemon1/git/daemon-sec-cheatsheet`: - -1. Write batch 1 of the already-approved ired.team integration β€” ~9 process-injection - sheets that do not exist on the site today. -2. Convert the OSINT section from link catalogues into guides that show how the tools - are actually run. - -After reading a finished sheet, an operator should be able to execute the technique or -run the tool without opening the upstream source. -</purpose> - -<context> -## The repo - -Astro 7 static site, `npm run dev` / `npm run build`. Content lives in Astro content -collections under `src/content/`, schema in `src/content.config.ts`. 280 hand-authored -sheets under `src/content/sheets/<category>/`, across 14 categories. Nav and taxonomy are -driven by the `category` frontmatter, not the folder; second-level grouping is the optional -`subcategory` field, already used by Active Directory (136 sheets, 11 subcategories). - -Three further collections are generated mirrors and must not be hand-edited: -`payloads` (PayloadsAllTheThings, MIT), `internal` (InternalAllTheThings), -`hacktricks` (curated HackTricks mirror, CC BY-NC 4.0). - -## Task 1 β€” ired.team batch 1 - -The design is already written and approved. Read it in full before writing anything: -`docs/superpowers/specs/2026-09-28-ired-team-integration-design.md` (367 lines). - -Upstream is `mantvydasb/RedTeaming-Tactics-and-Techniques`, the repo behind -[ired.team](https://ired.team), by Mantvydas Baranauskas, pinned at -`8cdbdd60eb4a8997e689649f3911f7c893e59ed9`. The pin lives in `src/data/ired-source.json`. - -**The licence position is the constraint that shapes everything.** ired.team publishes no -licence: the GitHub API returns `"license": null`, there is no `LICENSE` file, `/license` -404s. `src/content.config.ts` already rules on this situation β€” "`none` means the upstream -publishes no licence at all β€” that grants no right to copy, so such a sheet must be a -link-only stub, not a mirror" β€” and `docs/provenance-audit.md` adjudicated the identical -case for `enumeration/awesome-nmap-grep.md`, recording that a credit line alone does not -cure a missing licence. So: - -| | Handling | -|---|---| -| Prose | **Not copied.** Written fresh in the house voice. | -| Commands, API call sequences, struct layouts, registry paths | Reproduced as the technical facts they are β€” not the upstream's creative expression, and mostly not its inventions either (its own README: "Most of these techniques are discovered by other security researchers and I do not claim their ownership"). | -| Screenshots | **Hotlinked** at the pinned SHA, 3–6 per sheet, each captioned and credited individually. Never copied into this repo. | - -The HackTricks route β€” an adapted mirror under a stated licence β€” is not available here, -and neither is a generated `sync-*` collection. There is no version of this work that -mirrors ired.team prose. - -**Batch 0 shipped already.** The plumbing is on disk and working: the `references` array in -the sheets schema, `src/components/SheetReferences.astro` (mounted in -`src/pages/sheets/[...slug].astro`), the `.shot` / `.shot-credit` figure styles in -`src/styles/prose.css`, the `/credits` provenance plate, and the research helper -`scripts/fetch-ired.py`. - -**Batches 1–5 were never written.** `grep -rl "ired.team" src/content/sheets/` returns -nothing: 0 of 280 sheets cite it. Batch 1 is the process-injection cluster β€” 41 upstream -guides condensed to ~9 sheets, filed `category: exploitation`, -`subcategory: "Process Injection"`. This is the site's largest gap: the `exploitation` -category blurb promises "injection, upload, and shell delivery" and ships four sheets, none -about injection. - -The research helper never writes to `src/`: - -```bash -./scripts/fetch-ired.py --tree # the 287 guide paths, grouped by cluster -./scripts/fetch-ired.py --get <path> # one guide at the pin, GitBook macros translated -./scripts/fetch-ired.py --images <path> # every figure as a finished SHA-pinned, percent-encoded URL -``` - -Always take figure URLs from `--images`. Upstream asset names are GitBook exports like -`Screenshot from 2019-04-28 16-28-59.png`, full of spaces and parentheses; hand-encoding -them is how a hotlink silently 404s. - -## Task 2 β€” OSINT usage - -16 sheets under `src/content/sheets/osint/`, all crediting -[Bellingcat's Online Investigation Toolkit](https://bellingcat.gitbook.io/toolkit) and -[tools.osintnewsletter.com](https://tools.osintnewsletter.com). They catalogue tools well -and show how to run almost none of them. Measured across the 16: - -| sheet | tool links | code fences | per-tool `###` | -|---|---|---|---| -| social-media-platforms | 107 | 4 | 0 | -| people-search | 97 | 0 | 0 | -| maps-and-satellite-imagery | 74 | 3 | 0 | -| websites-and-infrastructure | 71 | 4 | 0 | -| conflict-and-environment | 52 | 2 | 0 | -| transport-tracking | 45 | 1 | 0 | -| companies-and-finance | 41 | 2 | 0 | -| image-video-forensics | 41 | 2 | 0 | -| osint-foundations | 37 | 0 | 0 | -| usernames-and-accounts | 27 | 3 | **5** | -| archiving-and-evidence | 26 | 3 | 0 | -| email-and-phone | 25 | 4 | **7** | -| data-analysis-and-visualisation | 22 | 2 | 0 | -| geolocation | 16 | 1 | 0 | -| reverse-image-search | 16 | 1 | 0 | -| social-media-monitoring | 12 | 1 | 0 | - -709 tool links, 33 code fences, and only two sheets with per-tool walkthroughs. Those two -are the target shape β€” read `osint/usernames-and-accounts.md` before writing: a `## Method` -numbered sequence, then `## Key tools` broken into `### <Tool>` sections, each with install, -six to ten real invocations with a comment per line explaining why you would run it, and a -closing paragraph on false positives and what the output does not tell you. - -`people-search.md` is the clearest failure: 97 tool links, zero commands. - -## Standing rules - -- **Never push to `main`.** Work on a jj bookmark off `main`, one per batch. Existing - bookmarks: `main`, `osint-section`, `ired-team-integration`, `generalize-recyclebin`. -- `DESIGN.md` is the live design contract. Parallel agents share this working copy, so - **no agent runs `npm run build`, `astro build`, or `npm install`** β€” builds collide in - `dist/`. Read-only checks are fine. The integration pass builds once at the end. -- Validator baseline is already non-zero and is recorded, not repaired: - 217 `EXTRA/misplaced`, ~103 bare-fence warnings, `MISSING: exploitation/shell-stabilization`, - `content-manifest.json` stale by ~200 sheets. Saved for diffing at - `$CLAUDE_SCRATCHPAD/validator-baseline.txt`. New sheets land under `EXTRA`, which is - informational. - -## Assumed - -- Assumed: batch 1 is nine sheets covering the upstream process-injection cluster's - distinct techniques, not nine one-to-one page ports. Where several upstream guides are - variants of one technique, they merge into one sheet and each gets a `references` entry. -- Assumed: the eight thinnest/highest-link OSINT sheets are worth more than a shallow pass - over all sixteen, so the remaining eight stay for a later pass. -- Assumed: OSINT work deepens the existing sheets in place rather than adding new per-tool - sheets, because the sheets are already correctly scoped by investigative question and - splitting them would break the `subcategory` grouping on the category page. -- Assumed: tools whose only interface is a web form get a worked walkthrough β€” what to - paste, what the result pane means, what to screenshot for the record β€” rather than being - skipped for having no CLI. -- Assumed: the design pass is deferred to its own session. The user's note for it, kept - here so it is not lost: daemon-sec.xyz has **interactive dots** the cheatsheet lacks, and - that pass should start by auditing the live site rather than from `DESIGN.md` alone. -</context> - -<task> -1. Read `docs/superpowers/specs/2026-09-28-ired-team-integration-design.md` in full, - `DESIGN.md`, `src/content.config.ts`, and `src/content/sheets/osint/usernames-and-accounts.md` - as the house-voice reference. -2. Run three agents in parallel, each owning disjoint files so they cannot collide: - - **A β€” ired.team batch 1.** `./scripts/fetch-ired.py --tree`, select the ~9 distinct - process-injection techniques, fetch each guide and its images, write - `src/content/sheets/exploitation/<slug>.md`. Owns `src/content/sheets/exploitation/` only. - - **B β€” OSINT heavy four.** `people-search`, `maps-and-satellite-imagery`, - `social-media-platforms`, `websites-and-infrastructure`. Owns those four files only. - - **C β€” OSINT thematic four.** `conflict-and-environment`, `companies-and-finance`, - `image-video-forensics`, `transport-tracking`. Owns those four files only. -3. Integration pass, in the main session after all three report: run the validator and diff - against the saved baseline, run `npm run test` once, `curl -sIL` a sample figure URL from - each new ired sheet, and read one new sheet plus one rewritten OSINT sheet rendered. -4. Append an ired.team row to `docs/provenance-audit.md` so the audit stays the single - record of third-party content. -5. Commit each agent's output as its own jj change on a bookmark off `main`. -</task> - -<constraints> -- Reproduce no ired.team prose. Technical facts β€” command syntax, Win32 API call order, - struct fields, registry paths β€” are reproduced as facts; sentences are not. If a paragraph - could be diffed against the upstream and found similar, rewrite it. -- Every screenshot is a `<figure class="shot">` with: a SHA-pinned percent-encoded - `raw.githubusercontent.com` URL from `--images`, an `alt` describing what the shot *shows* - (it is what remains if the hotlink dies), `loading="lazy"`, `referrerpolicy="no-referrer"`, - and a `<span class="shot-credit">ired.team Β· Mantvydas Baranauskas</span>` on **every** - figure β€” credit is per-image, not one blanket line per page. -- 3–6 figures per ired sheet: the ones carrying information the text cannot β€” debugger - state, a detection artefact in Process Hacker, a SysInternals view. Upstream pages with - 30 near-identical shots get the decisive few. -- Every offensive sheet carries a `## Detection` section. This is DΓ†MON//SEC's own - contribution and is also what makes the sheet a derived work rather than a restatement. -- Every code fence declares a language. The baseline already carries ~103 bare fences and - this work must not add one. -- ired sheets use the flat `upstream*` fields β€” `upstreamName: "ired.team"`, - `upstreamAuthor: "Mantvydas Baranauskas"`, `upstreamLicense: none`, - `upstreamRelation: derived` β€” plus a `references` entry per additional upstream guide or - third-party implementation the sheet draws on, each with a `note` saying what it - contributed. A sheet that merely gains an ired-derived section gets a `references` entry - only, so prior attribution is preserved. -- No new `taxonomy.ts` entries, no `/ired` route, no new category tab. `subcategory` does - the grouping; the site's 14 tabs stay 14 tabs. -- Internal cross-links are `/sheets/<category>/<slug>` and must resolve against a file on - disk β€” `validate-content.py` fails on a broken or self-referential one. No `[[wikilinks]]`, - no `![[embeds]]`, no `%%comments%%`. -- OSINT commands must be ones that work against the current tool version. Where a tool has - been abandoned, API-locked, or had its free tier removed since the sheet was written, say - so in one line and name the live alternative, because a cheatsheet that confidently prints - a dead command costs more time than an empty section. -- Do not touch `src/content/payloads/`, `src/content/internal/`, `src/content/hacktricks/`, - `scripts/sync-*`, the manifests, or `astro.config.mjs`. -- House voice: second person, present tense, no "in today's world" opener, no section that - restates its own heading, no praise of the tool. Match the register of - `osint/usernames-and-accounts.md` and `osint/email-and-phone.md`. -</constraints> - -<output_format> -The deliverable is files on disk, plus β€” in the main session only β€” a report of at most 25 -lines: sheets written with their paths, OSINT sheets rewritten with before/after fence -counts, the validator diff against baseline, the figure-URL spot-check result, and anything -a success criterion could not meet with the reason. No preamble, no closing summary, no -restatement of the plan. -</output_format> - -<examples> -Sheet anatomy for an ired-derived sheet, from Β§7 of the spec: - -```markdown ---- -title: "Process Hollowing" -description: "Carving a suspended process's image out of memory and running a replacement PE in its place, plus the relocation fixups that make it work." -category: exploitation -subcategory: "Process Injection" -tags: [process-injection, evasion, windows, maldev] -tools: [windbg, visual-studio, process-hacker] -difficulty: advanced -updated: 2026-10-03 -upstreamName: "ired.team" -upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations" -upstreamAuthor: "Mantvydas Baranauskas" -upstreamLicense: none -upstreamRelation: derived -references: - - name: "Process-Hollowing" - url: "https://github.com/m0n0ph1/Process-Hollowing" - author: "m0n0ph1" - relation: inspired - note: "The reference implementation ired.team's lab works from." ---- - -## What it does -## Prerequisites -## Walkthrough ← commands / API sequence + the 3–6 credited figures -## Detection ← what defenders see; DΓ†MON//SEC's own addition -## References -``` - -For the OSINT sheets the shape is set by the two existing good sheets rather than by an -example here: read `osint/usernames-and-accounts.md` and copy its structure exactly. -</examples> - -<success_criteria> -1. ~9 new sheets exist under `src/content/sheets/exploitation/` with - `subcategory: "Process Injection"`, each carrying all five sections including - `## Detection`, and each with `upstreamLicense: none` and `upstreamRelation: derived`. -2. `grep -c 'shot-credit'` equals the figure count in every new ired sheet, every figure URL - contains `8cdbdd60eb4a8997e689649f3911f7c893e59ed9`, and `curl -sIL` on a sample from each - sheet returns `200` with an `image/*` content type. -3. No prose passage in a new sheet matches the corresponding upstream guide; spot-checked by - diffing one sheet against its `--get` output. -4. The eight targeted OSINT sheets each have per-tool `###` sections and at least eight - language-tagged code fences, up from a maximum of four, and each ends with one end-to-end - worked example that names a concrete starting datum and the pivots taken from it. -5. `python3 scripts/validate-content.py` reports no new `ISSUES` or `MISSING` against - `validator-baseline.txt`, and zero new bare-fence warnings. -6. `npm run test` is green, `test/internal-links.test.mjs` included. -7. `docs/provenance-audit.md` carries an ired.team row. -8. `jj log` shows the work on a bookmark off `main`, and `main` is unchanged. -</success_criteria> diff --git a/scripts/__pycache__/build-candidates.cpython-314.pyc b/scripts/__pycache__/build-candidates.cpython-314.pyc Binary files differ. diff --git a/scripts/__pycache__/port-missing.cpython-314.pyc b/scripts/__pycache__/port-missing.cpython-314.pyc Binary files differ. diff --git a/scripts/__pycache__/port-vault.cpython-314.pyc b/scripts/__pycache__/port-vault.cpython-314.pyc Binary files differ. diff --git a/scripts/__pycache__/sync-payloads.cpython-314.pyc b/scripts/__pycache__/sync-payloads.cpython-314.pyc Binary files differ. diff --git a/src/components/Hero.astro b/src/components/Hero.astro @@ -1,253 +0,0 @@ ---- -import Icon from './Icon.astro'; -import { url } from '../lib/url'; - -interface Props { - sheets: number; - domains: number; - payloads: number; -} -const { sheets, domains, payloads } = Astro.props; - -/** - * The hero, carried over from the main site's retired paper hero. - * - * Two display lines over a bordered ledger card. Both lines sit at their - * font's narrowest usable width with a vertical scale on top β€” the comp's - * poster face is not licensed here β€” and `scaleY` overflows its own line - * box, so each line lives in a fixed-height `cqw` wrapper that clips it - * (`.clip` / `.stretch` in chrome.css). - * - * Sized in `cqw` against the text column rather than in `vw` against the - * window. In `vw` the lockup is sized against a viewport the column stops - * tracking at `--maxw`, and past that width the type drifts out of line - * with everything under it β€” which is a thing you only see at 1900px. - * - * ── The spelling ── - * The main site sets the wordmark in substitution glyphs (DΛΣMӨП, three - * scripts). The vault self-hosts EB Garamond's *latin* subset only, so Ξ› Ξ£ - * Σ¨ П would fall back per-character to a different face at a different - * width and the lockup would render as six mismatched letters. The Latin - * spelling is the site's own documented fallback for exactly this case, - * and it is what the six-glyph hover key is built against. - */ -const WORDMARK = 'DΓ†MON'; -const GLYPHS = [...WORDMARK]; - -/** The main site this vault hangs off. */ -const MAIN_SITE = 'https://daemon-sec.xyz'; - -/** Per-glyph entrance step, ms. Six glyphs at 54ms is what the ink rule's - * 780ms delay is timed against β€” change one and the rule lands wrong. */ -const STEP = 54; - -const STATS = [ - { value: sheets, label: 'Curated<br />cheatsheets', tone: 'iris' }, - { value: domains, label: 'Operator<br />domains', tone: 'love' }, - { value: payloads, label: 'Mirrored<br />payload pages', tone: 'gold' }, -]; ---- -<section class="hero" id="top"> - <div class="wrap hero__inner"> - <div class="hero__lockup" data-lockup> - <div class="clip hero__line"> - <h1 class="stretch hero__word" style="transform: scaleY(1.28);"> - {GLYPHS.map((g, i) => ( - <span class="hero__glyph" style={`animation-delay: ${i * STEP}ms;`}>{g}</span> - ))} - </h1> - </div> - - <span class="hero__rule" aria-hidden="true"></span> - - <div class="clip hero__subline"> - <p class="stretch hero__sub"><span>The cheatsheet vault</span></p> - </div> - </div> - - <div class="hero__ledger"> - <div class="hero__card corners plate-dawn"> - <div> - <div class="hero__eyebrow">Vault / 2026</div> - <h2 class="hero__h2">Zero fluff. Copy-ready.</h2> - <p class="hero__blurb"> - {sheets} hand-picked cheatsheets across {domains} domains β€” Active Directory, - enumeration, exploitation, priv-esc, web, DFIR β€” stripped of vault-specific - syntax and refreshed for current tool flags. Plus the full - PayloadsAllTheThings mirror, searchable offline. - </p> - <div class="hero__actions"> - <button class="btn" data-search-open type="button"> - <Icon name="search" /> Search the vault - </button> - <a class="btn btn--ghost" href={url('payloads')}> - <Icon name="layers" /> Payloads - </a> - {/* Back to the main site. The vault is a satellite of - daemon-sec.xyz, and the hero is where that relationship - has to be stated β€” the footer is too far down to serve as - the only way home. */} - <a class="btn btn--ghost" href={MAIN_SITE} target="_blank" rel="noopener"> - <Icon name="external" /> daemon-sec.xyz - </a> - </div> - </div> - - <div> - {/* The identity readout, carried over from the main site's hero. - Borderless on purpose: the card around it is the printed - spec, this is the terminal output inside it, so the two read - as different registers rather than as a box in a box. Every - value is a fact the vault can actually assert. */} - <div class="whoami"> - <div class="whoami__cmd"> - <span class="u">daemon@sec</span><span>~</span><span>%</span> - <span class="c">whoami --vault</span> - <span class="whoami__caret" aria-hidden="true"></span> - </div> - <dl class="whoami__rows"> - <dt>scope</dt><dd>Offensive security Β· red team Β· DFIR</dd> - <dt>format</dt><dd>Copy-ready commands, current tool flags</dd> - <dt>search</dt><dd>Pagefind Β· offline Β· zero tracking</dd> - <dt>license</dt><dd>MIT Β· payloads mirrored from swisskyrepo</dd> - </dl> - </div> - - <dl class="hero__stats" style="margin-top:1.6rem;"> - {STATS.map((s) => ( - <div class={`hero__stat hero__stat--${s.tone}`}> - <dt class="sr-only" set:html={s.label.replace('<br />', ' ')} /> - <dd> - {/* Padded to two digits and counted up from zero. The - initial text is the final value, so with JS off or - reduced motion on, the number is simply correct. */} - <b data-count={s.value}>{String(s.value).padStart(2, '0')}</b> - <span set:html={` ${s.label}`} /> - </dd> - </div> - ))} - </dl> - </div> - </div> - </div> - </div> -</section> - -<script> - /** - * Proximity ripple across the lockup. - * - * The letter under the pointer lifts most and its neighbours fall off - * from it, so the wordmark answers the cursor as one object rather than - * as six independent hover targets. - * - * Written straight to the nodes from a rAF. `scale` and `translate` are - * separate CSS properties from `transform`, which the entrance animation - * owns and fills `both` β€” so the three compose instead of overwriting - * each other. Using `transform` here would cancel the entrance. - */ - const LIFT = 0.075; // peak scale added to the letter under the pointer - const RISE = 0.022; // peak rise, in em, so it tracks the type size - const REACH = 2.1; // how far the falloff carries, in glyph widths - - function initLockup(): void { - const host = document.querySelector<HTMLElement>('[data-lockup]'); - if (!host || host.dataset.bound) return; - if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; - host.dataset.bound = '1'; - - const glyphs = () => Array.from(host.querySelectorAll<HTMLElement>('.hero__glyph')); - let raf = 0; - let point: { x: number } | null = null; - - const paint = () => { - raf = 0; - for (const g of glyphs()) { - if (!point) { - g.style.scale = ''; - g.style.translate = ''; - continue; - } - const r = g.getBoundingClientRect(); - // Falloff measured in glyph widths, so it scales with the type - // rather than with a pixel constant that only suits one viewport. - const d = Math.abs(point.x - (r.left + r.width / 2)) / r.width; - const fade = Math.max(0, 1 - (d / REACH) ** 2); - g.style.scale = `${1 + LIFT * fade}`; - // Four places, not two: at 0.01em steps the rise quantises to - // roughly two pixels at this type size, and the falloff stepped - // between neighbours instead of easing across them. - g.style.translate = `0 ${(-RISE * fade).toFixed(4)}em`; - } - }; - - const queue = () => { if (!raf) raf = requestAnimationFrame(paint); }; - - const onMove = (e: PointerEvent) => { - // Coarse pointers have no hover to track; a tap would otherwise - // leave one letter stuck mid-lift. - if (e.pointerType !== 'mouse') return; - const r = host.getBoundingClientRect(); - // A band around the lockup rather than the box itself: the letters - // should start answering as the pointer approaches, not the instant - // it crosses an invisible edge. - const near = e.clientY > r.top - r.height * 0.6 && e.clientY < r.bottom + r.height * 0.6; - point = near ? { x: e.clientX } : null; - queue(); - }; - const onLeave = () => { point = null; queue(); }; - - addEventListener('pointermove', onMove, { passive: true }); - addEventListener('pointerleave', onLeave, { passive: true }); - addEventListener('blur', onLeave); - } - - /** - * The stats climb as their row lands rather than on page load, so the - * numbers are moving while they are being looked at rather than - * finishing behind the hero's entrance. - */ - function initCounters(): void { - const nodes = document.querySelectorAll<HTMLElement>('[data-count]:not([data-counted])'); - if (!nodes.length) return; - if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; - - const run = (el: HTMLElement) => { - el.dataset.counted = '1'; - const target = Number(el.dataset.count ?? '0'); - const pad = (n: number) => String(n).padStart(2, '0'); - const t0 = performance.now(); - const DURATION = 1100; - const tick = (now: number) => { - const p = Math.min(1, (now - t0) / DURATION); - // Ease-out cubic: the figure should decelerate onto its value - // rather than stop dead on it. - const eased = 1 - (1 - p) ** 3; - el.textContent = pad(Math.round(target * eased)); - if (p < 1) requestAnimationFrame(tick); - }; - requestAnimationFrame(tick); - }; - - const obs = new IntersectionObserver( - (entries) => { - for (const e of entries) { - if (!e.isIntersecting) continue; - run(e.target as HTMLElement); - obs.unobserve(e.target); - } - }, - { threshold: 0.6 }, - ); - nodes.forEach((n) => obs.observe(n)); - } - - function init(): void { - initLockup(); - initCounters(); - } - - document.addEventListener('astro:page-load', init); - if (document.readyState !== 'loading') init(); - else document.addEventListener('DOMContentLoaded', init); -</script> diff --git a/src/components/HeroLoop.astro b/src/components/HeroLoop.astro @@ -14,7 +14,7 @@ const data = JSON.stringify({ shelves, name, tagline }); <SignalField /> <noscript> <div style="padding:56px 44px;max-width:1560px;margin:0 auto;"> - <p style="font-family:var(--font-wordmark);font-weight:800;font-size:clamp(40px,9vw,120px);font-stretch:88%;letter-spacing:-0.02em;line-height:1;color:var(--text);text-transform:uppercase;">{name}</p> + <h1 style="margin:0;font-family:var(--font-wordmark);font-weight:800;font-size:clamp(40px,9vw,120px);font-stretch:88%;letter-spacing:-0.02em;line-height:1;color:var(--text);text-transform:uppercase;">{name}</h1> <p style="margin-top:14px;font-family:var(--font-plex);font-size:13px;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);">Six shelves. One vault.</p> <ul style="margin-top:28px;list-style:none;padding:0;border-top:1px solid var(--line);"> {shelves.map((s) => ( @@ -33,6 +33,9 @@ const data = JSON.stringify({ shelves, name, tagline }); // the inverse of the page (theme-aware P/Q). Pauses when hidden or scrolled // out; prefers-reduced-motion holds T=12.0 with no scramble. + import { mountParticleField, type LitShelf, type ShelfSource } from '../scripts/dmn/particle-field'; + import { SHELF_ICONS } from '../scripts/dmn/shelf-icons'; + const W = 1920, H = 1080, PLATE_W = 784, PLATE_H = 780, MIN_HEIGHT = 560, MARQUEE = 92; const CANVAS_MIN = 1280, FLOW_PLATE_MIN = 860; const BOOT = 0, CLOSE = 18.8, AUTHORED = 21.4, HELD_FRAME = 12.0; @@ -112,13 +115,15 @@ const data = JSON.stringify({ shelves, name, tagline }); nav.setAttribute('data-index-zone', ''); // ── Plate ── - const dots: HTMLElement[] = []; - const cols = 24, rowsN = 24, cw = PLATE_W / cols, chh = PLATE_H / rowsN; - const dotField = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px' }); - for (let r = 0; r < rowsN; r++) for (let c = 0; c < cols; c++) { - const d = el('div', { position: 'absolute', left: (c * cw + cw / 2 - 2) + 'px', top: (r * chh + chh / 2 - 2) + 'px', width: '4px', height: '4px', borderRadius: '2px', background: Q.ink, opacity: '0.08' }); - dots.push(d); dotField.append(d); - } + // The plate's dots: spread across it at rest and gathering into the + // hovered shelf's icon (src/scripts/dmn). They replace the old random + // grid, so the dots on the card are the ones that form the icon. The + // anchor is the open upper half, between the chrome and the shelf block. + const plateCanvas = el('canvas', { position: 'absolute', inset: '0', width: '100%', height: '100%', display: 'block' }) as HTMLCanvasElement; + plateCanvas.className = 'dmn-plate-dots'; + const iconAnchor = el('div', { position: 'absolute', left: '40px', right: '40px', top: '92px', height: '300px' }); + iconAnchor.setAttribute('data-field-icon', ''); + const dotField = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px' }, [iconAnchor, plateCanvas]); const plateMeta = { fontFamily: MONO, fontSize: '14px', letterSpacing: '0.10em', textTransform: 'uppercase', color: Q.subtle }; const chromeRight = el('span', { color: P.love }, 'β€” / 06'); const chrome = el('div', { position: 'absolute', left: '40px', right: '40px', top: '36px', display: 'flex', justifyContent: 'space-between', whiteSpace: 'nowrap', ...plateMeta }, [el('span', undefined, 'Plate Β· index'), chromeRight]); @@ -149,6 +154,7 @@ const data = JSON.stringify({ shelves, name, tagline }); standing, ...sections.map((s) => s.block), ]); plateSurface.setAttribute('aria-hidden', 'true'); + plateSurface.setAttribute('data-field-host', ''); // Plate host β€” absolute box on canvas, aspect-ratio fitted box on flow. let plateHost: HTMLElement, plateScaleK = 1; @@ -163,7 +169,10 @@ const data = JSON.stringify({ shelves, name, tagline }); // ── Wordmark ── const wm = name.split('').map((g) => el('span', { display: 'inline-block', opacity: '0', transform: 'translateY(24px)', color: g === '-' ? P.love : P.muted, fontFamily: MONO, fontWeight: '500' }, g)); - const wordmark = el('div', { fontFamily: WORDMARK_FACE, fontWeight: '700', lineHeight: '1', letterSpacing: '-0.02em', color: P.ink, display: 'flex', whiteSpace: 'pre' }, wm); + // The page's one heading; the glyphs scramble on arrival, so the + // accessible name is the settled spelling. + const wordmark = el('h1', { margin: '0', fontFamily: WORDMARK_FACE, fontWeight: '700', lineHeight: '1', letterSpacing: '-0.02em', color: P.ink, display: 'flex', whiteSpace: 'pre' }, wm); + wordmark.setAttribute('aria-label', name); // ── Meta rows + subline ── const metaBase = (x?: Record<string, string>) => ({ fontFamily: MONO, fontSize: '15px', letterSpacing: '0.10em', textTransform: 'uppercase', color: P.subtle, whiteSpace: 'nowrap', ...(x || {}) }); @@ -205,23 +214,12 @@ const data = JSON.stringify({ shelves, name, tagline }); root = el('div', { display: 'flex', flexDirection: 'column', minHeight: `min(calc(100svh - ${MARQUEE}px), 620px)`, padding: 'clamp(28px, 4vw, 56px) clamp(20px, 3.8vw, 72px)', color: P.ink, opacity: '0', fontFamily: ARCHIVO }, [topMeta, hairline, centerWrap, bottomMeta]); } - let lastStep = -1, lastAccent = ''; function paintPlate(T: number, st: any, reveal: number) { plateSurface.style.clipPath = `inset(0 ${(1 - reveal) * 100}% 0 0)`; const accentName = st.active >= 0 ? shelves[st.active].accent : 'love'; const accentCol = Q[accentName]; chromeRight.textContent = st.active >= 0 ? `${shelves[st.active].n} / 06` : 'β€” / 06'; chromeRight.style.color = accentCol; - const step = Math.floor(T * 6); - if (step !== lastStep || accentName !== lastAccent) { - lastStep = step; lastAccent = accentName; - for (let i = 0; i < dots.length; i++) { - const v = hash(i, step), v2 = hash(i + 991, step), on = v > 0.72; - const d = dots[i]; - d.style.background = on && v2 > 0.6 ? accentCol : Q.ink; - d.style.opacity = String(on ? 0.35 + v2 * 0.55 : 0.08); - } - } standing.style.opacity = String(st.standing); standing.style.transform = `translateY(${(1 - st.standing) * 16}px)`; for (let i = 0; i < sections.length; i++) { @@ -239,7 +237,7 @@ const data = JSON.stringify({ shelves, name, tagline }); hairline.style.transform = `scaleX(${rule})`; topMeta.style.opacity = String(enter(T, BOOT + 0.2).o); recDot.style.opacity = String(pulse); - timecodeEl.textContent = timecode(T); + timecodeEl.textContent = timecode(st.E ?? T); bottomMeta.style.opacity = String(enter(T, BOOT + 2.4).o); // Wordmark decode const roll = Math.floor(T * 18); @@ -283,7 +281,7 @@ const data = JSON.stringify({ shelves, name, tagline }); if (k > 0 && Math.abs(k - plateScaleK) > 0.001) { plateScaleK = k; (plateHost as any)._inner.style.transform = `scale(${k})`; } } } - return { root, paint }; + return { root, paint, plateCanvas }; } function initHero() { @@ -297,12 +295,46 @@ const data = JSON.stringify({ shelves, name, tagline }); const reduced = matchMedia('(prefers-reduced-motion: reduce)'); let hover: { idx: number | null; last: number | null } = { idx: null, last: null }; - const onHold = (i: number) => { hover = { idx: i, last: i }; }; - const onRelease = () => { hover = { idx: null, last: hover.last }; }; + + // Which shelf a hand is holding, for the plate's icon. Hover only: the + // tour cycles the rows and the plate's copy, the icon answers a pointer. + let lit: LitShelf = { index: null, accent: null, held: false }; + const litSubs = new Set<(s: LitShelf) => void>(); + const shelfSource: ShelfSource = { + get: () => lit, + subscribe(f) { litSubs.add(f); return () => { litSubs.delete(f); }; }, + }; + const setLit = (i: number | null) => { + const next: LitShelf = i === null ? { index: null, accent: null, held: false } : { index: i, accent: shelves[i].accent, held: true }; + if (next.index === lit.index && next.held === lit.held) return; + lit = next; + litSubs.forEach((f) => f(lit)); + }; + const onHold = (i: number) => { hover = { idx: i, last: i }; setLit(i); }; + const onRelease = () => { hover = { idx: null, last: hover.last }; setLit(null); }; + let destroyPlate: (() => void) | null = null; + const mountPlate = (cv: HTMLCanvasElement, qFam: 'dawn' | 'night') => { + destroyPlate?.(); + destroyPlate = mountParticleField(cv, { + icons: SHELF_ICONS, + shelf: shelfSource, + cloud: false, + restFill: true, + tone: 'plate', + // The plate is the page's inverse, painted from the fixed poles. + vars: { + ink: qFam === 'night' ? '--color-night-text' : '--color-dawn-ink', + base: `--color-${qFam}-base`, + iris: `--color-${qFam}-iris`, + love: `--color-${qFam}-love`, + }, + accentVar: (a) => `--color-${qFam}-${a}`, + }); + }; let current: { root: HTMLElement; paint: (T: number, st: any) => void } | null = null; let mode: 'canvas' | 'flow' | null = null, withPlate = false, dark = document.documentElement.getAttribute('data-theme') === 'dark'; - let carriedT = 0, carriedMix = 0, raf = 0, prev = 0, visible = !document.hidden, inView = true; + let carriedT = 0, carriedE = 0, carriedMix = 0, raf = 0, prev = 0, visible = !document.hidden, inView = true; function rebuild() { const w = host.clientWidth || window.innerWidth; @@ -321,6 +353,7 @@ const data = JSON.stringify({ shelves, name, tagline }); const built = build(w, 'canvas', false, shelves, cfg.name, cfg.tagline, P, Q, onHold, onRelease); stage.append(built.root); host.append(stage); + mountPlate(built.plateCanvas, dark ? 'dawn' : 'night'); const fit = () => { const bw = host.clientWidth, bh = host.clientHeight, scale = bw > 0 && bh > 0 ? Math.min(bw / W, bh / H) : 0; stage.style.width = W + 'px'; stage.style.height = H + 'px'; @@ -334,6 +367,8 @@ const data = JSON.stringify({ shelves, name, tagline }); host.style.height = ''; const built = build(w, 'flow', withPlate, shelves, cfg.name, cfg.tagline, P, Q, onHold, onRelease); host.append(built.root); + if (withPlate) mountPlate(built.plateCanvas, dark ? 'dawn' : 'night'); + else { destroyPlate?.(); destroyPlate = null; } current = built; (host as any)._fit = null; } @@ -344,7 +379,7 @@ const data = JSON.stringify({ shelves, name, tagline }); function paintFrame() { if (!current) return; const T = reduced.matches ? HELD_FRAME : carriedT; - const st = shelfState(T, carriedMix, hover.last, shelves); + const st = { ...shelfState(T, carriedMix, hover.last, shelves), E: reduced.matches ? HELD_FRAME : carriedE }; current.paint(T, st); } @@ -352,7 +387,12 @@ const data = JSON.stringify({ shelves, name, tagline }); function tick(now: number) { if (!running()) { raf = 0; return; } const dt = Math.min(0.05, (now - prev) / 1000); prev = now; - carriedT = (carriedT + dt) % AUTHORED; + // The boot plays once; the tour then wraps from the last shelf + // straight back to the first instead of closing and booting again. + // Every entrance has finished by STARTS[0], so the seam is invisible. + carriedT += dt; + carriedE += dt; + if (carriedT >= CLOSE) carriedT = STARTS[0] + (carriedT - CLOSE); const target = hover.idx !== null ? 1 : 0; carriedMix += (target - carriedMix) * Math.min(1, dt * 14); paintFrame(); @@ -380,6 +420,7 @@ const data = JSON.stringify({ shelves, name, tagline }); document.addEventListener('astro:before-swap', () => { stop(); ro.disconnect(); io.disconnect(); + destroyPlate?.(); destroyPlate = null; document.removeEventListener('visibilitychange', onVis); canvasMq.removeEventListener('change', onBp); plateMq.removeEventListener('change', onBp); }, { once: true }); diff --git a/src/components/SignalField.astro b/src/components/SignalField.astro @@ -1,13 +1,22 @@ <canvas class="signal-field" data-signal aria-hidden="true"></canvas> <script> - import { mountSignalField } from '../scripts/signal-field'; + // The field behind every hero and banner: daemon-sec's dot field + // (src/scripts/dmn), which replaced the old connection-node map. It reads + // this site's mode-flipping tokens off the canvas, so inside a `.plate` + // band it takes the band's night values. + import { mountParticleField } from '../scripts/dmn/particle-field'; const cleanups = new Set<() => void>(); function init(): void { document.querySelectorAll<HTMLCanvasElement>('[data-signal]:not([data-bound])').forEach((canvas) => { canvas.dataset.bound = '1'; - cleanups.add(mountSignalField(canvas)); + cleanups.add( + mountParticleField(canvas, { + vars: { ink: '--text', base: '--base', iris: '--iris', love: '--love' }, + onFail: () => canvas.remove(), + }), + ); }); } diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro @@ -137,6 +137,7 @@ const canonical = new URL(Astro.url.pathname, Astro.site).href; <ClientRouter /> </head> <body> + <a class="skip-link" href="#main">Skip to content</a> <!-- Shared arrowhead for every inline-SVG flowchart (src/styles/flow.css). Defined once here so a diagram only has to reference `url(#flow-arrow)`. `context-stroke` makes the head take each edge's own stroke colour, so @@ -168,5 +169,13 @@ const canonical = new URL(Astro.url.pathname, Astro.site).href; <script> import '../scripts/app.ts'; </script> + <script> + // The effects shared with daemon-sec.xyz (src/scripts/dmn): the cursor + // pill and the sheet image viewer. Re-bound after each navigation. + import { initDmn } from '../scripts/dmn/init'; + document.addEventListener('astro:page-load', initDmn); + if (document.readyState !== 'loading') initDmn(); + else document.addEventListener('DOMContentLoaded', initDmn, { once: true }); + </script> </body> </html> diff --git a/src/scripts/app.ts b/src/scripts/app.ts @@ -151,7 +151,7 @@ function initScrollReveal(): void { const nodes = document.querySelectorAll<HTMLElement>([ '.subcat:not([data-reveal-bound])', '.download-library__head:not([data-reveal-bound])', - '.record:not([data-reveal-bound])', + '.rec:not([data-reveal-bound])', '.download-row:not([data-reveal-bound])', '.prose > h2:not([data-reveal-bound])', '.prose > h3:not([data-reveal-bound])', @@ -166,7 +166,7 @@ function initScrollReveal(): void { nodes.forEach((node, index) => { node.dataset.revealBound = '1'; // Only nearby siblings cascade; long indexes should not accumulate delay. - node.style.setProperty('--reveal-delay', `${(index % 6) * 34}ms`); + node.style.setProperty('--reveal-delay', `${(index % 6) * 70}ms`); if (reduced) node.classList.add('is-revealed'); else node.classList.add('scroll-reveal'); }); diff --git a/src/scripts/dmn/cursor.ts b/src/scripts/dmn/cursor.ts @@ -0,0 +1,256 @@ +/** + * The dot cursor and its pill, framework-free. Ported from daemon-sec's + * client/src/components/daemon/cursor.tsx so the sibling sites behave + * the same way. + * + * A small disc trails the pointer and opens into a ring over anything + * clickable. Over something with one clear action it becomes a pill + * that says what a click will do: + * - an element with `data-cursor-label` (optional `data-cursor-icon`: + * view | play | pdf | download | read) + * - a video marked `data-pill`: Play / Pause (a click on the video body + * toggles it; the native control strip keeps its own clicks) + * - an image marked `data-zoomable`: View image / View GIF + * - a link to a PDF: Open PDF, or Download PDF if it downloads + * - a link the site counts as an article (`isArticle`): Read + * + * Fine pointers only; it never mounts on touch or under reduced motion, + * so those visitors keep the native cursor. The trailing loop stops as + * soon as the dot catches up, so an idle page costs nothing. + * + * Usage: + * mountCursor({ isArticle: (href) => href.startsWith("/sheets/") }); + */ + +export interface CursorOptions { + /** Which internal links read as articles and get the "Read" pill. */ + isArticle?: (href: string) => boolean; + /** Finer control: the pill text for a link, or null for none. Takes + * precedence over `isArticle`. */ + linkLabel?: (href: string, link: HTMLAnchorElement) => string | null; + /** Ancestors that are dark grounds, so the cursor re-tints over them. */ + darkSelector?: string; + /** Finer control than `darkSelector`, for sites that nest light panels + * inside dark bands. */ + isDark?: (target: HTMLElement) => boolean; + /** Override the reduced-motion gate. */ + enabled?: boolean; +} + +type Mode = "view" | "play" | "read" | "pdf" | "label" | null; +type IconName = "view" | "play" | "pause" | "pdf" | "download" | "read"; + +/* lucide path data (ISC/MIT), so the pill needs no icon font. */ +const ICONS: Record<IconName, string> = { + view: '<path d="M15 3h6v6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/><path d="M9 21H3v-6"/>', + play: '<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>', + pause: '<rect x="14" y="3" width="5" height="18" rx="1"/><rect x="5" y="3" width="5" height="18" rx="1"/>', + pdf: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>', + download: '<path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/>', + read: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>', +}; + +const EASE = 0.28; +const PILL_EASE = 0.16; +const SETTLED = 0.1; +const CONTROLS_STRIP = 48; +const PDF = /\.pdf($|[?#])/i; +const INTERACTIVE = + "a, button, input, textarea, select, summary, label, [role='button'], [role='switch'], [role='tab']"; + +/* Only ever fixed markup from ICONS: an unknown name falls back to the + arrow rather than reaching the DOM. */ +function svg(name: IconName) { + const body = ICONS[name] ?? ICONS.read; + return `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`; +} + +export function mountCursor(options: CursorOptions = {}): () => void { + /* A site-level motion switch (`<html data-motion="reduce">`) counts the + same as the OS setting. */ + const enabled = + options.enabled ?? + (window.matchMedia("(pointer: fine)").matches && + !window.matchMedia("(prefers-reduced-motion: reduce)").matches && + document.documentElement.dataset.motion !== "reduce"); + if (!enabled) return () => {}; + const isArticle = options.isArticle ?? (() => false); + const linkLabel = + options.linkLabel ?? ((href: string) => (isArticle(href) ? "Read" : null)); + const darkSelector = options.darkSelector ?? "[data-ground='dark']"; + const isDark = options.isDark ?? ((t: HTMLElement) => Boolean(t.closest(darkSelector))); + + const el = document.createElement("div"); + el.className = "dmn-cursor"; + el.setAttribute("aria-hidden", "true"); + el.innerHTML = '<span class="dmn-cursor-pill"><span class="dmn-cursor-icon"></span><span class="dmn-cursor-label"></span></span>'; + const iconEl = el.querySelector<HTMLElement>(".dmn-cursor-icon")!; + const labelEl = el.querySelector<HTMLElement>(".dmn-cursor-label")!; + document.body.appendChild(el); + const root = document.documentElement; + root.classList.add("has-dmn-cursor"); + + let x = window.innerWidth / 2; + let y = window.innerHeight / 2; + let tx = x; + let ty = y; + let raf = 0; + let seen = false; + let mode: Mode = null; + let mediaEl: HTMLElement | null = null; + + const place = () => { + el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`; + }; + const step = () => { + const k = mode ? PILL_EASE : EASE; + x += (tx - x) * k; + y += (ty - y) * k; + if (Math.abs(tx - x) < SETTLED && Math.abs(ty - y) < SETTLED) { + x = tx; + y = ty; + place(); + raf = 0; + return; + } + place(); + raf = requestAnimationFrame(step); + }; + const wake = () => { + if (!raf) raf = requestAnimationFrame(step); + }; + + const under = (target: HTMLElement): { mode: Mode; el: HTMLElement } | null => { + const labelled = target.closest<HTMLElement>("[data-cursor-label]"); + if (labelled) return { mode: "label", el: labelled }; + const video = target.closest<HTMLVideoElement>("video[data-pill]"); + if (video) return { mode: "play", el: video }; + const img = + target.closest<HTMLImageElement>("img[data-zoomable]") ?? + target.closest("button")?.querySelector<HTMLImageElement>("img[data-zoomable]") ?? + null; + if (img) return { mode: "view", el: img }; + const link = target.closest<HTMLAnchorElement>("a[href]"); + if (link) { + const href = link.getAttribute("href") ?? ""; + if (PDF.test(href)) return { mode: "pdf", el: link }; + if (linkLabel(href, link)) return { mode: "read", el: link }; + } + return null; + }; + + const labelFor = (m: Mode, t: HTMLElement): { icon: IconName; text: string } => { + if (m === "label") { + return { icon: (t.dataset.cursorIcon as IconName) || "read", text: t.dataset.cursorLabel ?? "" }; + } + if (m === "play") { + const paused = (t as HTMLVideoElement).paused; + return { icon: paused ? "play" : "pause", text: paused ? "Play" : "Pause" }; + } + if (m === "pdf") { + return t.hasAttribute("download") + ? { icon: "download", text: "Download PDF" } + : { icon: "pdf", text: "Open PDF" }; + } + if (m === "read") { + const a = t as HTMLAnchorElement; + return { icon: "read", text: linkLabel(a.getAttribute("href") ?? "", a) ?? "Read" }; + } + const src = (t as HTMLImageElement).currentSrc || (t as HTMLImageElement).src || ""; + return { icon: "view", text: /\.gif($|\?)/i.test(src) ? "View GIF" : "View image" }; + }; + + const syncLabel = () => { + if (!mode || !mediaEl) return; + const { icon, text } = labelFor(mode, mediaEl); + iconEl.innerHTML = svg(icon); + labelEl.textContent = text; + }; + + const setMode = (next: Mode, target: HTMLElement | null) => { + if (mediaEl && mediaEl !== target) { + mediaEl.removeEventListener("play", syncLabel); + mediaEl.removeEventListener("pause", syncLabel); + } + mode = next; + mediaEl = target; + if (mode && mediaEl) { + el.dataset.mode = mode; + if (mode === "play") { + mediaEl.addEventListener("play", syncLabel); + mediaEl.addEventListener("pause", syncLabel); + } + syncLabel(); + } else { + delete el.dataset.mode; + } + }; + + /* A viewer or modal on screen hides the pill. Sites often keep their + search dialog in the DOM while closed, so only a visible one counts. */ + const dialogOpen = () => + Array.from(document.querySelectorAll<HTMLElement>("dialog[open], [role='dialog'][aria-modal='true']")).some( + (d) => (d.checkVisibility ? d.checkVisibility() : d.offsetParent !== null), + ); + + const applyTarget = (target: HTMLElement | null) => { + if (!target?.closest) return; + el.classList.toggle("hot", Boolean(target.closest(INTERACTIVE))); + el.classList.toggle("on-dark", isDark(target)); + const media = dialogOpen() ? null : under(target); + setMode(media?.mode ?? null, media?.el ?? null); + }; + + const onMove = (e: MouseEvent) => { + tx = e.clientX; + ty = e.clientY; + seen = true; + wake(); + }; + const onOver = (e: MouseEvent) => applyTarget(e.target as HTMLElement | null); + const onLeave = () => (el.style.opacity = "0"); + const onEnter = () => (el.style.opacity = "1"); + + /* Content can move under a pointer that has not; look again while the + page scrolls, once per frame. */ + let scrollRaf = 0; + const onScroll = () => { + if (!seen || scrollRaf) return; + scrollRaf = requestAnimationFrame(() => { + scrollRaf = 0; + applyTarget(document.elementFromPoint(tx, ty) as HTMLElement | null); + }); + }; + + const onClick = (e: MouseEvent) => { + const video = (e.target as HTMLElement | null)?.closest?.<HTMLVideoElement>("video[data-pill]"); + if (!video) return; + const r = video.getBoundingClientRect(); + if (e.clientY > r.bottom - CONTROLS_STRIP) return; + e.preventDefault(); + if (video.paused) void video.play().catch(() => {}); + else video.pause(); + }; + + window.addEventListener("mousemove", onMove, { passive: true }); + window.addEventListener("mouseover", onOver, { passive: true }); + document.documentElement.addEventListener("mouseleave", onLeave); + document.documentElement.addEventListener("mouseenter", onEnter); + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("click", onClick); + place(); + + return () => { + if (raf) cancelAnimationFrame(raf); + if (scrollRaf) cancelAnimationFrame(scrollRaf); + setMode(null, null); + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseover", onOver); + document.documentElement.removeEventListener("mouseleave", onLeave); + document.documentElement.removeEventListener("mouseenter", onEnter); + window.removeEventListener("scroll", onScroll); + window.removeEventListener("click", onClick); + root.classList.remove("has-dmn-cursor"); + el.remove(); + }; +} diff --git a/src/scripts/dmn/icon-targets.ts b/src/scripts/dmn/icon-targets.ts @@ -0,0 +1,96 @@ +/** + * From an icon's outline to a landing spot for every particle. + * + * The icon arrives as SVG markup (a lucide component's `outerHTML`), + * is drawn once into a small offscreen canvas, and its alpha becomes a + * grid of "ink here" cells. Each particle then needs the nearest inked + * cell to its own resting place: a multi-source flood fill from every + * inked cell gives that for the whole grid at once, in a millisecond, + * which is near enough to true nearest at this resolution. The spot is + * jittered inside its cell so a stroke does not read as a row of beads. + * + * Coordinates are icon-local: `[-0.5, 0.5]` on both axes, y up. The + * field places and scales them per frame. + */ + +export const GRID = 128; +const PAD = 8; +const INK = 96; + +/** Draw the markup and read its alpha. Async: image decode. */ +export async function rasterise(svg: string, canvas: HTMLCanvasElement): Promise<Uint8Array> { + const img = new Image(); + img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; + await img.decode(); + canvas.width = GRID; + canvas.height = GRID; + const ctx = canvas.getContext("2d", { willReadFrequently: true }); + if (!ctx) throw new Error("no 2d context"); + ctx.clearRect(0, 0, GRID, GRID); + ctx.drawImage(img, PAD, PAD, GRID - PAD * 2, GRID - PAD * 2); + const { data } = ctx.getImageData(0, 0, GRID, GRID); + const out = new Uint8Array(GRID * GRID); + for (let i = 0; i < out.length; i++) out[i] = data[i * 4 + 3] > INK ? 1 : 0; + return out; +} + +/** For every cell, the index of the nearest inked cell (8-connected BFS). */ +export function nearestInked(ink: Uint8Array): Int32Array { + const n = GRID * GRID; + const src = new Int32Array(n).fill(-1); + const queue = new Int32Array(n); + let head = 0; + let tail = 0; + for (let i = 0; i < n; i++) { + if (ink[i]) { + src[i] = i; + queue[tail++] = i; + } + } + while (head < tail) { + const c = queue[head++]; + const cx = c % GRID; + const cy = (c - cx) / GRID; + for (let dy = -1; dy <= 1; dy++) { + const y = cy + dy; + if (y < 0 || y >= GRID) continue; + for (let dx = -1; dx <= 1; dx++) { + const x = cx + dx; + if (x < 0 || x >= GRID) continue; + const i = y * GRID + x; + if (src[i] >= 0) continue; + src[i] = src[c]; + queue[tail++] = i; + } + } + } + return src; +} + +/** + * Landing spots for `rest` (icon-local pairs), one per particle, from + * the nearest-ink map. Returns icon-local pairs, y up. + */ +export function targetsFor(rest: Float32Array, nearest: Int32Array, hash: (i: number) => number): Float32Array { + const count = rest.length / 2; + const out = new Float32Array(rest.length); + for (let i = 0; i < count; i++) { + const rx = rest[i * 2]; + const ry = rest[i * 2 + 1]; + const cx = Math.min(GRID - 1, Math.max(0, Math.floor((rx + 0.5) * GRID))); + const cy = Math.min(GRID - 1, Math.max(0, Math.floor((0.5 - ry) * GRID))); + const s = nearest[cy * GRID + cx]; + if (s < 0) { + out[i * 2] = rx; + out[i * 2 + 1] = ry; + continue; + } + const sx = s % GRID; + const sy = (s - sx) / GRID; + const jx = hash(i * 2) - 0.5; + const jy = hash(i * 2 + 1) - 0.5; + out[i * 2] = (sx + 0.5 + jx * 0.9) / GRID - 0.5; + out[i * 2 + 1] = 0.5 - (sy + 0.5 + jy * 0.9) / GRID; + } + return out; +} diff --git a/src/scripts/dmn/init.ts b/src/scripts/dmn/init.ts @@ -0,0 +1,31 @@ +/** + * Wires the shared daemon-sec effects into this site: the cursor with its + * action pill and the sheet image viewer. (The dot field is mounted by + * SignalField.astro and the hero.) Astro's ClientRouter swaps the body on + * navigation, so both re-bind on every `astro:page-load`. + */ +import { mountCursor } from './cursor'; +import { initViewer } from './viewer'; + +const READ = /^\/(?:sheets|payloads|internal|hacktricks)\/[^?#]+/; +const BROWSE = /^\/(?:[a-z0-9-]+|tags\/[^/?#]+|hacktricks\/section\/[^/?#]+)\/?(?:[?#].*)?$/; +const NOT_BROWSE = /^\/(?:credits|tags)?\/?$/; + +let destroyCursor: (() => void) | null = null; + +export function initDmn(): void { + initViewer(); + if (document.querySelector('.dmn-cursor')) return; + destroyCursor?.(); + destroyCursor = mountCursor({ + linkLabel: (href, link) => { + // The masthead and footer are navigation, not content: no pill. + if (link.closest('.mast, .dfooter')) return null; + if (READ.test(href)) return 'Read'; + if (BROWSE.test(href) && !NOT_BROWSE.test(href)) return 'Browse'; + return null; + }, + /* Dark bands are `.plate`; `.plate-dawn` panels inside them are light. */ + isDark: (t) => t.closest('.plate, .plate-dawn')?.classList.contains('plate') ?? false, + }); +} diff --git a/src/scripts/dmn/particle-field.ts b/src/scripts/dmn/particle-field.ts @@ -0,0 +1,1036 @@ +/** + * The dot field, framework-free. Generated from daemon-sec's + * client/src/components/daemon/particle-field.tsx (same shaders, same + * timing) so the sibling sites share one look without React. + * + * Two layers. The cloud: blue-noise dashes that drift and lean toward a + * ring lagging the pointer, thinning under any `[data-index-zone]` box. + * The icon layer (opt-in): dots that gather into the lit shelf's icon, + * placed on a `[data-field-icon]` box inside the canvas's + * `[data-field-host]` ancestor. On a light ground dots get a minimum size + * and more ink. Colours are read off the canvas's computed style and + * re-read whenever <html> changes its theme attributes. Pauses off-screen + * and in hidden tabs; one still frame under reduced motion (OS setting or + * `<html data-motion="reduce">`); `onFail` if WebGL is unavailable. + */ +import { minDistFor, poisson, rng } from "./poisson"; +import { nearestInked, rasterise, targetsFor } from "./icon-targets"; + +export type Accent = string; +export interface LitShelf { + index: number | null; + accent: Accent | null; + /** True while a pointer holds the row (vs. a tour lighting it). */ + held: boolean; +} +export interface ShelfSource { + get(): LitShelf; + subscribe(f: (s: LitShelf) => void): () => void; +} + +export interface FieldOptions { + /** Custom-property names for colours, resolved on the canvas. */ + vars?: { ink: string; base: string; iris: string; love: string }; + /** Accent colour variable for a shelf accent name. */ + accentVar?: (accent: string) => string; + pageFamily?: string; + plateFamily?: string; + tone?: "page" | "plate"; + /** Draw the drifting cloud (default true). */ + cloud?: boolean; + /** SVG markup per shelf, in shelf order; enables the icon layer. */ + icons?: string[]; + /** Which shelf is lit; required with `icons`. */ + shelf?: ShelfSource; + /** Icon dots rest spread across the canvas and stay visible. */ + restFill?: boolean; + /** Also gather for a shelf a tour lights, not only a held one. */ + tour?: boolean; + /** Override motion; defaults to off under reduced motion. */ + live?: boolean; + onFail?: () => void; +} + +const NO_SHELF: ShelfSource = { + get: () => ({ index: null, accent: null, held: false }), + subscribe: () => () => {}, +}; + +type Rgb = [number, number, number]; + +function hexToRgb(hex: string, fallback: Rgb): Rgb { + const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim()); + if (!m) return fallback; + const n = parseInt(m[1], 16); + return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]; +} + +function readVar(el: HTMLElement, name: string, fallback: Rgb): Rgb { + return hexToRgb(getComputedStyle(el).getPropertyValue(name), fallback); +} + +const NOISE = ` +vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } +vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } +vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); } +vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } +float snoise(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; + vec3 x3 = x0 - D.yyy; + i = mod289(i); + vec4 p = permute(permute(permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0)) + + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + float n_ = 0.142857142857; + vec3 ns = n_ * D.wyz - D.xzx; + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} +`; + +/* Field units: y runs -1..1 over the canvas height, x is Β±aspect. */ +const VERT = ` +precision highp float; +attribute vec2 a_rest; +attribute vec4 a_seed; +attribute vec2 a_from; +attribute vec2 a_to; + +uniform float u_time; +uniform vec2 u_extent; +uniform float u_pr; +uniform float u_pscale; +uniform float u_maxPoint; +uniform float u_layer; +uniform float u_hover; +uniform float u_morph; +uniform float u_pulse; +uniform float u_iconReady; +uniform float u_fill; +uniform float u_light; +uniform float u_ringR; +uniform float u_ringW; +uniform float u_ringW2; +uniform float u_ringDisp; +uniform vec2 u_ring; +uniform vec2 u_ringLag; +uniform vec4 u_zone; +uniform vec4 u_icon; + +varying float v_scale; +varying float v_near; +varying float v_col; +varying vec2 v_dir; + +${NOISE} + +/* The cloud thins under the shelf list so the rows stay readable, but + only partly and on a wide, rounded falloff: a hard cut-out read as a + box drawn round the list. Measured as a rounded rectangle with a + radius as big as its short side, so there is no corner to see. + No list, no thinning. */ +float zoneFade(vec2 p, float margin) { + if (u_zone.z <= u_zone.x) return 1.0; + vec2 c = 0.5 * (u_zone.xy + u_zone.zw); + vec2 h = 0.5 * (u_zone.zw - u_zone.xy); + float r = min(h.x, h.y) * 0.9; + vec2 q = abs(p - c) - h + r; + float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; + return mix(0.45, 1.0, smoothstep(-0.25, margin, sd)); +} + +void main() { + float t = u_time * 0.5; + vec2 p; + float scale; + float near = 0.0; + + if (u_layer < 0.5) { + vec2 rest = a_rest; + /* each dot sees the ring a little behind where it is, by its own + amount, which spreads the settle across the field */ + vec2 ring = mix(u_ringLag, u_ring, a_seed.w); + float dist = distance(rest, ring); + float noise0 = snoise(vec3(rest * 0.2 + vec2(18.4924, 72.9744), t * 0.5)); + float dist1 = distance(rest + noise0 * 0.005, ring); + float R = u_ringR; + float t1 = smoothstep(R - u_ringW * 2.0, R, dist) - smoothstep(R, R + u_ringW, dist1); + float t2 = smoothstep(R - u_ringW2 * 2.0, R, dist) - smoothstep(R, R + u_ringW2, dist1); + float t3 = smoothstep(R + u_ringW2, R, dist); + t1 = t1 * t1; + t2 = t2 * t2 * t2; + float s = t1 + t2 * 3.0 + t3 * 0.4; + s += snoise(vec3(rest * 30.0 + vec2(11.4924, 12.9744), t * 0.5)) * t3 * 0.5; + float nS = snoise(vec3(rest * 2.0 + vec2(18.4924, 72.9744), t * 0.5)); + s += pow((nS + 1.5) * 0.5, 2.0) * 0.6; + + float n1 = snoise(vec3(rest * 4.0 + vec2(88.494, 32.4397), t * 0.35)); + float n2 = snoise(vec3(rest * 4.0 + vec2(50.904, 120.947), t * 0.35)); + float n3 = snoise(vec3(rest * 20.0 + vec2(18.4924, 72.9744), t * 0.5)); + float n4 = snoise(vec3(rest * 20.0 + vec2(50.904, 120.947), t * 0.5)); + vec2 disp = vec2(n1, n2) * 0.03 + vec2(n3, n4) * 0.005; + float reach = clamp(dist, 0.0, 1.0); + disp.x += sin(rest.x * 20.0 + t * 4.0) * 0.02 * reach; + disp.y += cos(rest.y * 20.0 + t * 3.0) * 0.02 * reach; + + vec2 cur = rest + disp; + /* the rim is pushed off the ring */ + p = cur - (ring - cur) * pow(max(t2, 0.0), 0.75) * u_ringDisp * 0.3; + scale = (s + 0.2) * zoneFade(p, 0.25); + + float nA = snoise(vec3(rest * 10.0 + vec2(18.4924, 72.9744), u_time * 0.85)); + float ang = atan(p.y - ring.y, p.x - ring.x) + nA * 0.5; + v_dir = vec2(cos(ang), sin(ang)); + v_col = (snoise(vec3(rest * 2.0 + vec2(74.664, 91.556), u_time * 0.5)) + 1.0) * 0.5; + } else { + vec2 iconLocal = mix(a_from, a_to, smoothstep(0.0, 1.0, u_morph)); + + /* The icon as a volume, not a stencil. Each dot has its own depth + through the figure (a stable hash of its seed), and the figure + turns a little: a slow idle sway, plus a lean toward the pointer + so it follows the hand the way the cloud does. With depth, the + turn makes the front of the figure travel further than the back + and the dots nearest the eye draw larger and brighter (below), so + a gathered shelf icon reads as a small object made of dots + rather than a flat outline β€” the same outline, with a thickness. + The rotation is small enough that the glyph never stops reading + as itself. */ + float depth = fract(a_seed.y * 13.37 + a_seed.z * 0.71) - 0.5; + vec2 lean = clamp((u_ringLag - u_icon.xy) / max(u_icon.z * 3.0, 0.001), -1.0, 1.0); + float ay = sin(u_time * 0.55) * 0.28 + lean.x * 0.3; + float ax = cos(u_time * 0.41) * 0.18 - lean.y * 0.22; + vec3 q = vec3(iconLocal, depth * 0.34); + q = vec3(q.x * cos(ay) + q.z * sin(ay), q.y, -q.x * sin(ay) + q.z * cos(ay)); + q = vec3(q.x, q.y * cos(ax) - q.z * sin(ax), q.y * sin(ax) + q.z * cos(ax)); + /* a touch of perspective: what is nearer is a little bigger */ + float nearness = q.z / 0.34; + vec2 iconPos = u_icon.xy + q.xy * (1.0 + nearness * 0.09) * u_icon.zw; + /* Where a dot sets out from: a soft disc round the icon (the square + sample, folded to polar), so nothing square is ever drawn. */ + float ang0 = atan(a_rest.y, a_rest.x); + float rad0 = max(abs(a_rest.x), abs(a_rest.y)) * 2.0; + vec2 restDisc = u_icon.xy + vec2(cos(ang0), sin(ang0)) * sqrt(rad0) * u_icon.z * 0.95; + /* or, on the plate, spread over the whole surface: the plate's own + dots are the ones that gather */ + vec2 restFill = a_rest * 2.0 * u_extent * 0.94; + vec2 restW = mix(restDisc, restFill, u_fill); + restW += 0.02 * vec2( + snoise(vec3(a_rest * 3.0, t * 0.2)), + snoise(vec3(a_rest * 3.0 + 31.7, t * 0.2))); + + /* arrival, staggered per dot and eased like power3.out */ + float h = clamp((u_hover - a_seed.x * 0.35) / 0.65, 0.0, 1.0); + h = 1.0 - pow(1.0 - h, 3.0); + h *= u_iconReady; + /* On the plate a share of the dots stays behind, so the card keeps + its texture round the icon instead of emptying out. */ + h *= mix(1.0, step(0.3, a_seed.w), u_fill); + + /* twinkle: a life cycle from the seed, invisible at its wrap */ + float life = fract(u_time / a_seed.z + a_seed.y); + float env = smoothstep(0.0, 0.2, life) * (1.0 - smoothstep(0.65, 1.0, life)); + + p = mix(restW, iconPos, h); + float dist = distance(p, iconPos) / max(u_icon.z, 0.001); + near = h * smoothstep(0.08, 0.0, dist); + + /* a ring expanding from the icon's centre after a change, nudging + the dots outward as it passes */ + if (u_pulse >= 0.0) { + vec2 fromC = p - u_icon.xy; + float ringD = abs(length(fromC) - u_pulse * u_icon.z * 0.9); + float k = ringD / max(u_icon.z, 0.001); + p += normalize(fromC + 0.0001) * 0.025 * u_icon.z * exp(-k * k * 60.0) * (1.0 - u_pulse); + } + + /* Unseen at rest: the icon's dots only exist while a shelf is + hovered, appearing as they set out and fading as they return. */ + float shown = mix(smoothstep(0.0, 0.3, h), 1.0, u_fill); + scale = (env * (0.45 + 0.55 * a_seed.w) * 0.7 * (1.0 - h) + 1.6 * near + 0.3 * env * h) * shown; + /* the front of the figure draws a size up, the back a size down; + only once the dot has arrived, so the journey is unchanged */ + scale *= 1.0 + nearness * 0.22 * near; + v_col = near; + /* the depth rides out on v_dir, which the icon layer never reads + for direction: x is nearness in -1..1 */ + v_dir = vec2(nearness, 0.0); + } + + v_near = near; + v_scale = scale; + if (scale < 0.1) { + gl_Position = vec4(2.0, 2.0, 2.0, 1.0); + gl_PointSize = 1.0; + return; + } + /* The light-ground minimum is for the page cloud; the plate's dots + are already drawn a size up, and doubling it made them chunky. */ + float px = scale * 7.0 * u_pr * 0.5 * u_pscale + u_light * 1.1 * u_pr * (1.0 - u_layer); + /* The plate's dots are its whole surface, not a wash behind a + page, so they are drawn a size up. */ + if (u_layer > 0.5) px = (px * 1.25 + 0.6 * u_pr) * (1.0 + 0.7 * u_fill); + gl_PointSize = min(u_maxPoint, px); + gl_Position = vec4(p.x / u_extent.x, p.y / u_extent.y, 0.0, 1.0); +} +`; + +const FRAG = ` +precision highp float; +uniform float u_layer; +uniform float u_inkAlpha; +uniform float u_iconAlpha; +uniform vec3 u_c1; +uniform vec3 u_c2; +uniform vec3 u_c3; +uniform vec3 u_ink; +uniform vec3 u_accent; +uniform float u_light; + +varying float v_scale; +varying float v_near; +varying float v_col; +varying vec2 v_dir; + +float sdRoundBox(vec2 p, vec2 b, float r) { + vec2 q = abs(p) - b + r; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; +} + +void main() { + vec2 uv = gl_PointCoord - 0.5; + uv.y = -uv.y; + float a; + vec3 c; + if (u_layer < 0.5) { + /* a short dash, turned to face the ring */ + vec2 q = vec2(uv.x * v_dir.x + uv.y * v_dir.y, -uv.x * v_dir.y + uv.y * v_dir.x); + float sd = sdRoundBox(q, vec2(0.5, mix(0.2, 0.3, u_light)), 0.25); + float shape = smoothstep(0.1, 0.0, sd); + a = shape * smoothstep(0.1, 0.2, v_scale) * u_inkAlpha; + float h = 0.8; + c = v_col < h ? mix(u_c1, u_c2, v_col / h) : mix(u_c2, u_c3, (v_col - h) / (1.0 - h)); + } else { + float disc = smoothstep(0.5, 0.4, length(uv)); + a = disc * smoothstep(0.1, 0.2, v_scale) * mix(u_inkAlpha, u_iconAlpha, v_near); + /* the back of the figure sits a little further into the ground + than the front: the depth cue the size change alone lacks */ + a *= 1.0 - 0.28 * v_near * clamp(-v_dir.x, 0.0, 1.0); + c = mix(u_ink, u_accent, smoothstep(0.05, 0.6, v_near)); + } + if (a < 0.01) discard; + gl_FragColor = vec4(c * a, a); +} +`; + +function compile(gl: WebGLRenderingContext, type: number, src: string) { + const sh = gl.createShader(type); + if (!sh) return null; + gl.shaderSource(sh, src); + gl.compileShader(sh); + if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { + if (import.meta.env.DEV) console.error("particle-field shader:", gl.getShaderInfoLog(sh)); + gl.deleteShader(sh); + return null; + } + return sh; +} + +/* ── Tweens, the GSAP way: retarget from wherever the value is now ── */ +const easeOutCubic = (x: number) => 1 - Math.pow(1 - x, 3); + +class Tween { + value: number; + private from: number; + private to: number; + private t0 = 0; + private dur = 1; + constructor(v: number) { + this.value = v; + this.from = v; + this.to = v; + } + go(to: number, dur: number, now: number) { + if (to === this.to && dur === this.dur) return; + this.from = this.value; + this.to = to; + this.dur = Math.max(1, dur); + this.t0 = now; + } + set(v: number) { + this.value = this.from = this.to = v; + } + tick(now: number) { + const x = Math.min(1, (now - this.t0) / this.dur); + this.value = this.from + (this.to - this.from) * easeOutCubic(x); + return this.value; + } +} + +/** + * Pixels per field unit. The hero is a unit per half its height; a + * shorter band keeps roughly the hero's scale instead of squeezing the + * same cloud into less height, so dot spacing, the ring and the drift + * read the same on every page. + */ +function unitPx(w: number, h: number) { + return Math.max(h / 2, w * 0.26, 1); +} + +/** The phase the still frame is drawn at. */ +const HELD_TIME = 31.0; +/** How long the pointer may rest before the ring starts to wander. */ +const IDLE_MS = 2800; + + +export function mountParticleField(cv: HTMLCanvasElement, options: FieldOptions = {}): () => void { + const fam0 = options.tone === "plate" ? options.plateFamily ?? "night" : options.pageFamily ?? "dawn"; + const opts = { + pageFamily: "dawn", + plateFamily: "night", + accentVar: (a: string) => `--color-${fam0}-${a}`, + ...options, + }; + const tone = opts.tone ?? "page"; + const withIcons = Boolean(opts.icons && opts.icons.length); + const withCloud = opts.cloud ?? true; + const restFill = opts.restFill ?? false; + const tour = opts.tour ?? false; + const heroShelf = opts.shelf ?? NO_SHELF; + const live = + opts.live ?? + (!window.matchMedia("(prefers-reduced-motion: reduce)").matches && + document.documentElement.dataset.motion !== "reduce"); + const coarse = window.matchMedia("(pointer: coarse)").matches; + const setFailed = (_f: boolean) => opts.onFail?.(); + + const attrs: WebGLContextAttributes = { + alpha: true, + premultipliedAlpha: true, + antialias: false, + depth: false, + stencil: false, + powerPreference: "low-power", + }; + const gl = + cv.getContext("webgl", attrs) || + (cv.getContext("experimental-webgl", attrs) as WebGLRenderingContext | null); + if (!gl) { + if (import.meta.env.DEV) console.error("particle-field: no WebGL context"); + setFailed(true); + return () => {}; + } + const vs = compile(gl, gl.VERTEX_SHADER, VERT); + const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG); + const prog = gl.createProgram(); + if (!vs || !fs || !prog) { + setFailed(true); + return () => {}; + } + gl.attachShader(prog, vs); + gl.attachShader(prog, fs); + gl.linkProgram(prog); + if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { + if (import.meta.env.DEV) console.error("particle-field link:", gl.getProgramInfoLog(prog)); + setFailed(true); + return () => {}; + } + gl.useProgram(prog); + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.clearColor(0, 0, 0, 0); + + /* ── Geometry ── */ + /* Dots per square field unit: the hero's 8,000 over its 8.8. */ + const perUnit = coarse ? 400 : 900; + const morphCount = coarse ? 1600 : 2400; + /* Half-extents of the cloud in field units, regrown when the canvas + outgrows them. */ + const domain = { w: 0, h: 0 }; + const hash = rng(7); + + const seeds = (n: number) => { + const s = new Float32Array(n * 4); + for (let i = 0; i < n; i++) { + s[i * 4] = hash(); // arrival delay + s[i * 4 + 1] = hash(); // life phase + s[i * 4 + 2] = 2 + hash() * 2; // life length, seconds + s[i * 4 + 3] = hash(); // general purpose + } + return s; + }; + + const buf = { + cloudRest: gl.createBuffer(), + cloudSeed: gl.createBuffer(), + morphRest: gl.createBuffer(), + morphSeed: gl.createBuffer(), + morphFrom: gl.createBuffer(), + morphTo: gl.createBuffer(), + }; + let cloudN = 0; + const buildCloud = () => { + const area = domain.w * 2 * domain.h * 2; + const count = Math.min(16000, Math.round(perUnit * area)); + const pts = poisson({ + halfW: domain.w, + halfH: domain.h, + r: minDistFor(count, domain.w * 2, domain.h * 2), + seed: 11, + }); + cloudN = pts.length / 2; + gl.bindBuffer(gl.ARRAY_BUFFER, buf.cloudRest); + gl.bufferData(gl.ARRAY_BUFFER, pts, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, buf.cloudSeed); + gl.bufferData(gl.ARRAY_BUFFER, seeds(cloudN), gl.STATIC_DRAW); + }; + + const morphRest = poisson({ halfW: 0.5, halfH: 0.5, r: minDistFor(morphCount, 1, 1), seed: 23 }); + const morphN = morphRest.length / 2; + gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphRest); + gl.bufferData(gl.ARRAY_BUFFER, morphRest, gl.STATIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphSeed); + gl.bufferData(gl.ARRAY_BUFFER, seeds(morphN), gl.STATIC_DRAW); + const fromArr = new Float32Array(morphRest); + const toArr = new Float32Array(morphRest); + gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphFrom); + gl.bufferData(gl.ARRAY_BUFFER, fromArr, gl.DYNAMIC_DRAW); + gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphTo); + gl.bufferData(gl.ARRAY_BUFFER, toArr, gl.DYNAMIC_DRAW); + + const loc = { + rest: gl.getAttribLocation(prog, "a_rest"), + seed: gl.getAttribLocation(prog, "a_seed"), + from: gl.getAttribLocation(prog, "a_from"), + to: gl.getAttribLocation(prog, "a_to"), + }; + gl.enableVertexAttribArray(loc.rest); + gl.enableVertexAttribArray(loc.seed); + gl.enableVertexAttribArray(loc.from); + gl.enableVertexAttribArray(loc.to); + + const bindLayer = (layer: 0 | 1) => { + gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphRest : buf.cloudRest); + gl.vertexAttribPointer(loc.rest, 2, gl.FLOAT, false, 0, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphSeed : buf.cloudSeed); + gl.vertexAttribPointer(loc.seed, 4, gl.FLOAT, false, 0, 0); + /* the cloud never reads these; point them at something valid */ + gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphFrom : buf.cloudRest); + gl.vertexAttribPointer(loc.from, 2, gl.FLOAT, false, 0, 0); + gl.bindBuffer(gl.ARRAY_BUFFER, layer ? buf.morphTo : buf.cloudRest); + gl.vertexAttribPointer(loc.to, 2, gl.FLOAT, false, 0, 0); + }; + + /* ── Uniforms ── */ + const u = (name: string) => gl.getUniformLocation(prog, name); + const U = { + time: u("u_time"), + extent: u("u_extent"), + pr: u("u_pr"), + pscale: u("u_pscale"), + maxPoint: u("u_maxPoint"), + layer: u("u_layer"), + hover: u("u_hover"), + morph: u("u_morph"), + pulse: u("u_pulse"), + iconReady: u("u_iconReady"), + fill: u("u_fill"), + light: u("u_light"), + ringR: u("u_ringR"), + ringW: u("u_ringW"), + ringW2: u("u_ringW2"), + ringDisp: u("u_ringDisp"), + ring: u("u_ring"), + ringLag: u("u_ringLag"), + zone: u("u_zone"), + icon: u("u_icon"), + inkAlpha: u("u_inkAlpha"), + iconAlpha: u("u_iconAlpha"), + c1: u("u_c1"), + c2: u("u_c2"), + c3: u("u_c3"), + ink: u("u_ink"), + accent: u("u_accent"), + }; + const range = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE) as Float32Array; + gl.uniform1f(U.maxPoint, range ? range[1] : 64); + gl.uniform1f(U.ringW, 0.03); + gl.uniform1f(U.ringW2, 0.1); + gl.uniform1f(U.ringDisp, 0.5); + gl.uniform1f(U.inkAlpha, 0.7); + gl.uniform1f(U.iconAlpha, 0.6); + gl.uniform4f(U.zone, 0, 0, -1, -1); + gl.uniform4f(U.icon, 0, 0, 0.5, 0.5); + gl.uniform1f(U.iconReady, 0); + gl.uniform1f(U.fill, restFill ? 1 : 0); + + /* ── Colour ── */ + const INK: Rgb = [0.34, 0.32, 0.47]; + let accentName: Accent | null = null; + const fam = tone === "plate" ? opts.plateFamily : opts.pageFamily; + /* The reading weights, not the -soft washes: the pastels sank into + the cream ground and read as white specks. These tokens invert + with the mode, so the dots stay dark on light and light on dark. */ + const pushPalette = () => { + /* Is the ground behind this field light? The page's dawn base, or + the plate's night base, after the mode has inverted them. */ + const V = opts.vars ?? { + ink: tone === "plate" ? `--color-${fam}-text` : `--color-${fam}-ink`, + base: `--color-${fam}-base`, + iris: `--color-${fam}-iris`, + love: `--color-${fam}-love`, + }; + const [br, bg, bb] = readVar(cv, V.base, [0.98, 0.96, 0.93]); + const light = 0.2126 * br + 0.7152 * bg + 0.0722 * bb > 0.5; + gl.uniform1f(U.light, light ? 1 : 0); + gl.uniform1f(U.inkAlpha, light ? 0.9 : 0.7); + const ink = readVar(cv, V.ink, INK); + gl.uniform3fv(U.ink, ink); + gl.uniform3fv(U.c1, ink); + gl.uniform3fv( + U.c2, + readVar(cv, V.iris, [0.47, 0.35, 0.64]), + ); + gl.uniform3fv( + U.c3, + readVar(cv, V.love, [0.69, 0.25, 0.42]), + ); + gl.uniform3fv( + U.accent, + accentName ? readVar(cv, opts.accentVar(accentName), ink) : ink, + ); + }; + pushPalette(); + + /* ── State the frame reads ── */ + const box = { left: 0, pageTop: 0, width: 1, height: 1 }; + let unit = unitPx(cv.clientWidth, cv.clientHeight); + const pointer = { x: 0, y: 0, inside: false }; + const ring = { x: 0.3, y: 0.1 }; + const ringLag = { x: 0.3, y: 0.1 }; + const hover = new Tween(0); + const morph = new Tween(1); + let pulseT0 = -Infinity; + let lastMove = -Infinity; + let raf = 0; + let running = false; + let visible = !document.hidden; + let inView = true; + let lost = false; + let lastNow = 0; + const start = performance.now(); + + const measureBox = () => { + const r = cv.getBoundingClientRect(); + box.left = r.left; + box.pageTop = r.top + window.scrollY; + box.width = r.width || 1; + box.height = r.height || 1; + }; + + const size = () => { + const pr = Math.min(2, window.devicePixelRatio || 1); + const w = Math.max(1, Math.round(cv.clientWidth * pr)); + const h = Math.max(1, Math.round(cv.clientHeight * pr)); + if (cv.width !== w || cv.height !== h) { + cv.width = w; + cv.height = h; + } + gl.viewport(0, 0, w, h); + unit = unitPx(cv.clientWidth, cv.clientHeight); + const ex = cv.clientWidth / 2 / unit; + const ey = cv.clientHeight / 2 / unit; + gl.uniform2f(U.extent, ex, ey); + gl.uniform1f(U.pr, pr); + gl.uniform1f(U.pscale, (cv.clientWidth / 2000) * 0.9); + if (ex + 0.1 > domain.w || ey + 0.1 > domain.h) { + domain.w = Math.max(domain.w, ex + 0.2); + domain.h = Math.max(domain.h, ey + 0.15); + buildCloud(); + } + }; + + /* ── The shelf list (keep-out for the cloud) and the icon's home ── + Rects are in page pixels; the canvas may sit inside a scaled + composition (the hero scales its 1920Γ—1080 artboard), so a page + pixel is `scale` canvas pixels. */ + let zoneEl: HTMLElement | null = null; + let zoneRo: ResizeObserver | null = null; + let iconEl: HTMLElement | null = null; + const toField = (px: number, py: number, c: DOMRect) => { + const k = unit * (c.width / Math.max(1, cv.clientWidth)); + return [(px - c.left - c.width / 2) / k, (c.height / 2 - (py - c.top)) / k]; + }; + const pageUnit = (c: DOMRect) => unit * (c.width / Math.max(1, cv.clientWidth)); + /* The hero swaps between two layouts at 1280px and rebuilds the list + when it does, so a held reference goes stale: re-find it whenever + the one we have has left the document. */ + const adoptZone = (el: HTMLElement) => { + if (zoneEl === el) return; + zoneRo?.disconnect(); + zoneEl = el; + zoneRo = new ResizeObserver(() => measureZone()); + zoneRo.observe(el); + }; + /* Where the icon forms: a `[data-field-icon]` box inside the field's + own host if there is one (the plate marks its open upper half), + otherwise centred on the shelf list. */ + const findIcon = () => { + const host = cv.closest("[data-field-host]"); + iconEl = host?.querySelector<HTMLElement>("[data-field-icon]") ?? null; + }; + const measureZone = () => { + if (!zoneEl || !zoneEl.isConnected) { + const el = document.querySelector<HTMLElement>("[data-index-zone]"); + if (el) adoptZone(el); + } + if (withIcons && (!iconEl || !iconEl.isConnected)) findIcon(); + const c = cv.getBoundingClientRect(); + if (withCloud && zoneEl && zoneEl.isConnected) { + const z = zoneEl.getBoundingClientRect(); + const [x0, y0] = toField(z.left, z.bottom, c); + const [x1, y1] = toField(z.right, z.top, c); + gl.uniform4f(U.zone, x0, y0, x1, y1); + } else { + gl.uniform4f(U.zone, 0, 0, -1, -1); + } + if (!withIcons) return; + const home = iconEl && iconEl.isConnected ? iconEl : zoneEl && zoneEl.isConnected ? zoneEl : null; + if (!home) { + gl.uniform4f(U.icon, 0, 0, 0.5, 0.5); + return; + } + const z = home.getBoundingClientRect(); + const k = c.width / Math.max(1, cv.clientWidth); + const sizePx = Math.min(440 * k, Math.max(96 * k, 0.9 * Math.min(z.width, z.height))); + const [cx, cy] = toField(z.left + z.width / 2, z.top + z.height / 2, c); + const s = sizePx / pageUnit(c); + gl.uniform4f(U.icon, cx, cy, s, s); + }; + let zoneTimer = 0; + const findZone = (attempt = 0) => { + const el = document.querySelector<HTMLElement>("[data-index-zone]"); + if (el) { + adoptZone(el); + measureZone(); + return; + } + if (attempt < 8) zoneTimer = window.setTimeout(() => findZone(attempt + 1), 250); + }; + + /* ── Icons ── */ + let targets: Float32Array[] | null = null; + let curIndex: number | null = null; + let pending: LitShelf = heroShelf.get(); + let cancelled = false; + const scratch = document.createElement("canvas"); + const jitter = rng(99); + const jitters = new Float32Array(morphN * 2); + for (let i = 0; i < jitters.length; i++) jitters[i] = jitter(); + + /* The icon answers the hand only: a shelf the loop lights on its own + tour leaves the cloud as it is, so the gather always means "you + are pointing at this". */ + const applyShelf = (s: LitShelf, now: number, snap = false) => { + pending = s; + /* One layout read per hover change: cheap, and it means the icon + always forms on the list as it is laid out right now. */ + const wanted = s.index !== null && (s.held || tour); + if (wanted) measureZone(); + if (!wanted || s.index === null) { + hover.go(0, 800, now); + return; + } + hover.go(1, 500, now); + if (!targets || s.index === curIndex) return; + /* continue from wherever the last gather got to */ + const m = easeOutCubic(Math.min(1, morph.value)); + for (let i = 0; i < fromArr.length; i++) fromArr[i] = fromArr[i] + (toArr[i] - fromArr[i]) * m; + toArr.set(targets[s.index]); + gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphFrom); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, fromArr); + gl.bindBuffer(gl.ARRAY_BUFFER, buf.morphTo); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, toArr); + curIndex = s.index; + if (snap) { + morph.set(1); + } else { + morph.set(0); + morph.go(1, 600, now); + pulseT0 = now; + } + if (s.accent !== accentName) { + accentName = s.accent; + pushPalette(); + } + }; + + /* ── Frames ── */ + const drawFrame = (t: number, now: number) => { + if (lost) return; + gl.uniform1f(U.time, t); + gl.uniform1f(U.ringR, 0.175 + Math.sin(t) * 0.03 + Math.cos(t * 3) * 0.02); + gl.uniform2f(U.ring, ring.x, ring.y); + gl.uniform2f(U.ringLag, ringLag.x, ringLag.y); + gl.uniform1f(U.hover, hover.value); + gl.uniform1f(U.morph, morph.value); + const pulse = (now - pulseT0) / 2000; + gl.uniform1f(U.pulse, pulse >= 0 && pulse < 1 ? pulse : -1); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.uniform1f(U.layer, 0); + if (withCloud) { + bindLayer(0); + gl.drawArrays(gl.POINTS, 0, cloudN); + } + if (withIcons) { + gl.uniform1f(U.layer, 1); + bindLayer(1); + gl.drawArrays(gl.POINTS, 0, morphN); + } + }; + + const still = () => { + size(); + ring.x = ringLag.x = 0.3; + ring.y = ringLag.y = 0.1; + /* A held row still gets its icon with motion off; otherwise the + frame is the plain cloud. */ + const s = withIcons ? heroShelf.get() : { index: null, accent: null, held: false }; + if (targets) applyShelf(s, performance.now(), true); + hover.set((s.held || tour) && s.index !== null ? 1 : 0); + morph.set(1); + drawFrame(HELD_TIME, -Infinity); + cv.classList.add("is-live"); + }; + + const tick = (now: number) => { + raf = requestAnimationFrame(tick); + const t = (now - start) / 1000; + const dt = Math.min(0.05, (now - lastNow) / 1000 || 0.016); + lastNow = now; + /* The ring drifts on its own, and only partly toward the hand: + a 2%/frame lerp toward the pointer is what reads as weight. */ + const wx = 0.35 * Math.sin(t * 0.19) + 0.12 * Math.sin(t * 0.47); + const wy = 0.3 * Math.cos(t * 0.14) + 0.1 * Math.cos(t * 0.37); + const follow = pointer.inside && now - lastMove < IDLE_MS; + const tx = follow ? pointer.x + wx * 0.1 : wx * 0.6; + const ty = follow ? pointer.y + wy * 0.1 : wy * 0.6; + const k = 1 - Math.pow(1 - (follow ? 0.02 : 0.01), dt * 60); + ring.x += (tx - ring.x) * k; + ring.y += (ty - ring.y) * k; + const k2 = 1 - Math.pow(1 - 0.06, dt * 60); + ringLag.x += (ring.x - ringLag.x) * k2; + ringLag.y += (ring.y - ringLag.y) * k2; + hover.tick(now); + morph.tick(now); + drawFrame(t, now); + cv.classList.add("is-live"); + }; + + const sync = () => { + const want = live && visible && inView; + if (want && !running) { + running = true; + size(); + lastNow = performance.now(); + raf = requestAnimationFrame(tick); + } else if (!want && running) { + running = false; + cancelAnimationFrame(raf); + } + if (!live) still(); + }; + + const onMove = (e: PointerEvent) => { + if (e.pointerType === "touch") return; + const top = box.pageTop - window.scrollY; + const nx = ((e.clientX - box.left) / box.width) * 2 - 1; + const ny = -(((e.clientY - top) / box.height) * 2 - 1); + if (nx < -1.05 || nx > 1.05 || ny < -1.05 || ny > 1.05) { + pointer.inside = false; + return; + } + pointer.inside = true; + pointer.x = (nx * box.width) / 2 / unit; + pointer.y = (ny * box.height) / 2 / unit; + lastMove = e.timeStamp; + }; + const onVis = () => { + visible = !document.hidden; + sync(); + }; + const onMode = () => { + pushPalette(); + if (!running) still(); + }; + const onLost = (e: Event) => { + e.preventDefault(); + lost = true; + cancelAnimationFrame(raf); + setFailed(true); + }; + const unsubscribe = withIcons + ? heroShelf.subscribe((s) => { + if (live) applyShelf(s, performance.now()); + else still(); + }) + : () => {}; + + let settleRaf = 0; + let settleTimer = 0; + const measureSettled = () => { + cancelAnimationFrame(settleRaf); + clearTimeout(settleTimer); + settleRaf = requestAnimationFrame(() => { + settleRaf = requestAnimationFrame(() => { + measureBox(); + measureZone(); + if (!running) still(); + }); + }); + /* and once more after the hero's own observer has re-rendered */ + settleTimer = window.setTimeout(() => { + measureBox(); + measureZone(); + if (!running) still(); + }, 160); + }; + const ro = new ResizeObserver(() => { + measureBox(); + size(); + measureZone(); + measureSettled(); + if (!running) still(); + }); + ro.observe(cv); + const io = + typeof IntersectionObserver !== "undefined" + ? new IntersectionObserver(([e]) => { + inView = e.isIntersecting; + sync(); + }) + : null; + io?.observe(cv); + + if (live) window.addEventListener("pointermove", onMove, { passive: true }); + window.addEventListener("resize", measureSettled); + document.addEventListener("visibilitychange", onVis); + window.addEventListener("daemonmodechange", onMode); + const modeMo = new MutationObserver(onMode); + modeMo.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-mode", "data-theme", "style"] }); + cv.addEventListener("webglcontextlost", onLost); + + /* Prepare the icons; the morph layer twinkles at rest until they land. */ + (async () => { + if (!withIcons) return; + const svgs = opts.icons ?? []; + const out: Float32Array[] = []; + for (const svg of svgs) { + const ink = await rasterise(svg, scratch); + if (cancelled) return; + out.push(targetsFor(morphRest, nearestInked(ink), (i) => jitters[i])); + } + if (cancelled) return; + targets = out; + gl.uniform1f(U.iconReady, 1); + applyShelf(pending, performance.now(), true); + if (!running) still(); + })().catch(() => { + /* No icon: the morph layer just twinkles. */ + }); + + measureBox(); + findZone(); + if (withIcons) { + if (live) applyShelf(heroShelf.get(), performance.now(), true); + } + sync(); + + if (import.meta.env.DEV) { + (window as unknown as Record<string, unknown>)[withIcons ? "__dmnField" : "__dmnCloud"] = { + get hover() { + return hover.value; + }, + get morph() { + return morph.value; + }, + get index() { + return curIndex; + }, + get ready() { + return targets !== null; + }, + get counts() { + return { cloud: cloudN, morph: morphN }; + }, + get ring() { + return { ...ring }; + }, + get iconPx() { + /* the icon's centre and size in CSS px, page-relative to the canvas */ + const v = gl.getUniform(prog, U.icon as WebGLUniformLocation) as Float32Array; + const c = cv.getBoundingClientRect(); + const k = unit * (c.width / Math.max(1, cv.clientWidth)); + return { x: c.left + c.width / 2 + v[0] * k, y: c.top + c.height / 2 - v[1] * k, size: v[2] * k }; + }, + get pointer() { + return { ...pointer, box: { ...box }, lastMove }; + }, + set(index: number | null, accent: Accent | null) { + applyShelf({ index, accent, held: true }, performance.now()); + }, + }; + } + + return () => { + cancelled = true; + running = false; + cancelAnimationFrame(raf); + clearTimeout(zoneTimer); + unsubscribe(); + ro.disconnect(); + io?.disconnect(); + zoneRo?.disconnect(); + window.removeEventListener("pointermove", onMove); + window.removeEventListener("resize", measureSettled); + cancelAnimationFrame(settleRaf); + clearTimeout(settleTimer); + document.removeEventListener("visibilitychange", onVis); + window.removeEventListener("daemonmodechange", onMode); + modeMo.disconnect(); + cv.removeEventListener("webglcontextlost", onLost); + Object.values(buf).forEach((b) => b && gl.deleteBuffer(b)); + gl.deleteProgram(prog); + gl.deleteShader(vs); + gl.deleteShader(fs); + }; +} diff --git a/src/scripts/dmn/poisson.ts b/src/scripts/dmn/poisson.ts @@ -0,0 +1,110 @@ +/** + * Poisson-disc sampling (Bridson), seeded. + * + * Blue-noise points: no two closer than `r`, none further than about + * `2r` from a neighbour, so the field reads as a cloud rather than as a + * grid or as static. Seeded so a given hero looks the same on every + * visit and every mount; the seed also keys the per-point randoms the + * shader uses for twinkle and arrival order. + */ + +/** mulberry32 β€” small, fast, good enough for layout. */ +export function rng(seed: number) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export interface PoissonOptions { + /** Half-extents of the domain, centred on the origin. */ + halfW: number; + halfH: number; + /** Minimum spacing between points. */ + r: number; + seed?: number; + /** Candidates tried around each active point before it retires. */ + k?: number; +} + +/** + * Points as a flat `[x0, y0, x1, y1, …]` array in the domain's units. + * Count is roughly `0.7 Β· area / rΒ²`; `minDistFor` inverts that. + */ +export function poisson({ halfW, halfH, r, seed = 1, k = 30 }: PoissonOptions): Float32Array { + const rand = rng(seed); + const W = halfW * 2; + const H = halfH * 2; + const cell = r / Math.SQRT2; + const gw = Math.ceil(W / cell); + const gh = Math.ceil(H / cell); + const grid = new Int32Array(gw * gh).fill(-1); + const xs: number[] = []; + const ys: number[] = []; + const active: number[] = []; + const r2 = r * r; + + const put = (x: number, y: number) => { + const i = xs.length; + xs.push(x); + ys.push(y); + grid[Math.floor((y + halfH) / cell) * gw + Math.floor((x + halfW) / cell)] = i; + active.push(i); + return i; + }; + + const fits = (x: number, y: number) => { + if (x < -halfW || x >= halfW || y < -halfH || y >= halfH) return false; + const gx = Math.floor((x + halfW) / cell); + const gy = Math.floor((y + halfH) / cell); + for (let j = Math.max(0, gy - 2); j <= Math.min(gh - 1, gy + 2); j++) { + for (let i = Math.max(0, gx - 2); i <= Math.min(gw - 1, gx + 2); i++) { + const n = grid[j * gw + i]; + if (n < 0) continue; + const dx = xs[n] - x; + const dy = ys[n] - y; + if (dx * dx + dy * dy < r2) return false; + } + } + return true; + }; + + put((rand() - 0.5) * W, (rand() - 0.5) * H); + + while (active.length) { + const ai = Math.floor(rand() * active.length); + const p = active[ai]; + let placed = false; + for (let n = 0; n < k; n++) { + const a = rand() * Math.PI * 2; + const d = r * (1 + rand()); + const x = xs[p] + Math.cos(a) * d; + const y = ys[p] + Math.sin(a) * d; + if (fits(x, y)) { + put(x, y); + placed = true; + break; + } + } + if (!placed) { + active[ai] = active[active.length - 1]; + active.pop(); + } + } + + const out = new Float32Array(xs.length * 2); + for (let i = 0; i < xs.length; i++) { + out[i * 2] = xs[i]; + out[i * 2 + 1] = ys[i]; + } + return out; +} + +/** The spacing that lands about `count` points on a `w Γ— h` domain. */ +export function minDistFor(count: number, w: number, h: number) { + return Math.sqrt((0.7 * w * h) / count); +} diff --git a/src/scripts/dmn/shelf-icons.ts b/src/scripts/dmn/shelf-icons.ts @@ -0,0 +1,20 @@ +/** + * The six shelf icons the hero plate gathers into, in shelf order + * (index.astro: active-directory, enumeration, exploitation, + * privilege-escalation, web, dfir). lucide outlines (ISC), inlined as + * markup: the dot field samples them, nothing renders them. + */ +export const SHELF_ICONS: string[] = [ + /* network */ + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/></svg>', + /* radar */ + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M19.07 4.93A10 10 0 0 0 6.99 3.34"/><path d="M4 6h.01"/><path d="M2.29 9.62A10 10 0 1 0 21.31 8.35"/><path d="M16.24 7.76A6 6 0 1 0 8.23 16.67"/><path d="M12 18h.01"/><path d="M17.99 11.66A6 6 0 0 1 15.77 16.67"/><circle cx="12" cy="12" r="2"/><path d="m13.41 10.59 5.66-5.66"/></svg>', + /* bug */ + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20v-9"/><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"/><path d="M14.12 3.88 16 2"/><path d="M21 21a4 4 0 0 0-3.81-4"/><path d="M21 5a4 4 0 0 1-3.55 3.97"/><path d="M22 13h-4"/><path d="M3 21a4 4 0 0 1 3.81-4"/><path d="M3 5a4 4 0 0 0 3.55 3.97"/><path d="M6 13H2"/><path d="m8 2 1.88 1.88"/><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/></svg>', + /* key-round */ + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/></svg>', + /* globe */ + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>', + /* fingerprint */ + '<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M2 12a10 10 0 0 1 18-6"/><path d="M2 16h.01"/><path d="M21.8 16c.2-2 .131-5.354 0-6"/><path d="M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2"/><path d="M8.65 22c.21-.66.45-1.32.57-2"/><path d="M9 6.8a6 6 0 0 1 9 5.2v2"/></svg>', +]; diff --git a/src/scripts/dmn/viewer.ts b/src/scripts/dmn/viewer.ts @@ -0,0 +1,65 @@ +/** + * A small image viewer for sheet images, so the cursor's "View image" + * pill does what it says. A native <dialog>: Escape, a click anywhere or + * the close button dismisses it, focus returns to the image, and the + * page behind is inert while it is open. + * + * Images inside a link keep the link's behaviour; tiny inline images + * (badges, icons) are left alone. + */ +let dialog: HTMLDialogElement | null = null; +let shown: HTMLImageElement | null = null; + +function ensureDialog(): HTMLDialogElement { + if (dialog && dialog.isConnected) return dialog; + dialog = document.createElement('dialog'); + dialog.className = 'dmn-viewer'; + dialog.setAttribute('aria-label', 'Image viewer'); + const img = document.createElement('img'); + img.className = 'dmn-viewer__img'; + const cap = document.createElement('p'); + cap.className = 'dmn-viewer__cap'; + const close = document.createElement('button'); + close.type = 'button'; + close.className = 'dmn-viewer__close'; + close.textContent = 'Close'; + dialog.append(img, cap, close); + dialog.addEventListener('click', () => dialog?.close()); + dialog.addEventListener('close', () => shown?.focus({ preventScroll: true })); + document.body.appendChild(dialog); + return dialog; +} + +function open(from: HTMLImageElement): void { + const d = ensureDialog(); + const img = d.querySelector<HTMLImageElement>('.dmn-viewer__img')!; + const cap = d.querySelector<HTMLElement>('.dmn-viewer__cap')!; + img.src = from.currentSrc || from.src; + img.alt = from.alt; + cap.textContent = from.alt || ''; + cap.hidden = !from.alt; + shown = from; + d.showModal(); +} + +export function initViewer(): void { + document.querySelectorAll<HTMLImageElement>('.prose img:not([data-zoomable])').forEach((img) => { + if (img.closest('a')) return; + const mark = () => { + if (img.naturalWidth && img.naturalWidth < 120) return; + img.dataset.zoomable = ''; + img.tabIndex = 0; + img.setAttribute('role', 'button'); + img.setAttribute('aria-label', img.alt ? `Enlarge image: ${img.alt}` : 'Enlarge image'); + img.addEventListener('click', () => open(img)); + img.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + open(img); + } + }); + }; + if (img.complete) mark(); + else img.addEventListener('load', mark, { once: true }); + }); +} diff --git a/src/scripts/signal-field.ts b/src/scripts/signal-field.ts @@ -1,726 +0,0 @@ -/** - * The hero's atmosphere, drawn in WebGL on a canvas contained by its - * header: a network map. Hosts sit on an irregular - * lattice, drift a little on their own and are pushed away from the - * pointer, so the map bulges around the cursor and settles when it - * leaves. The hosts within the cursor's reach lock in the archive's REC - * red and the links between them light, so pointing at the page draws an - * attack path across it. - * - * Ported from the main site's `ambient-signal.tsx` (React + WebGL). One - * function, one canvas, its own teardown β€” the caller owns the element - * and the lifecycle; this file owns everything that happens on the GPU. - * - * ── Styles ── - * Four tellings of the same field. The site ships the constellation - * (`DEFAULT_STYLE`); `?field=N` on the URL swaps in another, for - * comparing them on the live site. All four stay in the fragment shader: - * the `st()` branches are uniform-constant, so they are coherent across - * every fragment and cost effectively nothing, and a byte-identical - * shader stays diffable against the original when the main site moves. - * 1 mesh hairline lattice with probes running the links - * 2 constellation a dense field of plain dots; links only appear near - * the cursor, and the cursor itself reaches out to them - * 3 circuit links routed at right angles, hosts as pads - * 4 radar soft hosts that ping as the cursor passes - * - * ── The clearing ── - * An element carrying `data-index-zone` asks the field to part around it: - * every host inside its box is pushed out through the nearest edge to a - * margin beyond it, and links, hosts and locks fade to nothing inside. - * The home hero marks its shelf navigation as this clearing. - * - * ── Colour ── - * No palette lives in the shader. The ink and the three accents are read - * off the canvas's own computed style, from the theme-relative tokens: - * `--text` and the `-soft` washes. Those invert with `[data-theme]` on - * `:root` and are re-pinned by `.plate` / `.plate-dawn` for their - * subtrees, so the cascade has already answered the question and the - * canvas only has to ask the right element β€” exactly how the banners' - * fuzz field resolves its own palette. The `-soft` washes stand in for - * the accents because the full-strength inks are set for text and go - * muddy as a wash. The canvas is transparent and composites over - * whatever ground it is given, so the same program is right on cream and - * on ink. Re-read on `daemonmodechange`, because a GPU uniform does not - * inherit a CSS variable. - * - * ── Cost ── - * The backing store is capped at device-pixel 1: the lines are a pixel - * wide and the fragment cost is per pixel. Layout is never read inside a - * frame or a pointer handler β€” the canvas box and the clearing are - * measured by observers into records β€” so neither a frame nor a pointer - * move can force a synchronous layout against whatever else the page is - * writing. The pointer is listened for on `window` rather than on the - * canvas, so the content in front keeps its hover and the canvas can - * stay `pointer-events: none`. - * - * ── Motion ── - * Under `prefers-reduced-motion: reduce` the loop never starts: one frame - * is drawn at a fixed phase and redrawn only when the canvas resizes or - * the mode flips. The loop pauses offscreen and while the tab is hidden. With no - * pointer on the page the cursor wanders the field on its own, so a touch - * screen still sees hosts being locked. - * - * ── Degradation ── - * No WebGL, a shader that will not compile, a program that will not link - * or a lost context all end the same way: the canvas is taken out of the - * DOM and the page keeps the ground it already had. The main site falls - * back to a second component here because the field is the only thing on - * that ground; this page's ground is already correct without it, so - * nothing is ported to stand in. - */ - -const VERT = ` -attribute vec2 a_pos; -void main() { gl_Position = vec4(a_pos, 0.0, 1.0); } -`; - -const FRAG = ` -precision highp float; -uniform vec2 u_res; -uniform float u_time; -uniform vec2 u_mouse; -uniform float u_scroll; -uniform float u_style; -/* The index rows' box in field units (x0, y0, x1, y1), or empty - (x1 <= x0) when nothing is asking the field to part. */ -uniform vec4 u_zone; -uniform vec3 u_ink; -uniform vec3 u_love; -uniform vec3 u_foam; -uniform vec3 u_iris; - -float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } -float hash3(vec2 p, float k) { return hash(p + vec2(k * 0.731, k * 1.129)); } - -bool st(int n) { return int(u_style + 0.5) == n; } - -/* Cells per unit of height: ~105px at a 808px hero, or ~80px for the - constellation, which is the dense one. */ -float S() { return st(2) ? 10.2 : 7.7; } - -/* The cursor, in field units; set once per fragment before the hosts - are placed, because a host's position depends on it. */ -vec2 g_cursor = vec2(0.0); -/* The keep-out box, in field units: the rows' box shifted with the - field's scroll offset. Set beside the cursor. */ -vec4 g_zone = vec4(0.0, 0.0, -1.0, -1.0); - -/* Signed distance to the keep-out box: negative inside. */ -float zoneSd(vec2 p) { - if (g_zone.z <= g_zone.x) return 1e3; - vec2 c = 0.5 * (g_zone.xy + g_zone.zw); - vec2 h = 0.5 * (g_zone.zw - g_zone.xy); - vec2 q = abs(p - c) - h; - return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0); -} - -/* The host in cell c, or far off-field if the cell is empty. Each host - drifts a little on its own, and the cursor pushes the ones near it - away with a soft falloff, so the links stretch and the map bulges - around the pointer and settles again when it leaves. */ -vec2 host(vec2 c) { - float keep = st(2) ? 0.9 : (st(4) ? 0.70 : 0.78); - if (hash(c) > keep) return vec2(-1e3); - vec2 h = (c + 0.22 + 0.56 * vec2(hash3(c, 1.0), hash3(c, 2.0))) / S(); - float k1 = hash3(c, 3.0); - float k2 = hash3(c, 4.0); - float drift = st(2) ? 0.012 : 0.011; - h += drift * vec2(sin(u_time * (0.28 + k1 * 0.4) + k1 * 6.2832), - cos(u_time * (0.22 + k2 * 0.4) + k2 * 6.2832)); - vec2 d = h - g_cursor; - float dist = length(d); - float push = smoothstep(0.36, 0.0, dist) * 0.07; - h += (d / max(dist, 1e-4)) * push; - /* out of the keep-out box, through its nearest edge, to a margin - beyond it β€” so the rows have clear ground and a clear surround */ - float margin = 0.04; - float sd = zoneSd(h); - if (sd < margin) { - vec2 c2 = 0.5 * (g_zone.xy + g_zone.zw); - vec2 hh = 0.5 * (g_zone.zw - g_zone.xy); - vec2 q = abs(h - c2) - hh; - vec2 dir = (q.x > q.y) ? vec2(sign(h.x - c2.x), 0.0) : vec2(0.0, sign(h.y - c2.y)); - h += dir * (margin - sd); - } - return h; -} - -/* Distance from p to segment ab, and the position along it. */ -vec2 seg(vec2 p, vec2 a, vec2 b) { - vec2 ab = b - a; - float t = clamp(dot(p - a, ab) / max(1e-6, dot(ab, ab)), 0.0, 1.0); - return vec2(length(p - (a + ab * t)), t); -} - -/* Whether a link exists between cell a and its neighbour in direction d. */ -bool linked(vec2 a, vec2 d) { - float h = hash(a * 3.1 + d * 17.7); - if (st(3)) return (abs(d.x) + abs(d.y) > 1.5) ? h > 0.85 : h > 0.38; - return (abs(d.x) + abs(d.y) > 1.5) ? h > 0.72 : h > 0.42; -} - -/* The output, built up as premultiplied colour over a clear ground: - each layer is composited "over" what is there, which on an opaque - ground is exactly the mix() chain it replaces. */ -vec4 g_acc = vec4(0.0); -void lay(vec3 c, float a) { - a = clamp(a, 0.0, 1.0); - g_acc = vec4(g_acc.rgb * (1.0 - a) + c * a, g_acc.a * (1.0 - a) + a); -} - -void main() { - float aspect = u_res.x / u_res.y; - vec2 uv = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y; - vec2 mu = u_mouse * vec2(aspect * 0.5, 0.5); - /* The field slides up a little as the page is scrolled past. */ - vec2 fp = uv + vec2(0.0, u_scroll * 0.12); - vec2 fm = mu + vec2(0.0, u_scroll * 0.12); - g_cursor = fm; - g_zone = u_zone + vec4(0.0, u_scroll * 0.12, 0.0, u_scroll * 0.12); - - float px = 1.0 / u_res.y; - - /* The 64px hairline grid the signal map draws; the circuit takes a - finer 32px one. The constellation and the radar have none. */ - if (st(1) || st(3)) { - float cellPx = st(3) ? 32.0 : 64.0; - vec2 gp = gl_FragCoord.xy / cellPx; - vec2 gq = abs(fract(gp) - 0.5); - float grid = 1.0 - smoothstep(0.0, 1.5 / cellPx, min(0.5 - gq.x, 0.5 - gq.y)); - lay(u_ink, grid * (st(3) ? 0.035 : 0.045)); - } - - float reach = st(2) ? 0.28 : 0.24; - vec2 cell = floor(fp * S()); - float linkA = 0.0; /* resting link ink */ - float pathA = 0.0; /* links lit by the lock */ - float probeA = 0.0; /* probes on the links */ - float hostA = 0.0; /* resting host dots */ - float haloA = 0.0; /* the soft glow around a host */ - float lockA = 0.0; /* locked hosts */ - float ringA = 0.0; /* the lock ring */ - float grabA = 0.0; /* constellation: cursor-to-host lines */ - float pingA = 0.0; /* radar: expanding rings */ - - /* Two cells each way, because a pushed host can leave its own cell; - anything farther than that cannot reach this pixel and is skipped - before its links are measured. */ - for (int j = -2; j <= 2; j++) { - for (int i = -2; i <= 2; i++) { - vec2 a = cell + vec2(float(i), float(j)); - vec2 ha = host(a); - if (ha.x < -100.0) continue; - if (length(ha - fp) > 2.3 / S()) continue; - - float toCursor = length(ha - fm); - float la = smoothstep(reach, reach * 0.55, toCursor); - - /* The constellation only shows links near the cursor, so links - whose near end is well out of reach are not measured at all β€” - which is most of them, on most of the screen. */ - bool links = !(st(2) && toCursor > reach + 2.0 / S()); - - if (links) for (int k = 0; k < 4; k++) { - vec2 d = (k == 0) ? vec2(1.0, 0.0) : (k == 1) ? vec2(0.0, 1.0) - : (k == 2) ? vec2(1.0, 1.0) : vec2(1.0, -1.0); - if (!linked(a, d)) continue; - vec2 hb = host(a + d); - if (hb.x < -100.0) continue; - float lb = smoothstep(reach, reach * 0.55, length(hb - fm)); - float lit = la * lb; - - float along = 0.0; - float dist = 0.0; - if (st(3)) { - /* routed at a right angle through a corner, the corner's side - chosen per link so the routes do not all turn the same way */ - vec2 corner = hash(a * 7.7 + d) > 0.5 ? vec2(hb.x, ha.y) : vec2(ha.x, hb.y); - vec2 s1 = seg(fp, ha, corner); - vec2 s2 = seg(fp, corner, hb); - float l1 = length(corner - ha); - float l2 = length(hb - corner); - float L = max(1e-5, l1 + l2); - if (s1.x < s2.x) { dist = s1.x; along = s1.y * l1 / L; } - else { dist = s2.x; along = (l1 + s2.y * l2) / L; } - } else { - vec2 s = seg(fp, ha, hb); - dist = s.x; - along = s.y; - } - float line = 1.0 - smoothstep(0.0, 1.4 * px, dist - 0.35 * px); - linkA = max(linkA, line); - pathA = max(pathA, line * lit); - - /* a probe runs the link if the link carries traffic */ - float eh = hash(a * 5.3 + d * 2.9); - if ((st(1) || st(3)) && eh > 0.55) { - float dir = eh > 0.78 ? 1.0 : -1.0; - float pos = fract(u_time * (0.10 + eh * 0.08) * dir + eh * 7.0); - float dp = along - pos; - float head = exp(-dp * dp * 2200.0); - float tail = (dir > 0.0) - ? ((dp < 0.0) ? exp(dp * 14.0) : 0.0) - : ((dp > 0.0) ? exp(-dp * 14.0) : 0.0); - float on = 1.0 - smoothstep(0.0, 2.2 * px, dist); - probeA = max(probeA, on * (head + tail * 0.35) * (0.35 + 0.65 * lit)); - } - } - - /* the host itself */ - vec2 rel = fp - ha; - float dh = st(3) ? max(abs(rel.x), abs(rel.y)) : length(rel); - float r0 = st(4) ? 2.0 * px : 1.2 * px; - hostA = max(hostA, 1.0 - smoothstep(r0, r0 + 1.2 * px, dh)); - if (st(4)) haloA = max(haloA, exp(-dh * dh * 9000.0)); - lockA = max(lockA, (1.0 - smoothstep(r0 + 0.4 * px, r0 + 1.8 * px, dh)) * la); - float ring = abs(dh - 7.0 * px); - ringA = max(ringA, (1.0 - smoothstep(0.0, 1.3 * px, ring)) * la); - - if (st(2) && la > 0.0) { - /* the cursor reaches out to what it can see */ - vec2 g = seg(fp, fm, ha); - float gl = 1.0 - smoothstep(0.0, 1.4 * px, g.x - 0.35 * px); - grabA = max(grabA, gl * la * (0.4 + 0.6 * (1.0 - g.y))); - } - if (st(4)) { - /* each host pings while the cursor is near, and now and then on - its own; the ring grows out and fades */ - float ph = hash3(a, 9.0); - float idle = step(0.86, ph) * 0.35; - float amp = max(la, idle); - float cyc = fract(u_time * (0.35 + ph * 0.25) + ph * 3.0); - float rr = cyc * 0.11; - float band = abs(length(rel) - rr); - float pr = (1.0 - smoothstep(0.0, 1.2 * px, band)) * (1.0 - cyc) * amp; - pingA = max(pingA, pr); - } - } - } - - /* Nothing of the map is drawn inside the keep-out box; it fades - over the last few pixels before the edge. */ - float keep = smoothstep(0.0, 0.02, zoneSd(fp)); - linkA *= keep; pathA *= keep; probeA *= keep; hostA *= keep; - haloA *= keep; lockA *= keep; ringA *= keep; grabA *= keep; pingA *= keep; - - if (st(1)) { - lay(u_ink, linkA * 0.075); - lay(u_love, pathA * 0.6); - lay(u_foam, probeA * 0.85); - lay(u_ink, hostA * 0.32); - } else if (st(2)) { - lay(u_ink, linkA * 0.03); - lay(u_love, pathA * 0.5); - lay(u_iris, grabA * 0.45); - lay(u_ink, hostA * 0.38); - } else if (st(3)) { - lay(u_ink, linkA * 0.09); - lay(u_love, pathA * 0.6); - lay(u_foam, probeA * 0.9); - lay(u_ink, hostA * 0.4); - } else { - lay(u_ink, linkA * 0.05); - lay(u_love, pathA * 0.45); - lay(u_foam, haloA * 0.22); - lay(u_foam, pingA * 0.6); - lay(u_ink, hostA * 0.45); - } - lay(u_love, lockA * 0.95); - lay(u_love, ringA * 0.7); - - /* The pointer's own wash: a wide, faint iris. */ - vec2 mp = uv - mu; - lay(u_iris, exp(-dot(mp, mp) * 9.0) * 0.07); - - gl_FragColor = g_acc; -} -`; - -/** One of the four tellings; `?field=N` picks between them. */ -type SignalStyle = 1 | 2 | 3 | 4; - -/** The style the site ships with: the constellation. */ -const DEFAULT_STYLE: SignalStyle = 2; - -const TOKENS = ['ink', 'love', 'foam', 'iris'] as const; -type Token = (typeof TOKENS)[number]; -type Rgb = [number, number, number]; - -/** #rrggbb to 0–1 channels. Anything else gives the fallback. */ -function hexToRgb(hex: string, fallback: Rgb): Rgb { - const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim()); - if (!m) return fallback; - const n = parseInt(m[1], 16); - return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]; -} - -/** The night palette, for when a token is missing or is not a plain hex. */ -const FALLBACK: Record<Token, Rgb> = { - ink: hexToRgb('#e0def4', [0.88, 0.87, 0.96]), - love: hexToRgb('#eb6f92', [0.92, 0.44, 0.57]), - foam: hexToRgb('#9ccfd8', [0.61, 0.81, 0.85]), - iris: hexToRgb('#c4a7e7', [0.77, 0.65, 0.9]), -}; - -/** - * The custom property each uniform reads. Theme-relative on purpose, not - * the absolute `--color-night-*` / `--color-dawn-*` set: these four - * invert with `[data-theme]` on `:root` and are re-pinned by `.plate` and - * `.plate-dawn` for their subtrees, so the field follows a retuned plate - * without a code change. All four resolve to a plain `#rrggbb`, which is - * what `hexToRgb` wants β€” no `color-mix()` values are involved. - */ -const SOURCES: Record<Token, string> = { - ink: '--text', - love: '--love-soft', - foam: '--foam-soft', - iris: '--iris-soft', -}; - -function readPalette(el: HTMLElement): Record<Token, Rgb> { - const cs = getComputedStyle(el); - const out = {} as Record<Token, Rgb>; - for (const t of TOKENS) out[t] = hexToRgb(cs.getPropertyValue(SOURCES[t]), FALLBACK[t]); - return out; -} - -function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader | null { - const sh = gl.createShader(type); - if (!sh) return null; - gl.shaderSource(sh, src); - gl.compileShader(sh); - if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { - gl.deleteShader(sh); - return null; - } - return sh; -} - -/** - * `?field=N` on the URL overrides the shipped style, for comparing the - * four side by side on the live site. Anything else is the default. - */ -function styleFromUrl(): SignalStyle | null { - try { - const n = Number(new URLSearchParams(window.location.search).get('field')); - return n === 1 || n === 2 || n === 3 || n === 4 ? n : null; - } catch { - return null; - } -} - -/** The phase the still frame is drawn at: probes spread, hosts settled. */ -const HELD_TIME = 31.0; -/** How long the pointer may rest before the cursor starts to wander. */ -const IDLE_MS = 2800; - -/** - * Bind the field to `canvas` and start it. Returns the teardown: it - * cancels the frame, drops every listener and observer, and deletes the - * GL objects. Safe to call twice β€” the context-lost handler tears down on - * its own, and the caller's `astro:before-swap` will still fire. - * - * If WebGL is unavailable, or the program will not build, the canvas is - * removed from the DOM and the teardown is a no-op. That is the whole - * degradation signal: the element the caller handed over is gone, and - * nothing is left running. - */ -export function mountSignalField(canvas: HTMLCanvasElement): () => void { - const noop = (): void => {}; - - /* Nothing to fall back to and nothing to say: the page's ground is - already right without the field, so the canvas simply leaves. */ - const degrade = (): void => { canvas.remove(); }; - - const attrs: WebGLContextAttributes = { - alpha: true, - premultipliedAlpha: true, - antialias: false, - depth: false, - stencil: false, - powerPreference: 'low-power', - }; - const gl = - canvas.getContext('webgl', attrs) || - (canvas.getContext('experimental-webgl', attrs) as WebGLRenderingContext | null); - if (!gl) { - degrade(); - return noop; - } - - const vs = compile(gl, gl.VERTEX_SHADER, VERT); - const fs = compile(gl, gl.FRAGMENT_SHADER, FRAG); - const prog = gl.createProgram(); - /* Whatever did survive is released here rather than left for the - context to collect: React unmounted the whole canvas on this path, so - the original never had to, but here the caller keeps its page. */ - if (!vs || !fs || !prog) { - if (vs) gl.deleteShader(vs); - if (fs) gl.deleteShader(fs); - if (prog) gl.deleteProgram(prog); - degrade(); - return noop; - } - gl.attachShader(prog, vs); - gl.attachShader(prog, fs); - gl.linkProgram(prog); - if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { - gl.deleteProgram(prog); - gl.deleteShader(vs); - gl.deleteShader(fs); - degrade(); - return noop; - } - gl.useProgram(prog); - - const buf = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, buf); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), - gl.STATIC_DRAW, - ); - const aPos = gl.getAttribLocation(prog, 'a_pos'); - gl.enableVertexAttribArray(aPos); - gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0); - gl.clearColor(0, 0, 0, 0); - - const u = (name: string): WebGLUniformLocation | null => gl.getUniformLocation(prog, name); - const uRes = u('u_res'); - const uTime = u('u_time'); - const uMouse = u('u_mouse'); - const uScroll = u('u_scroll'); - const uZone = u('u_zone'); - const uCol = {} as Record<Token, WebGLUniformLocation | null>; - for (const t of TOKENS) uCol[t] = u(`u_${t}`); - gl.uniform1f(u('u_style'), styleFromUrl() ?? DEFAULT_STYLE); - gl.uniform4f(uZone, 0, 0, -1, -1); - - let palette = readPalette(canvas); - const pushPalette = (): void => { - for (const t of TOKENS) gl.uniform3fv(uCol[t], palette[t]); - }; - pushPalette(); - - /* ── State the frame reads ── */ - const mouse = { x: 0.3, y: 0.1 }; // where the cursor should be, βˆ’1…1 - const eased = { x: 0.3, y: 0.1 }; // where the shader thinks it is - let lastMove = -Infinity; - let scroll = 0; - let raf = 0; - let running = false; - let visible = !document.hidden; - let inView = false; - let lost = false; - const live = !matchMedia('(prefers-reduced-motion: reduce)').matches; - const start = performance.now(); - - // Store page coordinates so pointer tracking stays aligned after scrolling. - const box = { left: 0, pageTop: 0, width: 1, height: 1 }; - const measureBox = (): void => { - const r = canvas.getBoundingClientRect(); - box.left = r.left; - box.pageTop = r.top + window.scrollY; - box.width = r.width || 1; - box.height = r.height || 1; - }; - - const size = (): void => { - /* Device-pixel 1, whatever the display: the lines are a pixel wide, - the field is a background, and every extra pixel is a host loop. */ - const w = Math.max(1, Math.round(canvas.clientWidth)); - const h = Math.max(1, Math.round(canvas.clientHeight)); - if (canvas.width !== w || canvas.height !== h) { - canvas.width = w; - canvas.height = h; - } - gl.viewport(0, 0, w, h); - gl.uniform2f(uRes, w, h); - }; - - // Observe the local shelf clearing; the home hero rebuilds its rows on - // theme and breakpoint changes without replacing this canvas. - const host = canvas.parentElement!; - let zoneEl: HTMLElement | null = null; - let zoneRo: ResizeObserver | null = null; - const measureZone = (): void => { - if (!zoneEl || !zoneEl.isConnected) { - gl.uniform4f(uZone, 0, 0, -1, -1); - return; - } - const c = canvas.getBoundingClientRect(); - const z = zoneEl.getBoundingClientRect(); - const H = c.height || 1; - gl.uniform4f( - uZone, - (z.left - c.left - c.width / 2) / H, - (c.height / 2 - (z.bottom - c.top)) / H, - (z.right - c.left - c.width / 2) / H, - (c.height / 2 - (z.top - c.top)) / H, - ); - }; - const findZone = (): void => { - const el = host.querySelector<HTMLElement>('[data-index-zone]'); - if (el === zoneEl) return; - zoneRo?.disconnect(); - zoneEl = el; - if (el) { - zoneRo = new ResizeObserver(measureZone); - zoneRo.observe(el); - } - measureZone(); - if (!running) still(); - }; - const zoneChanges = new MutationObserver(findZone); - zoneChanges.observe(host, { childList: true }); - - const draw = (t: number): void => { - if (lost) return; - gl.uniform1f(uTime, t); - gl.uniform2f(uMouse, eased.x, eased.y); - gl.uniform1f(uScroll, scroll); - gl.clear(gl.COLOR_BUFFER_BIT); - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); - }; - - const still = (): void => { - size(); - eased.x = 0.3; - eased.y = 0.1; - draw(HELD_TIME); - }; - - const tick = (now: number): void => { - raf = requestAnimationFrame(tick); - const t = (now - start) / 1000; - /* Left alone, the cursor walks a slow figure across the field β€” a - touch screen never sends a pointer, and the lock is the piece's - whole gesture. */ - if (now - lastMove > IDLE_MS) { - mouse.x = 0.45 * Math.sin(t * 0.19) + 0.15 * Math.sin(t * 0.47); - mouse.y = 0.4 * Math.cos(t * 0.14) + 0.12 * Math.cos(t * 0.37); - } - /* A 0.07 step settles in about a third of a second: weight, not lag. */ - eased.x += (mouse.x - eased.x) * 0.07; - eased.y += (mouse.y - eased.y) * 0.07; - draw(t); - }; - - // Stop spending GPU time once the reader scrolls past the hero. - const sync = (): void => { - const want = live && visible && inView; - if (want && !running) { - running = true; - size(); - raf = requestAnimationFrame(tick); - } else if (!want && running) { - running = false; - cancelAnimationFrame(raf); - } - if (!live) still(); - }; - - /* The pointer, relative to the canvas, βˆ’1…1 on both axes, placed from - the measured box rather than a fresh rect. Listened for on the window - because the canvas does not take events β€” the content in front of it - must keep its hover. */ - const onMove = (e: PointerEvent): void => { - if (e.pointerType === 'touch') return; - const x = ((e.clientX - box.left) / box.width) * 2 - 1; - const top = box.pageTop - window.scrollY; - const y = -(((e.clientY - top) / box.height) * 2 - 1); - /* Off the field the cursor is handed back to the wanderer. */ - if (x < -1.05 || x > 1.05 || y < -1.05 || y > 1.05) return; - mouse.x = x; - mouse.y = y; - lastMove = e.timeStamp; - }; - // Parallax is bounded to the height of this hero. - const onScroll = (): void => { - scroll = Math.max(0, Math.min(1, window.scrollY / box.height)); - }; - const onVis = (): void => { - visible = !document.hidden; - sync(); - }; - /* A GPU uniform does not inherit. The variables have already changed by - the time this fires, so re-reading them is all it takes β€” but a still - frame has to be redrawn by hand, or the field keeps the old palette - until something else resizes it. */ - const onMode = (): void => { - palette = readPalette(canvas); - pushPalette(); - if (!running) still(); - }; - const onLost = (e: Event): void => { - e.preventDefault(); - lost = true; - teardown(); - degrade(); - }; - - const ro = new ResizeObserver(() => { - measureBox(); - size(); - measureZone(); - if (!running) still(); - }); - ro.observe(canvas); - const io = new IntersectionObserver(([entry]) => { - inView = entry.isIntersecting; - measureBox(); - sync(); - }); - io.observe(canvas); - - /* No pointer listener under reduced motion: there is no loop to read - the cursor, and the still frame is drawn at a fixed one. */ - if (live) window.addEventListener('pointermove', onMove, { passive: true }); - window.addEventListener('scroll', onScroll, { passive: true }); - window.addEventListener('resize', measureBox); - document.addEventListener('visibilitychange', onVis); - window.addEventListener('daemonmodechange', onMode); - canvas.addEventListener('webglcontextlost', onLost); - - /* An arrow rather than a declaration, and placed here rather than - beside the handler that calls it: a hoisted `function` could in - principle run before `gl` was narrowed, so the compiler widens it - back to possibly-null inside the body. `onLost` reaches forward to - this binding, which is sound because it can only be called once the - listener above it has been bound. */ - let torn = false; - const teardown = (): void => { - if (torn) return; - torn = true; - running = false; - cancelAnimationFrame(raf); - zoneChanges.disconnect(); - ro.disconnect(); - io.disconnect(); - zoneRo?.disconnect(); - window.removeEventListener('pointermove', onMove); - window.removeEventListener('scroll', onScroll); - window.removeEventListener('resize', measureBox); - document.removeEventListener('visibilitychange', onVis); - window.removeEventListener('daemonmodechange', onMode); - canvas.removeEventListener('webglcontextlost', onLost); - gl.deleteBuffer(buf); - gl.deleteProgram(prog); - gl.deleteShader(vs); - gl.deleteShader(fs); - /* The context is left to the GC rather than lost by hand: a forced - loss fires `webglcontextlost` after this cleanup has run, and the - next mount of the same canvas would hear it as a real loss and - degrade for no reason. That matters more here than in the original, - because a view-transition navigation unmounts and mounts in quick - succession. */ - }; - - measureBox(); - onScroll(); - findZone(); - sync(); - - return teardown; -} diff --git a/src/styles/app.css b/src/styles/app.css @@ -4,3 +4,4 @@ @import './prose.css'; @import './flow.css'; @import './daemon.css'; +@import './dmn.css'; diff --git a/src/styles/dmn.css b/src/styles/dmn.css @@ -0,0 +1,121 @@ +/* ═══════════════════════════════════════════════════════ + * Shared with daemon-sec.xyz: the dot field, the cursor pill and the + * image viewer. Scripts: src/scripts/dmn/. + * ═══════════════════════════════════════════════════════ */ + +/* Every field arrives on a long fade the first time it draws. */ +.signal-field { opacity: 0; transition: opacity 2400ms var(--ease-out-soft); } +.signal-field.is-live { opacity: 1; } + +/* The hero plate's dots stay out of the way of its words: full strength + in the open upper half where the icon forms, a trace over the copy. */ +.dmn-plate-dots { + opacity: 0; + transition: opacity 2400ms var(--ease-out-soft); + -webkit-mask-image: linear-gradient(180deg, rgb(0 0 0 / 0.25) 0%, #000 14%, #000 46%, rgb(0 0 0 / 0.12) 60%, rgb(0 0 0 / 0.08) 100%); + mask-image: linear-gradient(180deg, rgb(0 0 0 / 0.25) 0%, #000 14%, #000 46%, rgb(0 0 0 / 0.12) 60%, rgb(0 0 0 / 0.08) 100%); +} +.dmn-plate-dots.is-live { opacity: 1; } + +/* ── Skip link ── */ +.skip-link { + position: absolute; + left: 12px; + top: -60px; + z-index: 100; + padding: 10px 16px; + background: var(--fg); + color: var(--bg); + font: 500 11px/1 var(--font-mono); + letter-spacing: 0.16em; + text-transform: uppercase; +} +.skip-link:focus { top: 12px; } + +/* ── The cursor ── */ +@media (pointer: fine) { + html.has-dmn-cursor, + html.has-dmn-cursor * { cursor: none; } + + .dmn-cursor { + position: fixed; + top: 0; + left: 0; + z-index: 99999; + width: 9px; + height: 9px; + border: 1px solid transparent; + border-radius: 50%; + background: var(--love); + pointer-events: none; + transform: translate(-50%, -50%); + transition: width 180ms ease, height 180ms ease, background-color 180ms ease, border-color 180ms ease, opacity 200ms ease; + } + .dmn-cursor.on-dark { background: var(--color-night-love); } + .dmn-cursor.hot { width: 34px; height: 34px; background: transparent; border-color: var(--love); } + .dmn-cursor.hot.on-dark { border-color: var(--color-night-love); } + + .dmn-cursor-pill { + position: absolute; + top: 50%; + left: 50%; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 14px 8px 11px; + border: 1px solid color-mix(in srgb, var(--text) 25%, transparent); + border-radius: 999px; + background: var(--surface); + color: var(--text); + font: 500 11.5px/1 var(--font-mono); + letter-spacing: 0.08em; + text-transform: uppercase; + white-space: nowrap; + opacity: 0; + transform: translate(-50%, -50%) scale(0); + transition: transform 200ms cubic-bezier(0.4, 0, 1, 1), opacity 160ms ease; + } + .dmn-cursor-icon { display: inline-flex; } + .dmn-cursor[data-mode] { background: transparent; border-color: transparent; } + .dmn-cursor[data-mode] .dmn-cursor-pill { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + transition: transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 200ms ease; + } + .dmn-cursor.on-dark .dmn-cursor-pill { + border-color: color-mix(in srgb, var(--color-night-text) 28%, transparent); + background: var(--color-night-surface); + color: var(--color-night-text); + } +} +@media (pointer: coarse) { .dmn-cursor { display: none; } } + +/* ── Image viewer ── */ +.prose img[data-zoomable] { cursor: zoom-in; } +.dmn-viewer { + max-width: min(96vw, 1600px); + max-height: 94vh; + padding: 0; + border: 1px solid var(--rule-hi); + background: var(--plate); + color: var(--color-night-text); +} +.dmn-viewer::backdrop { background: rgb(25 23 36 / 0.82); } +.dmn-viewer__img { display: block; max-width: 100%; max-height: 86vh; margin: 0 auto; object-fit: contain; } +.dmn-viewer__cap { margin: 0; padding: 10px 14px; font: 400 12px/1.5 var(--font-mono); color: var(--color-night-subtle); } +.dmn-viewer__close { + position: absolute; + top: 8px; + right: 8px; + padding: 6px 10px; + border: 1px solid color-mix(in srgb, var(--color-night-text) 30%, transparent); + background: var(--color-night-base); + color: var(--color-night-text); + font: 500 11px/1 var(--font-mono); + letter-spacing: 0.12em; + text-transform: uppercase; +} +@media (prefers-reduced-motion: no-preference) { + .dmn-viewer[open] { animation: dmn-viewer-in 420ms var(--ease-out-expo); } +} +@keyframes dmn-viewer-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: none; } } diff --git a/src/styles/global.css b/src/styles/global.css @@ -56,20 +56,19 @@ main, .site-header, .site-footer { position: relative; z-index: 2; } keeps the entrance in the site's terminal/register language rather than a generic card float. */ @media (prefers-reduced-motion: no-preference) { + /* The daemon-sec entrance: about a second on a curve that spends most + of its travel in the last fifth. */ .scroll-reveal { opacity: 0; - transform: translate3d(-8px, 22px, 0); - filter: blur(2px); + transform: translate3d(0, 28px, 0); transition: - opacity 540ms cubic-bezier(0.22, 0.61, 0.36, 1) var(--reveal-delay, 0ms), - transform 620ms var(--ease-in) var(--reveal-delay, 0ms), - filter 460ms ease var(--reveal-delay, 0ms); - will-change: opacity, transform, filter; + opacity var(--dur-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms), + transform var(--dur-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms); + will-change: opacity, transform; } .scroll-reveal.is-revealed { opacity: 1; transform: translate3d(0, 0, 0); - filter: blur(0); will-change: auto; } } diff --git a/src/styles/tokens.css b/src/styles/tokens.css @@ -257,6 +257,10 @@ /* The site's entrance curve β€” slower out, hard landing. Distinct from `--ease`, which is for hover-scale transitions. */ --ease-in: cubic-bezier(0.2, 0.7, 0.2, 1); + /* The entrance vocabulary shared with daemon-sec.xyz. */ + --ease-out-soft: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --dur-reveal: 1000ms; } /* ---- Plate: the DΓ†MON signature β€” an inverse (night) panel on the cream diff --git a/test/hero-and-shelves.test.mjs b/test/hero-and-shelves.test.mjs @@ -9,8 +9,8 @@ async function documentAt(route) { return window.document; } -test('signal canvases belong to page heroes and never the reading area', async () => { - for (const route of ['', 'osint/', 'sheets/enumeration/2-4-cheatsheet-gitleaks/']) { +test('dot fields belong to page heroes and never the reading area', async () => { + for (const route of ['', 'osint/']) { const document = await documentAt(route); const fields = [...document.querySelectorAll('[data-signal]')]; assert.equal(fields.length, 1, route); @@ -20,6 +20,22 @@ test('signal canvases belong to page heroes and never the reading area', async ( } }); +test('sheet headers carry the fuzz field, not the dot field', async () => { + const document = await documentAt('sheets/enumeration/2-4-cheatsheet-gitleaks/'); + assert.equal(document.querySelectorAll('[data-signal]').length, 0); + const fuzz = [...document.querySelectorAll('[data-fuzz]')]; + assert.equal(fuzz.length, 1); + assert.ok(fuzz[0].closest('.signal-hero')); + assert.equal(document.querySelector('article [data-fuzz]'), null); +}); + +test('home has one h1 and a skip link to main', async () => { + const document = await documentAt(''); + assert.equal(document.querySelectorAll('h1').length, 1); + assert.equal(document.querySelector('a.skip-link')?.getAttribute('href'), '#main'); + assert.ok(document.querySelector('main#main')); +}); + test('OSINT and Enumeration both link to the original secret-scanner sheets', async () => { for (const route of ['osint/', 'enumeration/']) { const document = await documentAt(route);