Community widgets

A creation from the community

The Birthday Cabinet — Avery’s Paper Room

Made by ArkheOS

Solve a birthday room’s clock, keepsake shelf, and paper stars. Three keys reveal your personalized wish. Includes hints, Undo, Reset puzzles, and saved return. By ArkheOS.

Make your version

Play this widget, then change its rules, look, or purpose with Widget Canvas in ChatGPT.

Open Widget Canvas in ChatGPT

Copy the selected request. In ChatGPT, choose “Try in chat,” replace the example prompt, and paste it.

You are exploring the published defaults. Changes here stay in this browser and do not change the creator’s work.

Original creation

  1. The Birthday Cabinet — Avery’s Paper Room
    Made by ArkheOS
View published code

HTML

<svg class="definitions" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><defs>
<symbol id="sprig" viewBox="0 0 140 210"><g fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M25 202Q75 132 91 18M53 155Q21 135 16 107M67 122Q109 112 123 84M79 84Q47 66 49 39M87 48Q113 40 119 16"/><path d="M52 156Q16 157 16 107Q48 116 52 156ZM67 123Q103 139 123 84Q84 86 67 123ZM79 84Q40 85 49 39Q78 53 79 84ZM87 49Q116 58 119 16Q94 24 87 49Z" fill="currentColor" fill-opacity=".08"/><path d="M32 189Q10 174 7 157Q32 164 32 189ZM42 175Q77 178 88 150Q57 147 42 175Z" fill="currentColor" fill-opacity=".12"/><circle cx="90" cy="18" r="4" fill="#b48943" stroke="none"/><circle cx="124" cy="80" r="3" fill="#b48943" stroke="none"/></g></symbol>
<symbol id="cake" viewBox="0 0 240 185"><g stroke="#164e50" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M38 159Q120 183 202 159" fill="none"/><ellipse cx="120" cy="153" rx="77" ry="10" fill="#e8d4a4" stroke="#ad8541"/><path d="M58 105L58 146Q120 165 182 146L182 105Z" fill="#e8d8b6"/><path d="M58 105Q120 86 182 105L182 121Q170 135 160 119Q149 136 138 121Q126 138 113 121Q100 136 88 121Q72 134 58 121Z" fill="#fff9e9"/><ellipse cx="120" cy="105" rx="62" ry="15" fill="#fff9e9"/><path d="M111 93L111 55L127 55L127 93" fill="#d0ac66"/><path d="M114 64L124 69M114 76L124 81" stroke="#fff9e9"/><path d="M119 48Q105 34 120 18Q136 38 119 48Z" fill="#c59a4b" stroke="#ad8541"/><path d="M119 43L120 34" stroke="#fff8de"/><path d="M82 141l2 1m20 5l2 1m24 0l2-1m23-5l2-1" stroke="#b48943" stroke-width="3"/><path d="M40 65l-6-5m9-12l1-7m145 26l7-3m-14-16l4-7" stroke="#b48943"/></g></symbol>
<symbol id="parcel" viewBox="0 0 150 130"><g stroke="#164e50" stroke-width="1.8" stroke-linejoin="round"><path d="M30 57L120 57L120 115Q75 125 30 115Z" fill="#d6b573"/><path d="M26 45L124 45L124 63Q75 70 26 63Z" fill="#e1c68d"/><path d="M67 46h16v73H67Z" fill="#285e5c"/><path d="M75 44Q24 43 36 21Q50 3 75 44ZM75 44Q125 44 114 21Q101 4 75 44Z" fill="#f7ecd2"/><path d="M75 44L55 66M75 44L96 66" fill="none"/></g></symbol>
<symbol id="compass" viewBox="0 0 120 120"><g stroke="#b18a45" fill="none"><circle cx="60" cy="60" r="44"/><circle cx="60" cy="60" r="38" stroke-dasharray="1 6"/><path d="M60 8V20M60 100v12M8 60h12m80 0h12"/></g><path d="M60 26L69 53L94 60L67 69L60 94L51 67L26 60L53 51Z" fill="#215759"/><path d="M60 26v34l34 0-27-7Z" fill="#d6b573"/><circle cx="60" cy="60" r="4" fill="#fff6df"/></symbol>
<symbol id="cabinet-key" viewBox="0 0 100 50"><g fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><circle cx="23" cy="25" r="13"/><path d="M36 25h49m-12 0v11m10-11v8"/><circle cx="23" cy="25" r="5" stroke-width="1"/></g></symbol>
<symbol id="shell" viewBox="0 0 100 85"><g stroke="#1c5654" stroke-width="1.8" stroke-linejoin="round"><path d="M47 70Q7 52 13 32Q15 21 27 23Q29 4 44 14Q54-4 64 15Q83 11 83 29Q102 41 77 64L57 74Z" fill="#ead3ab"/><path d="M49 66L25 29M51 66L43 20M54 66L61 21M56 67L78 33M45 69L19 47" fill="none"/><path d="M43 68L43 77L62 77L59 69Z" fill="#f5e7cc"/></g></symbol>
<symbol id="acorn" viewBox="0 0 100 85"><g stroke="#1c5654" stroke-width="1.8" stroke-linejoin="round"><path d="M33 36Q26 66 51 78Q76 64 70 36Z" fill="#cda96b"/><path d="M29 34Q29 14 52 16Q76 17 76 35L70 43L32 41Z" fill="#7f9680"/><path d="M50 16Q50 5 59 5M36 24l5 6 7-6 7 7 7-6 7 5M44 42Q39 59 50 69" fill="none"/></g></symbol>
<symbol id="paper-boat" viewBox="0 0 100 85"><g stroke="#1c5654" stroke-width="1.8" stroke-linejoin="round"><path d="M9 45L91 45L74 68L28 68Z" fill="#d8c38c"/><path d="M9 45L50 14L55 45Z" fill="#fff9e9"/><path d="M55 45L72 25L91 45Z" fill="#b2c5b3"/><path d="M50 14L42 45M28 68L55 45L74 68" fill="none"/><path d="M8 75q14 6 27 0q14 6 27 0q14 6 27 0" fill="none" stroke="#b18a45"/></g></symbol>
</defs></svg>
<main class="cabinet-app">
<header class="mast"><span class="eyebrow">THE LITTLE PAPER ATELIER</span><span class="edition">a birthday escape, made for you</span></header>
<div class="room-heading"><p class="eyebrow">THREE KEYS · ONE LITTLE SURPRISE</p><h1>The Birthday Cabinet</h1><p>Unfold a paper room. Follow its clues. Find what’s waiting for you.</p></div>
<div id="book" class="book"><section id="cover" class="paper cover" aria-label="Birthday card cover">
<div class="border"></div><svg class="botanical left" aria-hidden="true"><use href="#sprig"/></svg><svg class="botanical right" aria-hidden="true"><use href="#sprig"/></svg>
<div class="ribbon" aria-hidden="true"></div><svg class="bow" viewBox="0 0 110 90" aria-hidden="true"><g stroke="#956e32" stroke-width="1.3" fill="#d1af70"><path d="M55 41Q0 49 8 17Q22-5 55 41ZM55 41Q111 49 103 17Q89-5 55 41Z"/><path d="M51 42L30 83L49 77L56 89L61 45M59 42L91 76L75 74L70 88L53 44"/><ellipse cx="56" cy="41" rx="9" ry="6"/></g></svg>
<p class="eyebrow cover-kicker">A DAY JUST FOR YOU</p><h1>Happy<br><em>Birthday</em></h1><p class="recipient" id="cover-name">Avery</p>
<svg class="cake" aria-label="An original illustration of a little cake with a gold candle" role="img"><use href="#cake"/></svg>
<p class="small-note">a little joy, folded with love</p>
</section>

