Files
org_web/play/the-rain-index.org
gitea-actions 2bb816b2ae
All checks were successful
Build Org Website / build (push) Successful in 30s
change mode and fix build
2026-07-08 22:42:05 +01:00

1333 lines
44 KiB
Org Mode
Executable File

#+TITLE: The Rain Index
#+OPTIONS: num:nil title:nil toc:nil
#+NO_SIDENOTES: t
#+DATE: <2026-05-16 Sat>
#+BEGIN_EXPORT html
<style>
.content:has(.game),
#content.rain-index-content {
max-width: none !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
body:has(.game),
body.rain-index-page {
margin: 0;
overflow-x: hidden;
background:
radial-gradient(circle at 8% 16%, rgba(163, 111, 44, 0.2), transparent 26rem),
radial-gradient(circle at 82% 12%, rgba(38, 56, 74, 0.22), transparent 24rem),
linear-gradient(125deg, #1e1a17, #473424 46%, #151c20);
}
body:has(.game) #postamble,
body.rain-index-page #postamble {
display: none;
}
.game {
--ink: #211b16;
--brown: #5f4631;
--paper: #e8ddc3;
--paper-2: #d7c6a7;
--paper-3: #f4ecd9;
--green: #243d35;
--blue: #26384a;
--red: #7f3330;
--gold: #a36f2c;
--glass: rgba(244, 236, 217, 0.74);
--shadow: rgba(28, 20, 13, 0.32);
--crt: rgba(143, 202, 164, 0.18);
color-scheme: light;
font-family: Georgia, "Times New Roman", serif;
position: relative;
width: 100%;
min-height: 680px;
height: 100dvh;
max-height: 100dvh;
overflow: hidden;
isolation: isolate;
cursor: none;
color: var(--ink);
background:
radial-gradient(circle at 8% 16%, rgba(163, 111, 44, 0.2), transparent 26rem),
radial-gradient(circle at 82% 12%, rgba(38, 56, 74, 0.22), transparent 24rem),
linear-gradient(125deg, #1e1a17, #473424 46%, #151c20);
}
.game *,
.game *::before,
.game *::after {
box-sizing: border-box;
}
.game::before,
.game::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
z-index: 40;
}
.game::before {
opacity: 0.32;
background:
repeating-linear-gradient(0deg, rgba(255,255,255,0.04) 0 1px, transparent 1px 4px),
repeating-linear-gradient(90deg, rgba(25,17,11,0.06) 0 1px, transparent 1px 9px);
mix-blend-mode: soft-light;
}
.game::after {
opacity: 0.34;
background-image:
linear-gradient(90deg, transparent 0 12px, rgba(255,255,255,0.05) 13px, transparent 14px),
radial-gradient(circle at 22% 30%, rgba(255,255,255,0.09), transparent 0.15rem),
radial-gradient(circle at 76% 68%, rgba(0,0,0,0.1), transparent 0.18rem);
background-size: 17px 100%, 23px 31px, 29px 37px;
animation: rain 1.8s linear infinite;
}
.game button,
.game input {
font: inherit;
}
.game button {
color: inherit;
}
.desk {
position: absolute;
inset: clamp(10px, 2vw, 28px);
overflow: hidden;
border: 1px solid rgba(232, 221, 195, 0.34);
border-radius: 8px;
background:
linear-gradient(90deg, rgba(255,255,255,0.03), transparent 20% 72%, rgba(0,0,0,0.18)),
repeating-linear-gradient(6deg, rgba(244,236,217,0.045) 0 1px, transparent 1px 12px),
linear-gradient(145deg, rgba(232,221,195,0.16), rgba(20,17,14,0.18));
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.42), inset 0 0 90px rgba(0,0,0,0.28);
}
.zones {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
pointer-events: none;
opacity: 0.5;
transition: opacity 400ms ease;
}
.zone {
position: relative;
border: 1px dashed rgba(232, 221, 195, 0.14);
color: rgba(232, 221, 195, 0.32);
font: 700 0.68rem/1.1 "Courier New", monospace;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.zone span {
position: absolute;
left: 0.8rem;
top: 0.75rem;
}
.zone.is-lit {
background: rgba(232, 221, 195, 0.07);
color: rgba(245, 234, 201, 0.62);
}
.masthead {
position: absolute;
left: clamp(18px, 4vw, 54px);
top: clamp(16px, 4vw, 42px);
z-index: 12;
max-width: min(560px, calc(100vw - 2rem));
color: #efe4c9;
text-shadow: 0 2px 18px rgba(0,0,0,0.65);
user-select: none;
transition: transform 600ms ease, filter 600ms ease;
}
.masthead small {
display: block;
margin-bottom: 0.35rem;
color: rgba(236, 222, 186, 0.72);
font: 700 0.72rem/1.3 "Courier New", monospace;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.masthead h1 {
margin: 0;
font-size: clamp(2.4rem, 10vw, 7.4rem);
font-weight: 500;
line-height: 0.83;
letter-spacing: 0;
}
.masthead p {
width: min(39rem, 88vw);
margin: 1rem 0 0;
color: rgba(239, 228, 201, 0.76);
font-size: clamp(1rem, 1.8vw, 1.25rem);
line-height: 1.45;
}
.no-explain {
opacity: 0;
transform: translateY(5px);
transition: opacity 600ms ease, transform 600ms ease;
}
.game.has-touched .no-explain {
opacity: 1;
transform: translateY(0);
}
.machine {
position: absolute;
right: clamp(14px, 3vw, 42px);
top: clamp(14px, 3vw, 36px);
z-index: 20;
width: min(370px, calc(100vw - 2rem));
color: #d9f0cf;
font: 0.78rem/1.45 "Courier New", monospace;
letter-spacing: 0.02em;
}
.terminal {
min-height: 142px;
padding: 0.85rem;
border: 1px solid rgba(121, 205, 156, 0.28);
border-radius: 6px;
background:
radial-gradient(circle at 40% 0%, rgba(150, 255, 177, 0.12), transparent 60%),
linear-gradient(#122019, #09110f);
box-shadow: inset 0 0 34px rgba(90, 255, 136, 0.07), 0 14px 38px rgba(0,0,0,0.34);
overflow: hidden;
}
.terminal p {
margin: 0 0 0.35rem;
}
.terminal .line {
min-height: 1.25em;
white-space: pre-wrap;
}
.meter {
display: grid;
grid-template-columns: 1fr auto;
gap: 0.45rem 0.8rem;
align-items: center;
margin-top: 0.65rem;
color: rgba(239, 228, 201, 0.82);
}
.bar {
height: 10px;
border: 1px solid rgba(239, 228, 201, 0.24);
border-radius: 999px;
overflow: hidden;
background: rgba(0,0,0,0.24);
}
.bar i {
display: block;
width: var(--w, 0%);
height: 100%;
background: linear-gradient(90deg, var(--gold), #d9d0a2, #7fc798);
transition: width 360ms ease;
}
.rooms {
position: absolute;
inset: 0;
z-index: 10;
}
.room {
position: absolute;
width: clamp(205px, 23vw, 310px);
min-height: 245px;
padding: 1rem 1rem 0.95rem;
border: 1px solid rgba(69, 52, 36, 0.36);
border-radius: 6px;
background:
linear-gradient(110deg, rgba(255,255,255,0.34), transparent 34% 60%, rgba(81,59,39,0.16)),
repeating-linear-gradient(0deg, transparent 0 16px, rgba(88,66,44,0.035) 17px),
var(--paper);
box-shadow: 0 14px 38px var(--shadow), inset 0 0 0 1px rgba(255,255,255,0.22);
transform: translate(var(--x), var(--y)) rotate(var(--r));
touch-action: none;
user-select: none;
transition: filter 300ms ease, box-shadow 300ms ease, background 300ms ease;
}
.room::before {
content: attr(data-mark);
position: absolute;
right: 0.85rem;
top: 0.7rem;
color: rgba(95, 70, 49, 0.2);
font: 900 2.2rem/1 "Courier New", monospace;
transform: rotate(7deg);
}
.room::after {
content: "";
position: absolute;
inset: 0.45rem;
pointer-events: none;
border: 1px solid rgba(95, 70, 49, 0.12);
border-radius: 3px;
}
.room.is-dragging {
z-index: 30;
filter: saturate(1.12) contrast(1.03);
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.46), inset 0 0 0 1px rgba(255,255,255,0.35);
}
.room.is-awake {
animation: breathe 3.8s ease-in-out infinite;
}
.room.is-buried {
filter: grayscale(0.4) brightness(0.78);
}
.room.is-buried .memory {
opacity: 0.06;
}
.room h2 {
position: relative;
z-index: 1;
margin: 0 0 0.35rem;
color: var(--green);
font-size: 1.45rem;
font-weight: 500;
line-height: 1;
}
.room .code {
margin: 0 0 0.95rem;
color: rgba(95, 70, 49, 0.72);
font: 700 0.72rem/1 "Courier New", monospace;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.diagram {
position: relative;
height: 90px;
margin: 0.75rem 0;
border: 1px solid rgba(95, 70, 49, 0.18);
background:
radial-gradient(circle at var(--mx, 50%) var(--my, 50%), rgba(163,111,44,0.38), transparent 10%),
repeating-linear-gradient(90deg, rgba(35,29,24,0.06) 0 1px, transparent 1px 12px),
rgba(244, 236, 217, 0.72);
overflow: hidden;
}
.diagram span {
position: absolute;
display: block;
border: 1px solid rgba(95, 70, 49, 0.42);
background: rgba(255, 255, 255, 0.18);
transition: transform 500ms ease, opacity 500ms ease;
}
.window span:nth-child(1) { left: 14%; top: 22%; width: 34%; height: 42%; }
.window span:nth-child(2) { right: 13%; top: 13%; width: 18%; height: 64%; }
.window span:nth-child(3) { left: 35%; bottom: 10%; width: 42%; height: 16%; }
.books span:nth-child(1) { left: 9%; bottom: 10%; width: 9%; height: 68%; }
.books span:nth-child(2) { left: 23%; bottom: 10%; width: 13%; height: 52%; }
.books span:nth-child(3) { left: 42%; bottom: 10%; width: 11%; height: 74%; }
.books span:nth-child(4) { left: 60%; bottom: 10%; width: 18%; height: 45%; }
.tape span:nth-child(1) { left: 14%; top: 26%; width: 26%; height: 46%; border-radius: 50%; }
.tape span:nth-child(2) { right: 14%; top: 26%; width: 26%; height: 46%; border-radius: 50%; }
.tape span:nth-child(3) { left: 28%; top: 46%; width: 44%; height: 11%; }
.stairs span:nth-child(1) { left: 14%; bottom: 13%; width: 18%; height: 18%; }
.stairs span:nth-child(2) { left: 31%; bottom: 30%; width: 18%; height: 18%; }
.stairs span:nth-child(3) { left: 48%; bottom: 47%; width: 18%; height: 18%; }
.stairs span:nth-child(4) { left: 65%; bottom: 64%; width: 18%; height: 18%; }
.table span:nth-child(1) { left: 17%; top: 26%; width: 66%; height: 12%; }
.table span:nth-child(2) { left: 26%; top: 38%; width: 9%; height: 38%; }
.table span:nth-child(3) { right: 26%; top: 38%; width: 9%; height: 38%; }
.memory {
position: relative;
z-index: 1;
min-height: 4.8rem;
margin: 0.75rem 0 0;
color: #35271d;
font-size: 0.96rem;
line-height: 1.42;
transition: opacity 400ms ease;
}
.room .stamp {
position: absolute;
right: 0.75rem;
bottom: 0.65rem;
width: 74px;
height: 74px;
border: 2px solid rgba(127, 51, 48, 0.45);
border-radius: 50%;
display: grid;
place-items: center;
color: rgba(127, 51, 48, 0.58);
font: 900 0.58rem/1.1 "Courier New", monospace;
text-align: center;
letter-spacing: 0.08em;
transform: rotate(-14deg) scale(0);
transition: transform 420ms cubic-bezier(.21,1.5,.45,1);
}
.room.is-indexed .stamp {
transform: rotate(-14deg) scale(1);
}
.slips {
position: absolute;
left: clamp(16px, 3vw, 40px);
bottom: clamp(16px, 3vw, 34px);
z-index: 18;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
max-width: min(720px, calc(100vw - 2rem));
}
.slip {
width: 120px;
min-height: 52px;
padding: 0.55rem 0.6rem;
border: 1px solid rgba(95, 70, 49, 0.44);
border-radius: 3px;
background: var(--paper-3);
color: rgba(33, 27, 22, 0.55);
box-shadow: 0 8px 18px rgba(0,0,0,0.18);
font: 700 0.62rem/1.28 "Courier New", monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
transform: rotate(var(--r));
transition: transform 300ms ease, color 300ms ease, background 300ms ease;
}
.slip.is-found {
color: var(--ink);
background: #fff4d6;
transform: rotate(var(--r)) translateY(-8px);
}
.drawer {
position: absolute;
right: clamp(18px, 4vw, 54px);
bottom: clamp(18px, 4vw, 46px);
z-index: 14;
width: min(360px, calc(100vw - 2rem));
min-height: 138px;
padding: 0.9rem;
border: 1px solid rgba(232, 221, 195, 0.26);
border-radius: 7px;
background:
linear-gradient(180deg, rgba(95,70,49,0.62), rgba(42,30,20,0.84)),
repeating-linear-gradient(90deg, rgba(255,255,255,0.04) 0 1px, transparent 1px 13px);
color: #eadfc2;
box-shadow: 0 16px 42px rgba(0,0,0,0.36);
}
.drawer h2 {
margin: 0 0 0.5rem;
font: 700 0.78rem/1.1 "Courier New", monospace;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.drawer p {
margin: 0;
color: rgba(234, 223, 194, 0.76);
font-size: 0.92rem;
line-height: 1.35;
}
.drawer button,
.sound {
border: 1px solid rgba(232, 221, 195, 0.36);
border-radius: 4px;
background: rgba(244, 236, 217, 0.09);
color: #eadfc2;
padding: 0.45rem 0.6rem;
cursor: pointer;
}
.drawer button:hover,
.sound:hover,
.drawer button:focus-visible,
.sound:focus-visible {
background: rgba(244, 236, 217, 0.2);
outline: 1px solid rgba(244, 236, 217, 0.5);
}
.drawer-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
.sound {
position: absolute;
left: clamp(18px, 4vw, 54px);
top: clamp(18px, 4vw, 44px);
z-index: 24;
transform: translateY(138px);
font: 700 0.7rem/1 "Courier New", monospace;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.cursor {
position: fixed;
left: 0;
top: 0;
z-index: 60;
width: 32px;
height: 32px;
pointer-events: none;
transform: translate(-50%, -50%);
transition: width 200ms ease, height 200ms ease, filter 300ms ease;
}
.cursor::before {
content: "";
position: absolute;
inset: 4px 9px 4px 12px;
border: 2px solid rgba(236, 206, 126, 0.95);
border-radius: 50% 50% 38% 38%;
box-shadow: 0 0 14px rgba(236, 206, 126, 0.45);
}
.cursor::after {
content: "";
position: absolute;
left: 17px;
top: 19px;
width: 18px;
height: 5px;
border-top: 2px solid rgba(236, 206, 126, 0.95);
border-right: 2px solid rgba(236, 206, 126, 0.95);
transform: rotate(25deg);
}
.cursor.is-waiting {
width: 46px;
height: 46px;
filter: drop-shadow(0 0 12px rgba(217, 208, 162, 0.65));
}
.word-catcher {
position: absolute;
left: -999px;
top: 0;
width: 1px;
height: 1px;
opacity: 0;
}
.typed {
position: fixed;
left: 50%;
bottom: 1rem;
z-index: 70;
max-width: calc(100vw - 2rem);
padding: 0.45rem 0.65rem;
border: 1px solid rgba(232, 221, 195, 0.28);
border-radius: 4px;
background: rgba(15, 17, 15, 0.7);
color: #d9f0cf;
font: 0.78rem/1 "Courier New", monospace;
letter-spacing: 0.1em;
text-transform: uppercase;
transform: translateX(-50%);
opacity: 0;
transition: opacity 260ms ease;
}
.typed.is-visible {
opacity: 1;
}
.ending {
position: fixed;
inset: 0;
z-index: 80;
display: grid;
place-items: center;
padding: 1rem;
pointer-events: none;
opacity: 0;
background:
radial-gradient(circle at 50% 42%, rgba(244,236,217,0.94), rgba(42,30,20,0.78) 58%, rgba(8,9,9,0.94));
transition: opacity 1400ms ease;
}
.ending.is-open {
pointer-events: auto;
opacity: 1;
}
.ending-card {
width: min(760px, 94vw);
padding: clamp(1.2rem, 5vw, 3rem);
border: 1px solid rgba(95, 70, 49, 0.34);
border-radius: 8px;
background:
linear-gradient(110deg, rgba(255,255,255,0.34), transparent 55%),
var(--paper-3);
box-shadow: 0 30px 90px rgba(0,0,0,0.5);
color: var(--ink);
transform: rotate(-0.8deg);
}
.ending-card h2 {
margin: 0 0 0.75rem;
color: var(--green);
font-size: clamp(2rem, 6vw, 4rem);
font-weight: 500;
line-height: 0.95;
}
.ending-card p {
max-width: 58ch;
margin: 0.85rem 0;
font-size: 1.05rem;
line-height: 1.52;
}
.ending-card button {
margin-top: 1rem;
border: 1px solid rgba(95, 70, 49, 0.42);
border-radius: 4px;
background: var(--green);
color: #f8efd9;
padding: 0.65rem 0.85rem;
cursor: pointer;
}
.ghost-note {
position: absolute;
z-index: 16;
max-width: 220px;
padding: 0.6rem 0.7rem;
border: 1px solid rgba(95,70,49,0.18);
background: rgba(244,236,217,0.88);
color: rgba(33,27,22,0.8);
font-size: 0.9rem;
line-height: 1.32;
box-shadow: 0 12px 30px rgba(0,0,0,0.22);
opacity: 0;
transform: translateY(8px) rotate(var(--r));
animation: note 5.5s ease forwards;
pointer-events: none;
}
.game.phase-dream .masthead {
transform: translateX(3vw) rotate(-1deg);
filter: blur(0.2px);
}
.game.phase-dream .desk {
background:
radial-gradient(circle at 48% 50%, rgba(38,56,74,0.3), transparent 34rem),
repeating-linear-gradient(90deg, rgba(244,236,217,0.04) 0 1px, transparent 1px 11px),
linear-gradient(145deg, rgba(49,67,72,0.28), rgba(20,17,14,0.34));
}
.game.phase-house .room {
background:
linear-gradient(110deg, rgba(255,255,255,0.34), transparent 34% 60%, rgba(81,59,39,0.16)),
repeating-linear-gradient(0deg, transparent 0 18px, rgba(88,66,44,0.04) 19px),
#ead8b7;
}
.game.phase-terminal .room {
filter: sepia(0.2) hue-rotate(40deg);
}
@media (max-width: 850px) {
.game {
min-height: 720px;
overflow: auto;
cursor: auto;
}
.cursor {
display: none;
}
.desk {
position: relative;
inset: auto;
min-height: 1080px;
margin: 0;
border-radius: 0;
}
.masthead,
.machine,
.drawer,
.sound,
.slips {
position: relative;
left: auto;
right: auto;
top: auto;
bottom: auto;
width: auto;
max-width: none;
margin: 1rem;
transform: none;
}
.machine {
z-index: 25;
}
.rooms {
position: relative;
display: grid;
gap: 1rem;
padding: 1rem;
}
.room {
position: relative;
width: auto;
min-height: 235px;
transform: rotate(var(--r));
}
.zones {
display: none;
}
.slips {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.slip {
width: auto;
}
}
@keyframes rain {
from { background-position: 0 0, 0 0, 0 0; }
to { background-position: 0 42px, 0 0, 0 0; }
}
@keyframes breathe {
0%, 100% { transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(1); }
50% { transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(1.014); }
}
@keyframes note {
12%, 72% { opacity: 1; transform: translateY(0) rotate(var(--r)); }
100% { opacity: 0; transform: translateY(-18px) rotate(var(--r)); }
}
@media (prefers-reduced-motion: reduce) {
.game,
.game *,
.game *::before,
.game *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.001ms !important;
}
}
</style>
<main class="game" id="game" aria-label="The Rain Index">
<input class="word-catcher" id="wordCatcher" aria-label="archive input" autocomplete="off" spellcheck="false">
<div class="desk" id="desk">
<div class="zones" aria-hidden="true">
<div class="zone" data-zone="attic"><span>attic weather</span></div>
<div class="zone" data-zone="terminal"><span>terminal dusk</span></div>
<div class="zone" data-zone="kitchen"><span>kitchen witness</span></div>
<div class="zone" data-zone="stair"><span>stair memory</span></div>
</div>
<header class="masthead">
<small>Department of Unfinished Domestic Topographies</small>
<h1>The Rain Index</h1>
<p class="no-explain">The exhibit accepts handling. The catalogue dislikes certainty.</p>
</header>
<button class="sound" id="soundBtn" type="button" aria-pressed="false">sound</button>
<aside class="machine" aria-live="polite">
<div class="terminal" id="terminal">
<p>RAIN-INDEX / teaching copy / wet boot sector</p>
<div class="line" id="lineA">specimens asleep behind paper.</div>
<div class="line" id="lineB">move a room. wait over it. type what returns.</div>
<div class="line" id="lineC"></div>
</div>
<div class="meter">
<span>catalogue humidity</span>
<span id="humidityText">0%</span>
<div class="bar" aria-hidden="true"><i id="humidityBar"></i></div>
<span id="visitText">first attendance</span>
</div>
</aside>
<section class="rooms" id="rooms" aria-label="movable archive rooms">
<article class="room" data-id="window" data-mark="I" data-word="rain" style="--x:15vw;--y:31vh;--r:-2deg">
<h2>South Window</h2>
<p class="code">specimen 04 / condensation script</p>
<div class="diagram window" aria-hidden="true"><span></span><span></span><span></span></div>
<p class="memory">The glass keeps a list of people who looked out instead of answering.</p>
<span class="stamp">not yet<br>indexed</span>
</article>
<article class="room" data-id="library" data-mark="II" data-word="borge" style="--x:39vw;--y:39vh;--r:1.5deg">
<h2>Borrowed Library</h2>
<p class="code">specimen 11 / recursive shelf</p>
<div class="diagram books" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<p class="memory">Every book is filed under the title it had in someone else's dream.</p>
<span class="stamp">not yet<br>indexed</span>
</article>
<article class="room" data-id="tape" data-mark="III" data-word="mother" style="--x:62vw;--y:28vh;--r:-1deg">
<h2>Cassette Kitchen</h2>
<p class="code">specimen 19 / domestic magnetic</p>
<div class="diagram tape" aria-hidden="true"><span></span><span></span><span></span></div>
<p class="memory">A spoon taps the cup twice. The tape records the third tap before it happens.</p>
<span class="stamp">not yet<br>indexed</span>
</article>
<article class="room" data-id="stairs" data-mark="IV" data-word="below" style="--x:22vw;--y:61vh;--r:1deg">
<h2>Reverse Stairwell</h2>
<p class="code">specimen 27 / impossible municipal</p>
<div class="diagram stairs" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
<p class="memory">Down is a rumor told by upstairs to make itself feel necessary.</p>
<span class="stamp">not yet<br>indexed</span>
</article>
<article class="room" data-id="desklet" data-mark="V" data-word="zaine" style="--x:51vw;--y:63vh;--r:-1.8deg">
<h2>Young Desk</h2>
<p class="code">specimen 32 / pencil pressure</p>
<div class="diagram table" aria-hidden="true"><span></span><span></span><span></span></div>
<p class="memory">The drawer contains a map of the room drawn from under the table.</p>
<span class="stamp">not yet<br>indexed</span>
</article>
</section>
<section class="slips" id="slips" aria-label="index slips">
<div class="slip" data-slip="window" style="--r:-1deg">I<br>window missing</div>
<div class="slip" data-slip="library" style="--r:1.4deg">II<br>shelf missing</div>
<div class="slip" data-slip="tape" style="--r:-2deg">III<br>voice missing</div>
<div class="slip" data-slip="stairs" style="--r:1deg">IV<br>stair missing</div>
<div class="slip" data-slip="desklet" style="--r:-1.3deg">V<br>name missing</div>
</section>
<aside class="drawer" id="drawer">
<h2>card drawer pretending to be closed</h2>
<p id="drawerText">This drawer opens when you close it. It closes when you trust it.</p>
<div class="drawer-actions">
<button id="closeDrawer" type="button">close</button>
<button id="misfile" type="button">misfile</button>
</div>
</aside>
</div>
<div class="cursor" id="cursor" aria-hidden="true"></div>
<div class="typed" id="typed" aria-hidden="true"></div>
<section class="ending" id="ending" aria-modal="true" aria-labelledby="endingTitle">
<div class="ending-card">
<h2 id="endingTitle">The House Files You Back</h2>
<p id="endingCopy">The rain has found its alphabetical order. A small room opens in the index where your visit has been carefully misremembered.</p>
<p>The page will remember that you came here. Next time, one sentence will be waiting before you touch anything.</p>
<button id="again" type="button">leave the drawer slightly open</button>
</div>
</section>
</main>
<script>
(() => {
document.body.classList.add("rain-index-page");
document.getElementById("content")?.classList.add("rain-index-content");
const game = document.getElementById("game");
const desk = document.getElementById("desk");
const roomsWrap = document.getElementById("rooms");
const rooms = Array.from(document.querySelectorAll(".room"));
const zones = Array.from(document.querySelectorAll(".zone"));
const cursor = document.getElementById("cursor");
const typed = document.getElementById("typed");
const input = document.getElementById("wordCatcher");
const lineA = document.getElementById("lineA");
const lineB = document.getElementById("lineB");
const lineC = document.getElementById("lineC");
const humidityBar = document.getElementById("humidityBar");
const humidityText = document.getElementById("humidityText");
const visitText = document.getElementById("visitText");
const drawerText = document.getElementById("drawerText");
const closeDrawer = document.getElementById("closeDrawer");
const misfile = document.getElementById("misfile");
const soundBtn = document.getElementById("soundBtn");
const ending = document.getElementById("ending");
const endingCopy = document.getElementById("endingCopy");
const again = document.getElementById("again");
const storageKey = "rainIndexState";
const phrases = {
attic: [
"attic weather has entered the catalogue.",
"a drip begins in a room with no ceiling.",
"the window alphabetizes the rain by surname."
],
terminal: [
"terminal dusk accepts the specimen.",
"green letters gather at the edge of the paper.",
"the old lab computer asks for a childhood answer."
],
kitchen: [
"kitchen witness confirms a domestic anomaly.",
"someone rinses a cup in the next decade.",
"the cassette smells faintly of warm dust."
],
stair: [
"stair memory folds the room through itself.",
"an impossible landing briefly becomes plausible.",
"the archive miscounts the floors and apologizes."
]
};
const wordSecrets = {
rain: "The window replies in water: not weather, but handwriting.",
borge: "A shelf turns inward. For one second, every title is the title of this page.",
mother: "The tape softens. A voice nearly says your name, then chooses the kettle.",
below: "The stairwell lowers the ceiling and raises the past.",
zaine: "The desk recognizes its owner without making a scene.",
library: "A librarian with no face stamps the inside of your cursor.",
sleep: "All rooms become lighter when they pretend not to hear you.",
home: "The drawer refuses the word, then keeps it."
};
const slipNames = {
window: "I\nrain indexed",
library: "II\nshelf indexed",
tape: "III\nvoice indexed",
stairs: "IV\nstair indexed",
desklet: "V\nname indexed"
};
const visits = Number(localStorage.getItem("rainIndexVisits") || "0") + 1;
let state = loadState();
let pointer = { x: innerWidth / 2, y: innerHeight / 2 };
let heldRoom = null;
let drag = null;
let zCounter = 30;
let typedBuffer = "";
let typeTimer = 0;
let stillTimer = 0;
let lastMove = Date.now();
let audio = null;
let drone = null;
let tick = null;
localStorage.setItem("rainIndexVisits", String(visits));
visitText.textContent = visits === 1 ? "first attendance" : `attendance ${visits}`;
if (visits > 1) {
lineC.textContent = "previous visit found under blotter.";
spawnNote("You have been here in a different order.", 48, 18);
}
applyState();
updateHumidity();
input.focus({ preventScroll: true });
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerdown", () => input.focus({ preventScroll: true }));
document.addEventListener("keydown", onKey);
window.addEventListener("resize", resizeReality);
closeDrawer.addEventListener("click", drawerClose);
misfile.addEventListener("click", misfileRooms);
soundBtn.addEventListener("click", toggleSound);
again.addEventListener("click", restart);
rooms.forEach((room) => {
room.addEventListener("pointerdown", startDrag);
room.addEventListener("pointerenter", () => {
heldRoom = room;
room.classList.add("is-awake");
lineB.textContent = `specimen ${room.dataset.mark} is warm at the edge.`;
});
room.addEventListener("pointerleave", () => {
if (!drag || drag.room !== room) room.classList.remove("is-awake");
if (heldRoom === room) heldRoom = null;
});
room.addEventListener("dblclick", () => {
reveal(room, "double handling produced unauthorized certainty.");
});
});
requestAnimationFrame(loop);
function loadState() {
try {
return JSON.parse(localStorage.getItem(storageKey)) || { found: [], ending: 0 };
} catch {
return { found: [], ending: 0 };
}
}
function saveState() {
localStorage.setItem(storageKey, JSON.stringify(state));
}
function applyState() {
rooms.forEach((room) => {
if (state.found.includes(room.dataset.id)) {
room.classList.add("is-indexed");
room.querySelector(".stamp").innerHTML = "indexed<br>in rain";
}
});
document.querySelectorAll(".slip").forEach((slip) => {
if (state.found.includes(slip.dataset.slip)) {
slip.classList.add("is-found");
slip.textContent = slipNames[slip.dataset.slip];
}
});
}
function onPointerMove(event) {
pointer = { x: event.clientX, y: event.clientY };
cursor.style.left = `${pointer.x}px`;
cursor.style.top = `${pointer.y}px`;
lastMove = Date.now();
game.classList.add("has-touched");
if (drag) moveDrag(event);
const rect = desk.getBoundingClientRect();
const mx = `${((pointer.x - rect.left) / rect.width) * 100}%`;
const my = `${((pointer.y - rect.top) / rect.height) * 100}%`;
rooms.forEach((room) => {
const r = room.getBoundingClientRect();
if (pointer.x >= r.left && pointer.x <= r.right && pointer.y >= r.top && pointer.y <= r.bottom) {
room.querySelector(".diagram").style.setProperty("--mx", mx);
room.querySelector(".diagram").style.setProperty("--my", my);
}
});
}
function startDrag(event) {
const room = event.currentTarget;
const rect = room.getBoundingClientRect();
drag = {
room,
startX: event.clientX,
startY: event.clientY,
baseX: rect.left - desk.getBoundingClientRect().left,
baseY: rect.top - desk.getBoundingClientRect().top
};
room.classList.add("is-dragging");
room.style.zIndex = String(++zCounter);
room.setPointerCapture(event.pointerId);
room.addEventListener("pointerup", endDrag, { once: true });
room.addEventListener("pointercancel", endDrag, { once: true });
lineA.textContent = "manual rearrangement detected.";
tickSound(220, 0.03);
}
function moveDrag(event) {
const x = drag.baseX + event.clientX - drag.startX;
const y = drag.baseY + event.clientY - drag.startY;
drag.room.style.setProperty("--x", `${x}px`);
drag.room.style.setProperty("--y", `${y}px`);
lightZone(event.clientX, event.clientY);
}
function endDrag(event) {
const room = event.currentTarget;
room.classList.remove("is-dragging");
const zone = getZone(pointer.x, pointer.y);
if (zone) {
const text = pick(phrases[zone.dataset.zone]);
lineA.textContent = text;
mutateRoom(room, zone.dataset.zone);
if (zone.dataset.zone === "terminal" && room.dataset.id === "library") reveal(room, "the library prefers phosphor.");
if (zone.dataset.zone === "kitchen" && room.dataset.id === "tape") reveal(room, "the kitchen heard itself on tape.");
if (zone.dataset.zone === "stair" && room.dataset.id === "stairs") reveal(room, "the stairwell agreed to be counted.");
if (zone.dataset.zone === "attic" && room.dataset.id === "window") reveal(room, "rain entered from below.");
if (zone.dataset.zone === "kitchen" && room.dataset.id === "desklet") reveal(room, "the desk remembered a table.");
}
zones.forEach((z) => z.classList.remove("is-lit"));
drag = null;
tickSound(165, 0.04);
}
function getZone(x, y) {
return zones.find((zone) => {
const r = zone.getBoundingClientRect();
return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
});
}
function lightZone(x, y) {
const active = getZone(x, y);
zones.forEach((zone) => zone.classList.toggle("is-lit", zone === active));
}
function mutateRoom(room, zone) {
const diagram = room.querySelector(".diagram");
const parts = Array.from(diagram.children);
parts.forEach((part, index) => {
const sign = zone === "stair" || zone === "terminal" ? -1 : 1;
part.style.transform = `translate(${sign * (index + 1) * 3}px, ${(index % 2 ? -1 : 1) * 4}px)`;
part.style.opacity = zone === "attic" ? "0.62" : "1";
});
game.classList.remove("phase-dream", "phase-house", "phase-terminal");
if (zone === "stair") game.classList.add("phase-dream");
if (zone === "kitchen") game.classList.add("phase-house");
if (zone === "terminal") game.classList.add("phase-terminal");
}
function onKey(event) {
if (event.metaKey || event.ctrlKey || event.altKey) return;
if (event.key === "Escape") {
typedBuffer = "";
showTyped("");
return;
}
if (event.key.length !== 1) return;
typedBuffer = (typedBuffer + event.key.toLowerCase()).replace(/[^a-z]/g, "").slice(-16);
showTyped(typedBuffer);
clearTimeout(typeTimer);
typeTimer = setTimeout(() => showTyped(""), 1600);
Object.keys(wordSecrets).forEach((word) => {
if (typedBuffer.endsWith(word)) {
lineC.textContent = wordSecrets[word];
const room = rooms.find((item) => item.dataset.word === word);
if (room) reveal(room, wordSecrets[word]);
typedBuffer = "";
showTyped("");
tickSound(440, 0.06);
}
});
}
function showTyped(text) {
typed.textContent = text ? `>${text}` : "";
typed.classList.toggle("is-visible", Boolean(text));
}
function reveal(room, reason) {
const id = room.dataset.id;
if (!state.found.includes(id)) {
state.found.push(id);
saveState();
room.classList.add("is-indexed");
room.querySelector(".stamp").innerHTML = "indexed<br>in rain";
const slip = document.querySelector(`[data-slip="${id}"]`);
if (slip) {
slip.classList.add("is-found");
slip.textContent = slipNames[id];
}
lineA.textContent = reason;
lineB.textContent = `${state.found.length}/5 catalogue slips recovered.`;
spawnNote(pick([
"A card slips out from behind the room.",
"Somebody wrote this down before you arrived.",
"The paper is briefly warmer than the screen."
]), pointer.x, pointer.y);
updateHumidity();
if (state.found.length >= 5) finish();
} else {
lineB.textContent = "already indexed, but not exhausted.";
}
}
function updateHumidity() {
const humidity = Math.min(100, Math.round((state.found.length / 5) * 100));
humidityText.textContent = `${humidity}%`;
humidityBar.style.setProperty("--w", `${humidity}%`);
}
function drawerClose() {
drawerText.textContent = pick([
"The closed drawer is open on the inside.",
"A brass label changes from SUBJECT to VISITOR.",
"You hear index cards settling into impossible order."
]);
rooms.forEach((room, index) => {
if ((index + state.found.length) % 2 === 0) room.classList.toggle("is-buried");
});
lineC.textContent = "conventional interface expectation broken politely.";
if (state.found.length >= 3) {
const unrevealed = rooms.find((room) => !state.found.includes(room.dataset.id));
if (unrevealed) reveal(unrevealed, "the drawer closed around a missing card.");
}
tickSound(110, 0.05);
}
function misfileRooms() {
const deskRect = desk.getBoundingClientRect();
rooms.forEach((room) => {
const x = 70 + Math.random() * Math.max(160, deskRect.width - 420);
const y = 170 + Math.random() * Math.max(160, deskRect.height - 420);
room.style.setProperty("--x", `${x}px`);
room.style.setProperty("--y", `${y}px`);
room.style.setProperty("--r", `${-3 + Math.random() * 6}deg`);
room.classList.remove("is-buried");
});
lineA.textContent = "classification failure made several new truths possible.";
spawnNote("The archive prefers a useful mistake.", pointer.x, pointer.y);
}
function resizeReality() {
if (innerWidth < 851) return;
lineB.textContent = innerWidth > innerHeight ? "wide room: the archive remembers maps." : "tall room: the archive remembers wells.";
if (Math.abs(innerWidth - innerHeight) < 90) {
const room = rooms.find((item) => item.dataset.id === "desklet");
if (room) reveal(room, "the square window admitted a private name.");
}
}
function loop() {
const waiting = Date.now() - lastMove > 1700;
cursor.classList.toggle("is-waiting", waiting);
if (waiting && heldRoom && !drag) {
stillTimer += 1;
if (stillTimer > 80) {
stillTimer = 0;
heldRoom.querySelector(".memory").textContent = developMemory(heldRoom);
if (Math.random() > 0.46) reveal(heldRoom, "a patient hover developed the image.");
}
} else {
stillTimer = 0;
}
requestAnimationFrame(loop);
}
function developMemory(room) {
const pool = {
window: [
"On the other side of the glass, someone is trying to remember this room into being.",
"The rain falls upward whenever nobody uses the word weather."
],
library: [
"A catalogue card describes a catalogue card describing your hand.",
"One book contains a floorplan. The floorplan contains a smaller library."
],
tape: [
"The cassette reverses and plays the sound of a chair being forgiven.",
"A recipe is read aloud, but all ingredients are dates."
],
stairs: [
"The fourth step is only visible from the fifth.",
"A landing opens into a lecture hall where the chalk is still raining."
],
desklet: [
"A pencil line moves by itself, then pretends to be a scratch.",
"The drawer has kept every failed password as a pressed flower."
]
};
return pick(pool[room.dataset.id]);
}
function spawnNote(text, x, y) {
const note = document.createElement("div");
note.className = "ghost-note";
note.textContent = text;
note.style.left = `${Math.min(Math.max(x - 40, 16), innerWidth - 250)}px`;
note.style.top = `${Math.min(Math.max(y - 20, 70), innerHeight - 160)}px`;
note.style.setProperty("--r", `${-3 + Math.random() * 6}deg`);
desk.appendChild(note);
setTimeout(() => note.remove(), 5600);
}
function finish() {
if (ending.classList.contains("is-open")) return;
state.ending = (state.ending || 0) + 1;
saveState();
endingCopy.textContent = state.ending > 1
? "The index recognizes the shape of your return. This time, it files the rain under Almost Home."
: "The rain has found its alphabetical order. A small room opens in the index where your visit has been carefully misremembered.";
setTimeout(() => ending.classList.add("is-open"), 900);
if (audio) {
tickSound(523, 0.08);
tickSound(659, 0.08, 0.14);
}
}
function restart() {
ending.classList.remove("is-open");
state.found = [];
saveState();
rooms.forEach((room) => {
room.classList.remove("is-indexed", "is-buried");
room.querySelector(".stamp").innerHTML = "not yet<br>indexed";
});
document.querySelectorAll(".slip").forEach((slip) => {
slip.classList.remove("is-found");
slip.textContent = `${slip.dataset.slip === "window" ? "I" : slip.dataset.slip === "library" ? "II" : slip.dataset.slip === "tape" ? "III" : slip.dataset.slip === "stairs" ? "IV" : "V"}\nmissing`;
});
updateHumidity();
misfileRooms();
}
function toggleSound() {
if (!audio) {
audio = new (window.AudioContext || window.webkitAudioContext)();
const gain = audio.createGain();
gain.gain.value = 0.028;
drone = audio.createOscillator();
drone.type = "sine";
drone.frequency.value = 56;
drone.connect(gain).connect(audio.destination);
drone.start();
tick = audio.createGain();
tick.gain.value = 0.08;
tick.connect(audio.destination);
soundBtn.setAttribute("aria-pressed", "true");
soundBtn.textContent = "sound on";
lineC.textContent = "rain carrier enabled.";
} else {
audio.close();
audio = null;
drone = null;
tick = null;
soundBtn.setAttribute("aria-pressed", "false");
soundBtn.textContent = "sound";
}
}
function tickSound(freq, volume, delay = 0) {
if (!audio) return;
const osc = audio.createOscillator();
const gain = audio.createGain();
osc.type = "triangle";
osc.frequency.value = freq;
gain.gain.value = volume;
osc.connect(gain).connect(audio.destination);
const now = audio.currentTime + delay;
gain.gain.setValueAtTime(volume, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
osc.start(now);
osc.stop(now + 0.2);
}
function pick(items) {
return items[Math.floor(Math.random() * items.length)];
}
})();
</script>
#+END_EXPORT