/* Self-hosted, so there is no third-party origin in the critical path and the
   CSP needs no font/style allowlist at all. Both files are subsets:
   - JetBrains Mono is the VARIABLE cut (400-700 in one file, 31 KB - half what
     the two static weights cost). unicode-range is kept so anything outside
     latin falls back to Menlo rather than rendering as .notdef.
   - Playfair carries ASCII letters, digits and punctuation only (19 KB instead
     of 34). It sets one epigraph; if that quote ever needs a character outside
     that set, re-cut the file - see the note by the quote in template.rs. */
@font-face {
    font-family: 'JetBrains Mono';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url(/fonts/jetbrains-mono-var-latin-v1.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Playfair';
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url(/fonts/playfair-italic-ascii-v1.woff2) format('woff2');
}

* { margin: 0; padding: 0; box-sizing: border-box; }
/* JetBrains Mono's fallbacks are chosen for METRIC closeness, measured at 16px:
   Menlo/Consolas/DejaVu land within 0.4% of it, so the swap does not reflow.
   ui-monospace (SF Mono) is ~22% narrower and would jump, hence its absence. */
html, body { height: 100%; background: #fff; font-family: 'JetBrains Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace; font-size: 16px; color: #4a4a49; }
a { color: #000; text-decoration: underline dashed 2px; text-underline-offset: 4px; }

/* site */
.page { text-align: center; margin-bottom: 20px; }
.header { display: flex; flex-direction: column; align-items: center; margin-bottom: 20px; }
#sigil { display: block; overflow: hidden; height: 260px; cursor: pointer; }
#sigil video { margin-bottom: -10px; pointer-events: none; }
#quote { font: 16px/1.6 'Playfair', Georgia, serif; color: #333; }
hr { width: 100%; max-width: 500px; margin: 10px auto 40px auto; }

/* terminal */
#terminal { overflow-y: auto; max-height: 400px; max-width: 500px; margin: 0 auto; text-align: left; }
.welcome { margin-bottom: 1em; }
.output { margin-top: 0.25em; line-height: 1.6; }
.echo { margin-top: 0.75em; white-space: pre-wrap; overflow-wrap: anywhere; }
.journal { font-family: inherit; white-space: pre-wrap; overflow-wrap: anywhere; }
.cipher {
    font-family: inherit;
    word-break: break-all;
    overflow-wrap: anywhere;
    line-height: 1.5;
}
/* One step per character: the width is counted in `ch`, which in a monospace
   face is exactly one glyph. Animating max-width to 100% instead made each
   step a fraction of the CONTAINER, so text came out in chunks of several
   characters and the line read as a wipe. 12ms matches CHAR_MS in template.rs. */
.typewriter span { display: block; overflow: hidden; white-space: nowrap; width: 0; max-width: 100%; animation: type calc(var(--c) * 12ms) steps(var(--c)) var(--d) forwards; }
@keyframes type { to { width: calc(var(--c) * 1ch); } }
.prompt { display: flex; align-items: center; margin-top: 0.5em; position: relative; }
.prompt label { white-space: nowrap; flex-shrink: 0; }
.prompt input { flex: 1; border: 0; outline: 0; font: inherit; color: inherit; background: transparent; }
/* No-JS keeps the native caret as the focus indicator. term.js hides it and
   draws the block caret below instead; the prompt label brightening on focus is
   the indicator that survives either way. */
.prompt:focus-within label { color: #000; }
body.js .prompt input { caret-color: transparent; }
.caret {
    position: absolute; top: 0; width: 9px; height: 1.25em;
    background: #4a4a49; pointer-events: none; display: none;
}
body.js .prompt:focus-within .caret { display: block; animation: blink 1.06s steps(2, jump-none) infinite; }
@keyframes blink { from { opacity: 1; } to { opacity: 0; } }

/* room modal -- the sigil is a label for this checkbox, so opening and closing
   is pure CSS; room.js only runs the game */
#room-toggle { position: absolute; opacity: 0; width: 1px; height: 1px; }
#roomframe { position: fixed; inset: 0; z-index: 1000; background: rgba(0,0,0,.55); display: none; align-items: center; justify-content: center; }
#room-toggle:checked ~ #roomframe { display: flex; }
#roomframe .backdrop { position: absolute; inset: 0; cursor: pointer; }
/* padding-top clears the [x]: the close button used to overlap the room's black
   frame, so the frame now starts below the button's line box. */
.roommodal { position: relative; z-index: 1; background: #fff; padding: 26px 20px 4px; border: 1px solid #888; box-shadow: 0 12px 40px rgba(0,0,0,.35); }
.room-x { position: absolute; top: 5px; right: 14px; cursor: pointer; color: #b00000; font-weight: bold; font-size: 13px; line-height: 1; z-index: 2; }

/* room -- pears' office, top down. Furniture without recovered pixel art
   (bookshelf, volcano) is drawn in CSS.
   The frame has no bottom border: the doorway is a notch in .framebottom, and
   walking into it leaves the room (see room.js). */
.room {
    position: relative;
    width: 440px; height: 360px;
    margin: 0 auto;
    border: 6px solid #2b2b2b;
    border-bottom: 0;
    overflow: hidden;
    background: #caa77d;
}
.room img { position: absolute; image-rendering: pixelated; }
.wall { position: absolute; top: 0; left: 0; right: 0; height: 128px; background: #8fa4b8; border-bottom: 6px solid #6b7d8f; }
.floor { position: absolute; top: 134px; left: 0; right: 0; bottom: 0; background: repeating-linear-gradient(90deg, #caa77d 0 40px, #c09f74 40px 42px); }
/* the bottom frame, with a door-sized notch punched out of it. The floor shows
   through the notch, which is what makes it read as an opening rather than a
   painted rectangle. */
.framebottom {
    position: absolute; left: 0; right: 0; bottom: 0; height: 6px; z-index: 3;
    background: linear-gradient(90deg,
        #2b2b2b 0 calc(50% - 28px), transparent calc(50% - 28px) calc(50% + 28px), #2b2b2b calc(50% + 28px) 100%);
}
/* the doorway itself: a lighter threshold between two jambs, sitting in the notch */
.door {
    position: absolute; left: calc(50% - 28px); bottom: 0;
    width: 56px; height: 13px; z-index: 2;
    background: linear-gradient(#b3906a, #8a6f4f);
    border-left: 3px solid #2b2b2b; border-right: 3px solid #2b2b2b;
}
/* the window on the back wall, looking at the volcano: the ember glow is baked
   into the sky, the muntin cross and the sill are what make it read as a
   window rather than a painting. Same size as before, shifted left so the
   bookshelf gets room to breathe. */
.window {
    position: absolute; left: 92px; top: 14px; width: 150px; height: 92px;
    border: 5px solid #5d4a3a;
    background: linear-gradient(#8fc7e8 0 62%, #7fb069 62% 78%, #5f8f4e 78%);
    box-shadow: inset 0 2px 5px rgba(0, 0, 0, .3);
    overflow: hidden;
}
.window::before {  /* muntin cross, above the view */
    content: ""; position: absolute; inset: 0; z-index: 4; pointer-events: none;
    background:
        linear-gradient(#5d4a3a, #5d4a3a) center / 5px 100% no-repeat,
        linear-gradient(#5d4a3a, #5d4a3a) center / 100% 5px no-repeat;
}
/* the volcano: off-center left, an irregular concave stratovolcano silhouette
   with an asymmetric crater. The lava SVG lives INSIDE the cone on purpose:
   the clip-path clips descendants, so the streams hug the flank for free. */
.volcano {
    position: absolute; z-index: 1; bottom: 10px; left: -14px; width: 122px; height: 66px;
    /* two whisper-quiet gradients for scree lines and ridge shadow, over the
       base rock gradient */
    background:
        repeating-linear-gradient(102deg, rgba(0, 0, 0, .07) 0 2px, transparent 2px 9px),
        repeating-linear-gradient(-97deg, rgba(255, 255, 255, .05) 0 1px, transparent 1px 13px),
        linear-gradient(#8a5f4d 0 30%, #5d4438 70%, #47362c);
    clip-path: polygon(41% 0, 44.5% 8%, 54% 9.5%, 59% 2%,
                       66% 25%, 76% 50%, 82% 63%, 86% 72%, 89% 80%, 95% 91%, 100% 100%,
                       0 100%, 5% 91%, 11% 80%, 16% 68%, 24% 50%, 34% 25%);
}
.volcano::before {  /* the lava pool glowing in the crater */
    content: ""; position: absolute; top: 4px; left: 43.5%; width: 12%; height: 7px;
    background: radial-gradient(#ffb1a0, #ff4b3b 60%, #c1180d);
    border-radius: 0 0 4px 4px;
    box-shadow: 0 0 7px 2px rgba(255, 70, 45, .9);
    transition: background 6s linear, box-shadow 6s linear;
}
/* once everything has cooled the pool dims but stays alive */
.window.cooled .volcano::before {
    background: radial-gradient(#e0897a, #b8402f 60%, #7a1208);
    box-shadow: 0 0 4px 1px rgba(170, 60, 40, .55);
}
/* a low treeline between the greenery and the cone, so the volcano sits IN the
   landscape rather than on it */
.hills {
    position: absolute; z-index: 2; left: -4px; right: -4px; bottom: 0; height: 15px;
    background:
        radial-gradient(circle 7px at 7px 8px, #35592f 98%, transparent) 0 0 / 14px 15px repeat-x,
        linear-gradient(transparent 0 8px, #35592f 8px);
}
/* the eruption: three streams ooze down curved paths ONCE, each led by a bright
   widening toe that spreads into a pool at the foot of the cone, then everything
   cools, darkens and keeps its final shape forever (fill-mode: forwards).
   Animations do not run under display:none, so in the modal the eruption starts
   the first time the room is opened. room.js swaps in one of three path sets per
   load (.v2 / .v3) so repeat visits differ. */
.lavaflow {
    position: absolute; inset: 0;
    filter: drop-shadow(0 0 3px rgba(255, 80, 45, .85));
    transition: filter 5s linear;
}
.window.cooled .lavaflow { filter: none; }
.lavaflow .f1 { --p: path("M59 8C62 17 56 23 60 32 65 43 62 51 66 58 67 60 66 61 67 62"); --dur: 26s; --delay: 0s; --tw: 23; }
.lavaflow .f2 { --p: path("M56 8C51 15 54 22 48 29 43 37 42 45 38 52 36 57 35 60 34 62"); --dur: 34s; --delay: 6s; --tw: 30; }
.lavaflow .f3 { --p: path("M63 8C68 14 65 21 71 28 75 34 74 39 75 45"); --dur: 30s; --delay: 11s; --tw: 27; }
.volcano.v2 .f1 { --p: path("M57 8C54 16 58 24 55 32 51 41 53 49 49 56 47 60 47 61 46 62"); }
.volcano.v2 .f2 { --p: path("M62 8C67 16 63 24 69 31 73 38 71 45 74 52 76 57 76 60 77 62"); }
.volcano.v2 .f3 { --p: path("M55 9C50 15 52 22 46 28 42 33 41 38 40 43"); }
.volcano.v3 .f1 { --p: path("M60 8C64 15 60 22 64 30 69 38 65 46 69 54 71 59 70 61 71 62"); }
.volcano.v3 .f2 { --p: path("M55 8C52 16 55 24 50 32 46 39 46 47 42 54 40 59 40 61 39 62"); }
.volcano.v3 .f3 { --p: path("M61 9C66 16 62 23 67 30 70 35 69 40 70 46"); }
.lavaflow .trail {
    fill: none; stroke: #e33517; stroke-width: 3.4; stroke-linecap: round;
    d: var(--p);
    stroke-dasharray: 100; stroke-dashoffset: 100;
    animation: draw var(--dur) cubic-bezier(.55, .05, .6, 1) var(--delay) forwards,
               cool var(--dur) linear var(--delay) forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes cool {
    0%, 84% { stroke: #e33517; }
    100% { stroke: #45261d; }
}
/* the advancing front: brightest, widening as it slows, then spreading into a
   basal pool once it reaches the foot */
.lavaflow .toe {
    fill: #ffe3d2; offset-path: var(--p); offset-rotate: 0deg; offset-distance: 0%;
    transform-box: fill-box; transform-origin: 50% 50%; opacity: 0;
    animation: advance var(--dur) cubic-bezier(.55, .05, .6, 1) var(--delay) forwards,
               pool 6s ease-out calc(var(--delay) + var(--dur)) forwards,
               cooltoe var(--dur) linear var(--delay) forwards;
}
@keyframes advance {
    0%   { opacity: 0; offset-distance: 0%; transform: scale(.5); }
    4%   { opacity: 1; }
    85%  { transform: scale(1); }
    100% { opacity: 1; offset-distance: 100%; transform: scale(1.2); }
}
@keyframes pool { to { transform: scale(2.2, 1.2); } }
@keyframes cooltoe {
    0%, 84% { fill: #ffe3d2; }
    100% { fill: #35211a; }
}
/* twinkle points riding the hot stream; they stop with the cooling filter */
.lavaflow .twinkle {
    fill: #fff3e8; offset-path: var(--p); offset-rotate: 0deg; offset-distance: 0%; opacity: 0;
    animation: advance var(--dur) cubic-bezier(.55, .05, .6, 1) calc(var(--delay) + 1.4s) forwards,
               twinkle 1.1s linear calc(var(--delay) + 1.4s) var(--tw) forwards;
}
@keyframes twinkle { 0% { opacity: .05; } 50% { opacity: .95; } 100% { opacity: 0; } }
/* a few red sparks pop off the crater while the eruption is hot, then stop */
.sparks, .sparks::before, .sparks::after {
    position: absolute; width: 4px; height: 4px; border-radius: 50%; opacity: 0;
    background: #ff5a4a; box-shadow: 0 0 4px 1px rgba(255, 80, 55, .8);
    animation: spark 2.6s linear 7;
}
.sparks { left: 47px; top: 6px; z-index: 1; --sx: 3px; }
.sparks::before { content: ""; left: -3px; top: 2px; background: #ff8a75; animation-duration: 3.4s; animation-delay: .9s; animation-iteration-count: 6; --sx: -9px; }
.sparks::after { content: ""; left: 3px; top: 1px; background: #ffb3a0; animation-duration: 2.1s; animation-delay: 1.5s; animation-iteration-count: 8; --sx: 10px; }
@keyframes spark {
    0% { transform: translate(0, 0) scale(.5); opacity: 0; }
    12% { opacity: 1; }
    100% { transform: translate(var(--sx), -14px) scale(1); opacity: 0; }
}
/* the lava dried, but the mountain is still alive: a slow steam wisp off the
   crater, which only starts once everything has cooled */
.steam, .steam::before {
    position: absolute; z-index: 3; width: 5px; height: 5px; border-radius: 50%;
    background: rgba(226, 230, 234, .8); opacity: 0;
}
.steam { left: 46px; top: 4px; }
.steam::before { content: ""; left: 4px; top: -2px; }
.window.cooled .steam { animation: wisp 7s ease-out infinite; }
.window.cooled .steam::before { animation: wisp 7s ease-out 3.4s infinite; }
@keyframes wisp {
    0% { opacity: 0; transform: translate(0, 0) scale(.5); }
    20% { opacity: .45; }
    100% { opacity: 0; transform: translate(7px, -22px) scale(2.3); }
}
.cloud {
    position: absolute; z-index: 1; left: 0; top: 9px; width: 26px; height: 9px;
    border-radius: 8px; background: #fff; opacity: .9;
    box-shadow: 9px -4px 0 -1px #fff, 17px -2px 0 -2px #fff;
    animation: drift 46s linear infinite;
}
@keyframes drift { from { transform: translateX(-46px); } to { transform: translateX(150px); } }
.desk { left: 121px; top: 92px; width: 92px; }
/* bookshelf, seen front-on against the back wall. The books are real elements
   laid out by the template, not a repeating gradient: a gradient gives every
   spine the same width and the same height, which reads as a colour barcode
   rather than a shelf. Varied widths, varied heights, a book leaning into the
   gap at the end of each row, and a couple lying flat are what sell it. */
.shelf {
    position: absolute; right: 20px; top: 8px; width: 112px; height: 112px;
    border: 6px solid #5d4a3a; border-bottom-width: 8px;
    background: #241c16;                     /* the case interior, in shadow */
    display: flex; flex-direction: column;
    padding: 0 2px;
}
.shelf .row {
    display: flex; align-items: flex-end; gap: 1px;
    height: 33px; padding: 0 1px;
    border-bottom: 4px solid #5d4a3a;        /* the board the books stand on */
    box-shadow: inset 0 3px 4px -2px rgba(0, 0, 0, .85);   /* the case shades its own contents */
}
.shelf .row:last-child { height: 32px; border-bottom: 0; }
.shelf .row div {
    width: var(--w); height: var(--h); background: var(--c);
    transform: rotate(var(--lean, 0deg)); transform-origin: bottom left;
    border-top: 2px solid rgba(255, 255, 255, .22);        /* the top edge catches the light */
    box-shadow: inset -1px 0 0 rgba(0, 0, 0, .45);         /* separates one spine from the next */
}
/* two books lying flat, filling the gap at the end of the bottom shelf */
.shelf .row .flat {
    width: 26px; height: 11px; border-top: 0; box-shadow: none; transform: none;
    background:
        linear-gradient(#7a3b52, #7a3b52) 0 6px / 26px 5px no-repeat,
        linear-gradient(#d9a441, #d9a441) 3px 1px / 21px 5px no-repeat;
}
/* sprites: fixed boxes; every frame is its own img, shown via .on, so frame
   flips never touch the network. Bottom-anchored contain because the frames
   have different natural sizes and the feet are the collision anchor. */
.player { position: absolute; left: 146px; top: 212px; width: 74px; height: 84px; }
.mimikyu { position: absolute; left: 96px; top: 240px; width: 62px; height: 64px; }
.player img, .mimikyu img { display: none; inset: 0; width: 100%; height: 100%; object-fit: contain; object-position: center bottom; }
.player img.on, .mimikyu img.on { display: block; }
.mimikyu.moving { animation: mimibob .36s steps(2, jump-none) infinite; }
@keyframes mimibob { from { transform: translateY(0); } to { transform: translateY(-3px); } }
.roomnote { width: 440px; max-width: 100%; margin: 14px auto 8px; font-size: 13px; color: #777; line-height: 1.8; }
/* the room is a fixed pixel stage; shrink the whole thing on narrow screens
   rather than reflowing it */
@media (max-width: 500px) { .room, .roommodal { zoom: 0.68; } }

/* Reduced motion: nothing loops, nothing types, and the eruption is shown as
   already dried rather than replayed. */
@media (prefers-reduced-motion: reduce) {
    .cloud, .sparks, .sparks::before, .sparks::after, .steam, .steam::before,
    .mimikyu.moving, .typewriter span, .caret,
    .lavaflow .trail, .lavaflow .toe, .lavaflow .twinkle { animation: none; }
    .typewriter span { width: auto; }
    .lavaflow { filter: none; }
    .lavaflow .trail { stroke-dashoffset: 0; stroke: #45261d; }
    .lavaflow .toe { opacity: 1; offset-distance: 100%; transform: scale(2.2, 1.2); fill: #35211a; }
    .lavaflow .twinkle { opacity: 0; }
}