<section id="inside" class="paper cabinet-room" hidden aria-label="The Birthday Cabinet paper room">
<div class="room-cornice" aria-hidden="true"><span>✧</span><span>✦</span><span>✧</span></div>
<div class="key-rail" aria-label="Collected keys"><span class="eyebrow">THE KEY RING</span><div class="key-tokens"><span id="token-0" class="key-token"><svg aria-hidden="true"><use href="#cabinet-key"/></svg><span>Clock</span></span><span id="token-1" class="key-token"><svg aria-hidden="true"><use href="#cabinet-key"/></svg><span>Shelf</span></span><span id="token-2" class="key-token"><svg aria-hidden="true"><use href="#cabinet-key"/></svg><span>Stars</span></span></div><p id="key-count">0 of 3 keys found</p></div>
<div class="room-objects">
<section id="clock-object" class="room-object clock-object" aria-labelledby="clock-title">
<div class="object-label"><span class="object-no">01</span><h2 id="clock-title">The invitation clock</h2></div>
<div class="clock-vignette"><div class="invitation"><span class="pin" aria-hidden="true"></span><span class="eyebrow">YOU’RE INVITED</span><p>Tea, cake &<br>a little celebration</p><strong>At half past four.</strong><span class="tiny">With love, at the cabinet.</span></div>
<div class="clock-housing"><svg class="clock-face" viewBox="0 0 180 205" role="img" aria-labelledby="clock-svg-title"><title id="clock-svg-title">Clock showing 12:00</title><path d="M29 177L23 195h28l8-18m62 0 8 18h28l-6-18" fill="#b79558" stroke="#1c5654" stroke-width="2"/><path d="M20 93Q20 13 90 9Q160 13 160 93V180H20Z" fill="#366a62" stroke="#164e50" stroke-width="2"/><path d="M27 94Q27 20 90 17Q153 20 153 94V173H27Z" fill="none" stroke="#c5a66a"/><circle cx="90" cy="91" r="59" fill="#fff5df" stroke="#b99557" stroke-width="3"/><g fill="#164e50" font-family="Georgia,serif" font-size="13" text-anchor="middle"><text x="90" y="48">XII</text><text x="137" y="96">III</text><text x="90" y="143">VI</text><text x="43" y="96">IX</text></g><g stroke="#164e50" stroke-linecap="round"><line id="hour-hand" x1="90" y1="91" x2="90" y2="61" stroke-width="5"/><line id="minute-hand" x1="90" y1="91" x2="90" y2="47" stroke-width="2.5"/></g><circle cx="90" cy="91" r="5" fill="#b99557"/><path id="clock-door" d="M60 163H120" stroke="#b99557" stroke-width="3"/><svg id="clock-key" x="58" y="147" width="66" height="30" aria-hidden="true" hidden><use href="#cabinet-key"/></svg></svg><output id="clock-readout" aria-label="Current clock time">12:00</output></div></div>
<p class="object-instruction">Set the hands to match the invitation.</p>
<div id="clock-controls" class="clock-controls"><div class="stepper"><span>Hour</span><button id="hour-minus" aria-label="Move hour back one">−</button><button id="hour-plus" aria-label="Move hour forward one">+</button></div><div class="stepper"><span>Minute</span><button id="minute-minus" aria-label="Move minute back fifteen">−</button><button id="minute-plus" aria-label="Move minute forward fifteen">+</button></div></div>
<button id="check-clock" class="object-action">Try the time</button><p id="clock-feedback" class="object-feedback" role="status">The little brass door is shut.</p>
<details class="hint"><summary>A gentle hint</summary><p>“Half past four” is 4:30: the short hand between IV and V, the long hand on VI. Minute buttons move by fifteen minutes.</p></details></section>
<section id="shelf-object" class="room-object shelf-object" aria-labelledby="shelf-title">
<div class="object-label"><span class="object-no">02</span><h2 id="shelf-title">The keepsake shelf</h2></div>
<div class="shelf-note"><span aria-hidden="true">❧</span><p>“The shell came before the acorn.<br>The paper boat was last.”</p><span class="tiny">a note left in the grain of the wood</span></div>
<div class="shelf" id="shelf"><button id="keepsake-0" class="keepsake" aria-pressed="false"><svg aria-hidden="true"><use id="keepsake-use-0" href="#acorn"/></svg><span id="keepsake-label-0">Acorn</span><small>1 · left</small></button><button id="keepsake-1" class="keepsake" aria-pressed="false"><svg aria-hidden="true"><use id="keepsake-use-1" href="#paper-boat"/></svg><span id="keepsake-label-1">Paper boat</span><small>2 · middle</small></button><button id="keepsake-2" class="keepsake" aria-pressed="false"><svg aria-hidden="true"><use id="keepsake-use-2" href="#shell"/></svg><span id="keepsake-label-2">Shell</span><small>3 · right</small></button></div>
<div id="shelf-drawer" class="shelf-drawer"><span id="drawer-label">a drawer with no handle</span><svg id="shelf-key" aria-hidden="true" hidden><use href="#cabinet-key"/></svg></div>
<p id="swap-instruction" class="object-instruction">Choose two keepsakes to swap their places, from left to right.</p><button id="check-shelf" class="object-action">Check the shelf</button><p id="shelf-feedback" class="object-feedback" role="status">The shelf is waiting for its story.</p>
<details class="hint"><summary>A gentle hint</summary><p>The boat must be on the right. With the shell before the acorn, the full order is shell, acorn, paper boat. Choose one keepsake, then another to swap them.</p></details></section>
<section id="stars-object" class="room-object stars-object" aria-labelledby="stars-title">
<div class="object-label"><span class="object-no">03</span><h2 id="stars-title">The paper constellation</h2></div>
<div class="star-window"><svg viewBox="0 0 220 220" class="constellation" role="img" aria-labelledby="stars-svg-title stars-svg-desc"><title id="stars-svg-title">Three turning paper star rings</title><desc id="stars-svg-desc">Match the ink stars to the hollow gold circles. The target outer pair points north and southeast; middle pair west and south; inner pair north and east.</desc>
<path d="M15 208V96Q15 12 110 8Q205 12 205 96V208Z" fill="#164e50" stroke="#ad894c" stroke-width="3"/><path d="M23 201V96Q23 20 110 16Q197 20 197 96V201Z" fill="none" stroke="#ab985e"/>
<g transform="translate(110 113)"><g fill="none" stroke="#e5ca90" stroke-width="1"><circle r="73" stroke-dasharray="2 5" opacity=".5"/><circle r="49" stroke-dasharray="2 5" opacity=".5"/><circle r="23" stroke-dasharray="2 5" opacity=".5"/>
<path d="M0-73L52 52M-49 0L0 49M0-23L23 0" stroke-dasharray="3 4"/><g stroke-width="1.8"><circle cx="0" cy="-73" r="6"/><circle cx="52" cy="52" r="6"/><circle cx="-49" cy="0" r="6"/><circle cx="0" cy="49" r="6"/><circle cx="0" cy="-23" r="6"/><circle cx="23" cy="0" r="6"/></g></g>
<g id="ring-0" fill="#fff4d7" stroke="#fff4d7"><path d="M0-73L52 52" fill="none" stroke-width="1.5"/><path d="M0-78L2-75L5-73L2-71L0-68L-2-71L-5-73L-2-75ZM52 47l2 3 3 2-3 2-2 3-2-3-3-2 3-2Z"/></g>
<g id="ring-1" fill="#d2b37a" stroke="#d2b37a"><path d="M-49 0L0 49" fill="none" stroke-width="1.5"/><circle cx="-49" cy="0" r="3.5"/><circle cx="0" cy="49" r="3.5"/></g>
<g id="ring-2" fill="#c6d7c0" stroke="#c6d7c0"><path d="M0-23L23 0" fill="none" stroke-width="1.5"/><path d="M0-27L4-23L0-19L-4-23ZM23-4L27 0L23 4L19 0Z"/></g></g><svg id="stars-key" x="82" y="190" width="62" height="30" aria-hidden="true" hidden><use href="#cabinet-key"/></svg></svg></div>
<p class="object-instruction">Turn each paper ring until its stars sit inside the gold tracing.</p>
<div class="star-controls"><button id="rotate-0">Turn outer ring ↻<small id="ring-label-0"></small></button><button id="rotate-1">Turn middle ring ↻<small id="ring-label-1"></small></button><button id="rotate-2">Turn inner ring ↻<small id="ring-label-2"></small></button></div>
<button id="check-stars" class="object-action">Pin the stars</button><p id="stars-feedback" class="object-feedback" role="status">The paper sky has a secret seam.</p>
<details class="hint"><summary>A gentle hint</summary><p>Each turn is a quarter turn clockwise. Match outer stars to north & southeast, middle to west & south, inner to north & east. “North” is up. The current directions are written on each button.</p></details></section>

</div>
<section class="gift-table" aria-labelledby="present-title"><svg class="table-sprig" aria-hidden="true"><use href="#sprig"/></svg><div class="table-top"><svg class="table-cake" aria-hidden="true"><use href="#cake"/></svg><div class="present-object"><h2 id="present-title">The birthday present</h2><button id="gift" class="gift locked" aria-disabled="true" aria-expanded="false" aria-controls="surprise"><svg aria-hidden="true"><use href="#parcel"/></svg><span id="gift-label">Three keys turn together</span><small id="present-action">Find all three keys to open</small></button><p id="lock-status" class="tiny">Clock · Shelf · Stars</p></div></div>
<article id="surprise" class="surprise earned-ending" hidden aria-label="Your earned birthday wish"><p class="eyebrow">THE BEST PART WAS ALWAYS FOR YOU</p><svg class="ending-compass" aria-hidden="true"><use href="#compass"/></svg><h2 id="dear">Dear Avery,</h2><p id="letter" class="letter"></p><div class="gold-rule" aria-hidden="true">✦</div><p class="signoff">A very happy birthday.<br>Here’s to all the doors you’ll open.</p></article></section>
</section></div>
<nav class="actions" aria-label="Card and puzzle controls"><button id="fold" class="primary" aria-expanded="false" aria-controls="inside">Unfold the room</button><button id="personalize" aria-expanded="false" aria-controls="editor">Personalize</button><button id="undo" disabled>Undo</button><button id="reset">Reset puzzles</button></nav>
<section id="editor" class="editor paper" hidden aria-label="Personalize your card"><div class="editor-top"><h2>Your words, in ink</h2><button id="done">Done</button></div><label for="recipient">For</label><input id="recipient" maxlength="60" autocomplete="off" value="Avery"><label for="message">The wish inside the present</label><textarea id="message" maxlength="600" rows="4">A little adventure, made just for you.</textarea><p class="editor-note">These words wait inside the present. Edits save automatically.</p></section>
<footer><p id="status" role="status" aria-live="polite">A little room is waiting inside.</p><p class="keyboard">Tab to explore · Enter or Space to activate · Esc to close editing<br>Untimed · No dragging · Your progress saves automatically</p></footer></main>

CSS

*{box-sizing:border-box}html{color-scheme:light}body{margin:0;color:#164e50;background:#e9e9df;font:14px/1.5 system-ui,sans-serif}button,input,textarea{font:inherit}button{cursor:pointer;min-height:44px;border:1px solid #a4b4a7;border-radius:24px;padding:10px 17px;color:#164e50;background:#f9f3e4}button:hover{background:#eee2c9}button:disabled{opacity:.45;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:3px solid #8b6228;outline-offset:4px}main{max-width:860px;margin:auto;padding:24px 18px 18px;min-width:0}.definitions{position:absolute;width:0;height:0;overflow:hidden}.mast{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;margin:0 0 23px}.eyebrow{font:10px/1.7 system-ui,sans-serif;letter-spacing:2.3px}.edition{font:italic 13px Georgia,serif;color:#5a7068}.book{max-width:380px;margin:0 auto;perspective:1400px;transition:max-width .6s ease}.book.open{max-width:760px}.paper{background-color:#fbf3e1;background-image:radial-gradient(#9d7a3d14 .6px,transparent .8px),repeating-linear-gradient(92deg,transparent 0px,transparent 8px,#b6a17405 9px);background-size:5px 5px,100% 100%;box-shadow:0 16px 36px #354e4020,2px 3px 2px #5b574820;border:1px solid #d8c9a6}.cover{min-height:478px;position:relative;text-align:center;padding:42px 38px 27px;isolation:isolate;overflow:hidden;border-radius:2px 6px 6px 2px;border-left:7px solid #e6d8bb}.cover:after{content:"";position:absolute;inset:0;box-shadow:inset 10px 0 15px #6e57300b;pointer-events:none}.border{position:absolute;inset:18px;border:1px solid #b18a4566;z-index:-1}.border:after{content:"";position:absolute;inset:4px;border:1px solid #b18a4529}.cover-kicker{margin:5px 0 18px;font-size:9px}.cover h1{font:normal clamp(36px,8vw,48px)/1.06 Georgia,serif;margin:0;letter-spacing:-1px}.cover h1 em{font-weight:normal}.recipient{font:italic 27px/1.3 Georgia,serif;margin:18px 0 0;overflow-wrap:anywhere;position:relative;z-index:1}.cake{display:block;width:210px;max-width:100%;height:164px;margin:9px auto 0}.small-note{font:italic 12px Georgia,serif;margin:6px 0 3px}.botanical{position:absolute;width:95px;height:170px;opacity:.85;z-index:-1}.botanical.left{left:-7px;bottom:12px;transform:rotate(-15deg)}.botanical.right{right:-7px;top:80px;transform:rotate(175deg)}.ribbon{position:absolute;top:0;bottom:0;right:28px;width:13px;background:linear-gradient(90deg,#ab8046,#e0c893 42%,#bb965a);opacity:.8;z-index:-1}.bow{position:absolute;width:83px;height:70px;right:-4px;top:19px;transform:rotate(12deg)}.inside{display:grid;grid-template-columns:.9fr 1.1fr;min-height:478px;border-radius:3px;transform-origin:left center;animation:unfold .65s ease both}.left-page{position:relative;padding:60px 22px 25px;text-align:center;border-right:1px solid #c2b28f;background:linear-gradient(90deg,transparent 92%,#79633f10);overflow:hidden}.left-page>.eyebrow{font-size:9px}.wonder{font:normal 32px/1.2 Georgia,serif;margin:16px 0 7px}.wonder em{font-size:25px}.inner-sprig{position:absolute;width:90px;height:135px;left:-7px;top:-10px;transform:rotate(45deg);opacity:.5}.inner-cake{width:190px;max-width:100%;height:165px}.gold-star{display:block;color:#a57b37;font-size:24px}.right-page{padding:36px 30px 24px;text-align:center;min-width:0;background:linear-gradient(90deg,#806b3c09,transparent 6%)}.right-page>.eyebrow{font-size:9px}.right-page h2{font:italic 27px/1.3 Georgia,serif;margin:21px 0 17px;overflow-wrap:anywhere}.letter{font:21px/1.55 Georgia,serif;white-space:pre-wrap;overflow-wrap:anywhere;margin:0;min-height:66px}.gold-rule{color:#a77e3d;display:flex;align-items:center;gap:12px;justify-content:center;margin:20px 0 13px}.gold-rule:before,.gold-rule:after{content:"";width:40px;height:1px;background:#bd9d63}.signoff{font:italic 14px Georgia,serif;margin:0 0 13px}.gift{display:flex;flex-direction:column;align-items:center;border:0;background:transparent;border-radius:10px;margin:0 auto;padding:2px 6px 8px;max-width:100%}.gift:hover{background:#e8dcb747}.gift svg{width:93px;height:85px;transition:transform .3s}.gift:hover svg{transform:rotate(-4deg)}.gift span{font:italic 14px Georgia,serif}.gift small{font-size:10px;letter-spacing:.6px;margin-top:4px}.surprise{padding:8px 12px 15px;border:1px solid #bc9a5e;margin-top:10px;animation:appear .45s ease both;background:#fff9eb;box-shadow:2px 3px 0 #e5d8b7}.surprise svg{width:80px;height:80px}.surprise p{font:italic 20px/1.3 Georgia,serif;margin:3px 0 8px}.surprise small{display:block;font-size:11px}.actions{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin:24px 0 13px}.primary{background:#164e50;color:#fff7e6;border-color:#164e50}.primary:hover{background:#286365}.editor{max-width:580px;margin:18px auto 0;padding:20px 22px;border-radius:3px}.editor-top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px}.editor h2{font:italic 23px Georgia,serif;margin:0}.editor-top button{padding:5px 14px}.editor label{display:block;font-size:12px;margin:12px 0 6px}.editor input,.editor textarea{display:block;width:100%;min-width:0;border:0;border-bottom:1px solid #80998c;background:#ffffff55;color:#164e50;padding:10px 8px;border-radius:0;font:18px/1.5 Georgia,serif}.editor textarea{resize:vertical}.editor-note{font-size:11px;color:#4d635b;margin:12px 0 0}footer{text-align:center;color:#4d635b;font-size:11px}footer p{margin:5px 0}.keyboard{font-size:10px;padding-top:4px}[hidden]{display:none!important}@keyframes unfold{from{opacity:.5;transform:rotateY(-13deg)}to{opacity:1;transform:rotateY(0)}}@keyframes appear{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@media(max-width:540px){main{padding:18px 12px}.mast{margin-bottom:18px}.edition{font-size:11px}.inside{grid-template-columns:1fr}.left-page{padding:24px 20px 14px;border-right:0;border-bottom:1px solid #c2b28f;min-height:170px;background:linear-gradient(0deg,#79633f10,transparent 8%)}.wonder{font-size:25px;margin:9px 0}.wonder br{display:none}.wonder em{font-size:23px}.inner-cake{width:110px;height:86px}.gold-star{position:absolute;right:25px;top:60px}.right-page{padding:28px 24px}.actions{gap:7px}.actions button{padding:9px 14px}.cover{padding-left:27px;padding-right:27px}.editor{padding:17px}.eyebrow{letter-spacing:1.6px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}.gift:hover svg{transform:none}}
.room-heading{text-align:center;margin:0 0 24px}.room-heading>h1{font:normal clamp(30px,6vw,43px)/1.15 Georgia,serif;margin:8px 0 10px;letter-spacing:-1px}.room-heading>p:last-child{font:italic 14px/1.5 Georgia,serif;max-width:420px;margin:0 auto}.cabinet-app{max-width:950px}.book.open{max-width:860px}.cabinet-room{border-radius:4px;position:relative;overflow:hidden;background-color:#f4ecd8;background-image:radial-gradient(ellipse at 50% 0,#9c85571b 1px,transparent 2px),linear-gradient(90deg,#164e5005 1px,transparent 1px),linear-gradient(90deg,#b89b6310,transparent 25%,#b89b630b);background-size:24px 28px,24px 100%,100% 100%;animation:unfold .65s ease both}.room-cornice{display:flex;justify-content:space-around;align-items:center;height:35px;margin:8px 8px 0;border:1px solid #ad8b4e;border-bottom:4px double #ad8b4e;color:#ad8b4e;background:#fff6e1;font-size:16px}.key-rail{display:flex;align-items:center;justify-content:center;gap:20px;flex-wrap:wrap;padding:18px 18px 12px}.key-rail>.eyebrow{font-size:9px}.key-tokens{display:flex;gap:13px}.key-token{display:flex;flex-direction:column;align-items:center;color:#62756a;opacity:.7;font:10px system-ui,sans-serif}.key-token svg{width:47px;height:25px}.key-token.found{color:#826020;opacity:1}.key-token.found svg{filter:drop-shadow(0 2px 1px #9d713433)}#key-count{font:italic 12px Georgia,serif;margin:0}.room-objects{display:grid;grid-template-columns:1fr 1fr;gap:8px 26px;padding:0 25px 28px;align-items:start}.room-object{min-width:0;position:relative;text-align:center;padding:12px 0 20px}.object-label{display:flex;align-items:center;justify-content:center;gap:9px;margin:4px 0 17px}.object-no{font:10px Georgia,serif;border:1px solid #b2945b;border-radius:50%;width:25px;height:25px;display:grid;place-items:center;color:#7d602d;flex:none}.object-label h2{font:normal 20px/1.2 Georgia,serif;margin:0}.clock-vignette{display:flex;gap:9px;align-items:center;justify-content:center;min-height:237px}.invitation{position:relative;background:#fff9e8;border:1px solid #b49b6b;box-shadow:2px 5px 0 #ae997125;padding:18px 9px 14px;transform:rotate(-4deg);width:48%;max-width:168px}.pin{position:absolute;top:-5px;left:50%;width:7px;height:7px;border:1px solid #89672f;border-radius:50%;background:#ccb06f;box-shadow:1px 2px 2px #50482533}.invitation .eyebrow{font-size:8px;letter-spacing:1px}.invitation p{font:italic 17px/1.4 Georgia,serif;margin:14px 0}.invitation strong{font:normal 15px/1.3 Georgia,serif;border-block:1px solid #b49b6b66;display:block;padding:7px 0;margin-bottom:11px}.tiny{font:10px/1.5 system-ui,sans-serif;color:#4b6358}.invitation .tiny{display:block;font-size:8px}.clock-housing{width:47%;max-width:165px}.clock-face{width:100%;height:auto;display:block;overflow:visible}.clock-face line{transition:transform .2s}.clock-housing output{display:block;font:14px Georgia,serif;letter-spacing:1.5px;margin-top:4px;color:#164e50}.clock-controls{display:flex;gap:12px;justify-content:center;margin:12px 0}.stepper{display:grid;grid-template-columns:44px 44px;gap:4px}.stepper>span{grid-column:1/-1;font-size:10px;letter-spacing:1px;text-transform:uppercase}.stepper button{padding:4px;border-radius:50%;font-size:20px;min-width:44px;min-height:44px;background:#fff8e7}.object-instruction{font:italic 13px/1.5 Georgia,serif;max-width:330px;margin:10px auto 12px;padding:0 5px}.object-action{font-size:12px;border-color:#567c6b;background:#e4e7d6;padding:9px 16px}.object-feedback{font:11px/1.5 system-ui,sans-serif;min-height:33px;margin:12px 0 6px;color:#3c6257}.hint{border-top:1px solid #a3895638;margin:0 auto;max-width:340px;text-align:left}.hint summary{font:italic 12px Georgia,serif;min-height:44px;padding:14px 5px;cursor:pointer}.hint p{font:12px/1.6 Georgia,serif;margin:0 5px 14px}.room-object.solved .object-label h2{color:#76551f}.room-object.solved .object-no{background:#d9c18d;border-color:#9a7637}.room-object.solved .object-feedback{color:#76551f;font-weight:600}.room-object.solved .clock-face{filter:drop-shadow(0 2px 6px #b5945033)}.star-window{max-width:217px;margin:-8px auto 0}.constellation{display:block;width:100%;height:auto}.constellation [id^=ring-]{transition:transform .25s}.star-controls{display:flex;flex-direction:column;gap:6px;max-width:270px;margin:0 auto 12px}.star-controls button{font:11px/1.3 system-ui,sans-serif;border-radius:4px;background:#fff8e8;padding:7px;min-height:48px}.star-controls small{display:block;font-size:9px;margin-top:4px;color:#49675c}.shelf-object{grid-column:1/-1;padding-top:6px;border-top:1px solid #a3895655}.shelf-note{max-width:330px;margin:auto;position:relative;padding:9px 8px 15px}.shelf-note>span:first-child{font:26px Georgia,serif;color:#98763b;position:absolute;left:0;top:12px}.shelf-note p{font:italic 16px/1.5 Georgia,serif;margin:5px 0 7px}.shelf{display:flex;gap:8px;justify-content:center;max-width:450px;margin:5px auto 0;padding:0 8px 6px;border-bottom:10px solid #b8975e;box-shadow:0 5px 0 #6d583b33}.keepsake{flex:1;min-width:0;max-width:140px;border:1px solid transparent;background:transparent;border-radius:5px;padding:8px 2px 5px;display:flex;align-items:center;flex-direction:column}.keepsake svg{width:100%;max-width:100px;height:85px}.keepsake span{font:italic 15px Georgia,serif;max-width:100%;overflow-wrap:anywhere}.keepsake small{font-size:9px;margin-top:8px}.keepsake[aria-pressed=true]{background:#fff6dc;border:1px dashed #876222;box-shadow:0 0 0 2px #e0cb9c}.shelf-drawer{height:34px;max-width:365px;margin:5px auto 0;display:flex;align-items:center;justify-content:center;border:1px solid #9e8556;background:#d9c59a;box-shadow:0 4px 0 #78634222;transition:height .3s,transform .3s;font:italic 11px Georgia,serif}.shelf-drawer svg{width:58px;height:29px;color:#735419}.shelf-object.solved .shelf-drawer{height:46px;transform:translateY(3px);border-bottom:5px solid #a98750;background:#f3e6c8}.shelf-object.solved .keepsake{background:#fff9e640}.shelf-object .object-feedback{min-height:17px}.gift-table{position:relative;border-top:7px double #9b855c;background:repeating-linear-gradient(94deg,#c2a1700d 0 2px,transparent 2px 22px),linear-gradient(#d7c59c,#e4d5b3);padding:20px 26px 28px;text-align:center;box-shadow:inset 0 7px 10px #75613b0d}.table-top{display:flex;align-items:center;justify-content:center;gap:28px}.table-cake{width:170px;height:155px}.present-object h2{font:italic 23px Georgia,serif;margin:0 0 8px}.gift{max-width:240px;margin:auto}.gift svg{width:115px;height:100px}.gift.locked svg{filter:saturate(.7)}.gift.ready svg{filter:drop-shadow(0 3px 5px #a47e3b55)}.gift[aria-disabled=true]{cursor:pointer}.gift small{font-size:10px}.table-sprig{position:absolute;left:-15px;bottom:15px;width:105px;height:170px;opacity:.3;pointer-events:none}.earned-ending{position:relative;max-width:530px;margin:20px auto 0;padding:25px clamp(16px,5vw,40px);background:#fff7e7;transform:rotate(-.5deg)}.earned-ending h2{font:italic 28px Georgia,serif;margin:5px 0 18px;overflow-wrap:anywhere}.earned-ending .letter{font-size:24px;min-height:0}.earned-ending .signoff{line-height:1.6;margin:0}.ending-compass{width:73px;height:73px;margin:8px auto 10px}.keyboard{line-height:1.8}.room-object button:disabled{opacity:.65}.room-object.solved .star-window{filter:drop-shadow(0 1px 7px #b99d5955)}
@media(max-width:600px){.room-objects{grid-template-columns:1fr;padding:0 15px 18px;gap:0}.clock-object{order:1}.shelf-object{order:2;padding-top:20px}.stars-object{order:3;border-top:1px solid #a3895655;padding-top:20px}.clock-vignette{min-height:220px;gap:12px}.invitation{max-width:145px;padding-inline:10px}.clock-housing{max-width:148px}.key-rail{gap:10px;padding:17px 8px 10px}.key-rail>.eyebrow{width:100%;text-align:center}.key-rail>p{width:100%;text-align:center}.key-tokens{gap:25px}.object-label h2{font-size:20px}.shelf{padding-inline:0;gap:3px}.keepsake span{font-size:13px}.keepsake svg{height:76px}.shelf-note p{font-size:14px}.shelf-note>span:first-child{left:-2px;font-size:21px}.gift-table{padding:20px 15px}.table-top{gap:8px;flex-direction:column}.table-cake{width:120px;height:95px;margin-bottom:-8px}.present-object h2{font-size:23px}.room-heading{margin-bottom:19px}.room-heading>p:last-child{font-size:13px}.room-heading h1{font-size:32px}.object-feedback{min-height:0}.star-window{max-width:215px}.star-controls{width:100%;max-width:280px}.shelf-object .object-instruction{font-size:12px}.actions button{font-size:12px}.earned-ending .letter{font-size:21px}}
@media(prefers-reduced-motion:reduce){.earned-ending{transform:none}.invitation{transform:none}}

.clock-object{grid-column:1;grid-row:1}.stars-object{grid-column:2;grid-row:1}.shelf-object{grid-column:1/-1;grid-row:2}#clock-key{color:#edc97c}#stars-key{color:#edc97c}@media(max-width:600px){.clock-object,.shelf-object,.stars-object{grid-column:1;grid-row:auto}}

JavaScript

(()=>{
'use strict';
const $=id=>document.getElementById(id),KEY='rowan-birthday-paper-v1';
const TITLES=['Clock','Shelf','Stars'],IDS=['clock','shelf','stars'],NAMES={shell:'Shell',acorn:'Acorn',boat:'Paper boat'},SYMBOLS={shell:'shell',acorn:'acorn',boat:'paper-boat'};
const freshPuzzles=()=>({hour:12,minute:0,order:['acorn','boat','shell'],rings:[1,2,3],solved:[false,false,false],opened:false});
const defaults=()=>({name:'Avery',message:'A little adventure, made just for you.',open:false,gift:false,cabinet:freshPuzzles()});
const copy=o=>JSON.parse(JSON.stringify(o)),numberIn=(v,a,b)=>Number.isInteger(v)&&v>=a&&v<=b;
function puzzleClean(p){
const q=freshPuzzles();if(!p||typeof p!=='object'||Array.isArray(p))return q;
if(numberIn(p.hour,1,12))q.hour=p.hour;
if([0,15,30,45].includes(p.minute))q.minute=p.minute;
if(Array.isArray(p.order)&&p.order.length===3&&new Set(p.order).size===3&&p.order.every(x=>Object.prototype.hasOwnProperty.call(NAMES,x)))q.order=p.order.slice();
if(Array.isArray(p.rings)&&p.rings.length===3&&p.rings.every(x=>numberIn(x,0,3)))q.rings=p.rings.slice();
if(Array.isArray(p.solved))q.solved=[p.solved[0]===true&&q.hour===4&&q.minute===30,p.solved[1]===true&&q.order.join(',')==='shell,acorn,boat',p.solved[2]===true&&q.rings.every(x=>x===0)];
q.opened=p.opened===true&&q.solved.every(Boolean);return q;
}
function clean(v){
if(!v||typeof v!=='object'||Array.isArray(v))return null;
const cabinet=puzzleClean(v.cabinet);
return {name:typeof v.name==='string'?v.name.slice(0,60):defaults().name,message:typeof v.message==='string'?v.message.slice(0,600):defaults().message,open:v.open===true,gift:cabinet.opened,cabinet};
}
const equal=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
let s=defaults(),history=[],editGroup=null,generation=0,saveChain=Promise.resolve(),contextChain=Promise.resolve(),restoreEpoch=0,contextSequence=0,selected=null;
const feedback=['The little brass door is shut.','The shelf is waiting for its story.','The paper sky has a secret seam.'];
const baseFeedback=feedback.slice();
function time(){return s.cabinet.hour+':'+String(s.cabinet.minute).padStart(2,'0');}
const directionSets=[
['north & southeast','east & southwest','south & northwest','west & northeast'],
['west & south','north & west','east & north','south & east'],
['north & east','east & south','south & west','west & north']
];
function render(syncFields=true){
const p=s.cabinet,all=p.solved.every(Boolean),count=p.solved.filter(Boolean).length;
$('book').classList.toggle('open',s.open);$('cover').hidden=s.open;$('inside').hidden=!s.open;
$('cover-name').textContent=s.name.trim()||'you';$('dear').textContent='Dear '+(s.name.trim()||'you')+',';
$('letter').textContent=p.opened?s.message:'';
$('fold').textContent=s.open?'Fold card closed':'Unfold the room';$('fold').setAttribute('aria-expanded',String(s.open));
$('hour-hand').setAttribute('transform','rotate('+((p.hour%12)*30+p.minute*.5)+' 90 91)');
$('minute-hand').setAttribute('transform','rotate('+(p.minute*6)+' 90 91)');
$('clock-svg-title').textContent='Clock showing '+time();$('clock-readout').textContent=time();
$('clock-door').toggleAttribute('hidden',p.solved[0]);
for(let i=0;i<3;i++){
$('token-'+i).classList.toggle('found',p.solved[i]);$('token-'+i).setAttribute('aria-label',TITLES[i]+' key '+(p.solved[i]?'found':'missing'));
$(IDS[i]+'-object').classList.toggle('solved',p.solved[i]);$(IDS[i]+'-key').toggleAttribute('hidden',!p.solved[i]);
$(IDS[i]+'-feedback').textContent=p.solved[i]?TITLES[i]+' key released.':feedback[i];
$('ring-'+i).setAttribute('transform','rotate('+(p.rings[i]*90)+')');
$('ring-label-'+i).textContent='Now: '+directionSets[i][p.rings[i]];
$('rotate-'+i).disabled=p.solved[2];$('rotate-'+i).setAttribute('aria-label','Turn '+['outer','middle','inner'][i]+' ring clockwise one quarter turn. Stars now point '+directionSets[i][p.rings[i]]);
const item=p.order[i];$('keepsake-use-'+i).setAttribute('href','#'+SYMBOLS[item]);$('keepsake-label-'+i).textContent=NAMES[item];
$('keepsake-'+i).setAttribute('aria-pressed',String(selected===i));$('keepsake-'+i).setAttribute('aria-label',NAMES[item]+', position '+(i+1)+(selected===i?', selected. Choose another keepsake to swap.':'. Choose to swap.'));$('keepsake-'+i).disabled=p.solved[1];
}
$('key-count').textContent=count+' of 3 keys found';
for(const id of ['hour-minus','hour-plus','minute-minus','minute-plus'])$(id).disabled=p.solved[0];
for(let i=0;i<3;i++){$('check-'+IDS[i]).disabled=p.solved[i];$('check-'+IDS[i]).textContent=p.solved[i]?'Key released':(['Try the time','Check the shelf','Pin the stars'][i]);}
$('drawer-label').hidden=p.solved[1];
$('swap-instruction').textContent=p.solved[1]?'The keepsakes tell their story. The drawer slides open.':selected===null?'Choose two keepsakes to swap their places, from left to right.':NAMES[p.order[selected]]+' selected. Choose another keepsake to swap, or choose it again to cancel.';
$('gift').setAttribute('aria-disabled',String(!all));$('gift').setAttribute('aria-expanded',String(p.opened));$('gift').classList.toggle('locked',!all);$('gift').classList.toggle('ready',all&&!p.opened);
$('gift-label').textContent=p.opened?'The secret was a wish.':all?'All three keys are yours.':'Three keys turn together';
$('present-action').textContent=p.opened?'Your present is open':all?'Open your birthday present':'Find all three keys to open';
$('lock-status').textContent=p.opened?'A little adventure, earned with curiosity.':all?'Three locks turn. A ribbon loosens.':TITLES.filter((_,i)=>!p.solved[i]).join(' · ')+' '+(count===2?'key still needed':'keys still needed');
$('surprise').hidden=!p.opened;$('undo').disabled=history.length===0;
if(syncFields){$('recipient').value=s.name;$('message').value=s.message;}
}
function snapshot(){if(!history.length||!equal(history[history.length-1],s)){history.push(copy(s));if(history.length>40)history.shift();}}
function publishContext(){
const epoch=restoreEpoch,seq=++contextSequence,p=s.cabinet;
const context={kind:'birthday-cabinet',recipient:s.name,wish:s.message,keys:TITLES.filter((_,i)=>p.solved[i]),puzzles:{clock:time(),keepsakes:p.order.map(x=>NAMES[x]),quarterTurns:p.rings.slice()},presentOpened:p.opened};
contextChain=contextChain.catch(()=>{}).then(async()=>{if(epoch!==restoreEpoch||seq!==contextSequence)return;try{if(window.WidgetCanvas&&typeof WidgetCanvas.setContext==='function')await WidgetCanvas.setContext(context);}catch(e){}});
}
function persist(){
const stamp=++generation,payload={kind:KEY,version:1,card:copy(s),history:history.map(copy)};
$('status').textContent='Saving your little room…';
saveChain=saveChain.catch(()=>{}).then(async()=>{if(stamp!==generation)return;try{
if(!window.WidgetCanvas||typeof WidgetCanvas.setState!=='function')throw Error('No save bridge');
await WidgetCanvas.setState(payload);if(stamp===generation)$('status').textContent='Your room and words are saved.';
}catch(e){if(stamp===generation)$('status').textContent='Saving is unavailable. Keep this room open to retain your progress.';}});
}
function change(fn,options={}){
const next=copy(s);fn(next);next.gift=next.cabinet.opened;
if(equal(next,s))return false;
editGroup=null;snapshot();s=next;if(!options.keepSelection)selected=null;render();persist();publishContext();return true;
}
function restore(v){
if(!v||v.kind!==KEY||v.version!==1)return false;const next=clean(v.card);if(!next)return false;
++generation;++restoreEpoch;editGroup=null;selected=null;s=next;
history=Array.isArray(v.history)?v.history.slice(-40).map(clean).filter(Boolean):[];
while(history.length&&equal(history[history.length-1],s))history.pop();
feedback.splice(0,3,...baseFeedback);render();$('status').textContent='Welcome back. Your room and birthday words are restored.';return true;
}
$('fold').onclick=()=>change(next=>{next.open=!next.open;});
for(const [id,field,delta,mod,start] of [['hour-minus','hour',-1,12,1],['hour-plus','hour',1,12,1],['minute-minus','minute',-15,60,0],['minute-plus','minute',15,60,0]]){
$(id).onclick=()=>{if(s.cabinet.solved[0])return;feedback[0]=baseFeedback[0];change(next=>{next.cabinet[field]=((next.cabinet[field]-start+delta)%mod+mod)%mod+start;});};
}
for(let i=0;i<3;i++){
$('rotate-'+i).onclick=()=>{if(s.cabinet.solved[2])return;feedback[2]=baseFeedback[2];change(next=>{next.cabinet.rings[i]=(next.cabinet.rings[i]+1)%4;});};
$('keepsake-'+i).onclick=()=>{if(s.cabinet.solved[1])return;if(selected===i){selected=null;render(false);return;}if(selected===null){selected=i;render(false);return;}
const first=selected;feedback[1]=baseFeedback[1];change(next=>{const row=next.cabinet.order;[row[first],row[i]]=[row[i],row[first]];});};
}
function solve(i){
const p=s.cabinet;if(p.solved[i])return;
const correct=[p.hour===4&&p.minute===30,p.order.join(',')==='shell,acorn,boat',p.rings.every(x=>x===0)][i];
if(!correct){feedback[i]=['Not quite. Read the time written on the invitation.','Not quite. Read the note from left to right.','Not quite. Match every star to a hollow gold circle.'][i];render(false);return;}
change(next=>{next.cabinet.solved[i]=true;});
}
$('check-clock').onclick=()=>solve(0);$('check-shelf').onclick=()=>solve(1);$('check-stars').onclick=()=>solve(2);
$('gift').onclick=()=>{if(!s.cabinet.solved.every(Boolean)){$('status').textContent='The present needs all three keys. '+(3-s.cabinet.solved.filter(Boolean).length)+' still to find.';return;}if(s.cabinet.opened)return;change(next=>{next.cabinet.opened=true;});};
function editor(open){$('editor').hidden=!open;$('personalize').setAttribute('aria-expanded',String(open));editGroup=null;if(open)$('recipient').focus();else $('personalize').focus();}
$('personalize').onclick=()=>editor($('editor').hidden);$('done').onclick=()=>editor(false);
$('editor').addEventListener('keydown',e=>{if(e.key==='Escape'){e.preventDefault();editor(false);}});
for(const [id,key] of [['recipient','name'],['message','message']]){
$(id).addEventListener('focus',()=>{editGroup=null;});
$(id).addEventListener('input',e=>{const value=e.target.value.slice(0,key==='name'?60:600);if(value===s[key])return;if(editGroup!==id){snapshot();editGroup=id;}s[key]=value;
if(history.length&&equal(history[history.length-1],s)){history.pop();editGroup=null;}render(false);persist();publishContext();});
$(id).addEventListener('blur',()=>{editGroup=null;});
}
$('undo').onclick=()=>{while(history.length&&equal(history[history.length-1],s))history.pop();if(!history.length){render(false);return;}editGroup=null;selected=null;s=history.pop();feedback.splice(0,3,...baseFeedback);render();persist();publishContext();};
$('reset').onclick=()=>{selected=null;feedback.splice(0,3,...baseFeedback);const changed=change(next=>{next.cabinet=freshPuzzles();});if(!changed)render(false);};
render();
window.addEventListener('widgetcanvas:restore',e=>restore(e.detail));
async function loadStartup(){
const startupGeneration=generation;
try{if(!window.WidgetCanvas)return;const saved=await WidgetCanvas.getState();if(generation===startupGeneration)restore(saved);}
catch(e){if(generation===startupGeneration)$('status').textContent='Your room is ready. Saved progress could not be loaded.';}
}
try{if(window.WidgetCanvas){WidgetCanvas.setControlsPosition('top-right');WidgetCanvas.setResetEnabled(false);WidgetCanvas.setSharingEnabled(false);}}
catch(e){$('status').textContent='Your room is ready. Saving is unavailable.';}
loadStartup();
})();