/*! 98.css v0.2.6 - https://botoxparty.github.io/XP.css/ */@font-face{font-family:"Pixelated MS Sans Serif";font-style:normal;font-weight:400;src:url(/assets/ms_sans_serif-C3pax6mQ.woff) format("woff");src:url(/assets/ms_sans_serif-Du8rjN1q.woff2) format("woff2")}@font-face{font-family:"Pixelated MS Sans Serif";font-style:normal;font-weight:700;src:url(/assets/ms_sans_serif_bold-B8yxhAcs.woff) format("woff");src:url(/assets/ms_sans_serif_bold-D5dpRRHG.woff2) format("woff2")}@font-face{font-family:Perfect DOS VGA\ 437 Win;font-style:normal;font-weight:400;src:url(/assets/PerfectDOSVGA437Win-BZfDqYRn.woff2) format("woff2"),url(/assets/PerfectDOSVGA437Win-C4lD4zTj.woff) format("woff")}.surface{background:silver}h1{font-size:5rem}h2{font-size:2.5rem}h3{font-size:2rem}h4{font-size:1.5rem}u{border-bottom:.5px solid #222;text-decoration:none}a{color:#00f}a:focus{outline:1px dotted #00f}code,code *{font-family:monospace}pre{background-color:#000;color:silver;display:block;font-size:1rem;margin:0;padding:12px 8px}pre,pre *{font-family:Perfect DOS VGA\ 437 Win}summary:focus{outline:1px dotted #000}::-webkit-scrollbar{width:16px}::-webkit-scrollbar:horizontal{height:17px}::-webkit-scrollbar-corner{background:#dfdfdf}::-webkit-scrollbar-track{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='none' viewBox='0 0 2 2'%3E%3Cpath fill='silver' fill-rule='evenodd' d='M1 0H0v1h1v1h1V1H1z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M2 0H1v1H0v1h1V1h1z' clip-rule='evenodd'/%3E%3C/svg%3E")}::-webkit-scrollbar-thumb{background-color:#dfdfdf;box-shadow:inset -1px -1px #0a0a0a,inset 1px 1px #fff,inset -2px -2px gray,inset 2px 2px #dfdfdf}::-webkit-scrollbar-button:horizontal:end:increment,::-webkit-scrollbar-button:horizontal:start:decrement,::-webkit-scrollbar-button:vertical:end:increment,::-webkit-scrollbar-button:vertical:start:decrement{display:block}::-webkit-scrollbar-button:vertical:start{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='none' viewBox='0 0 16 17'%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M15 0H0v16h1V1h14z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M2 1H1v14h1V2h12V1z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M16 17H0v-1h15V0h1z' clip-rule='evenodd'/%3E%3Cpath fill='gray' fill-rule='evenodd' d='M15 1h-1v14H1v1h14z' clip-rule='evenodd'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M8 6H7v1H6v1H5v1H4v1h7V9h-1V8H9V7H8z' clip-rule='evenodd'/%3E%3C/svg%3E");height:17px}::-webkit-scrollbar-button:vertical:end{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='none' viewBox='0 0 16 17'%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M15 0H0v16h1V1h14z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M2 1H1v14h1V2h12V1z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M16 17H0v-1h15V0h1z' clip-rule='evenodd'/%3E%3Cpath fill='gray' fill-rule='evenodd' d='M15 1h-1v14H1v1h14z' clip-rule='evenodd'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M11 6H4v1h1v1h1v1h1v1h1V9h1V8h1V7h1z' clip-rule='evenodd'/%3E%3C/svg%3E");height:17px}::-webkit-scrollbar-button:horizontal:start{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='none' viewBox='0 0 16 17'%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M15 0H0v16h1V1h14z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M2 1H1v14h1V2h12V1z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M16 17H0v-1h15V0h1z' clip-rule='evenodd'/%3E%3Cpath fill='gray' fill-rule='evenodd' d='M15 1h-1v14H1v1h14z' clip-rule='evenodd'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M9 4H8v1H7v1H6v1H5v1h1v1h1v1h1v1h1z' clip-rule='evenodd'/%3E%3C/svg%3E");width:16px}::-webkit-scrollbar-button:horizontal:end{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='none' viewBox='0 0 16 17'%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M15 0H0v16h1V1h14z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M2 1H1v14h1V2h12V1z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M16 17H0v-1h15V0h1z' clip-rule='evenodd'/%3E%3Cpath fill='gray' fill-rule='evenodd' d='M15 1h-1v14H1v1h14z' clip-rule='evenodd'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M7 4H6v7h1v-1h1V9h1V8h1V7H9V6H8V5H7z' clip-rule='evenodd'/%3E%3C/svg%3E");width:16px}button{background:silver;border:none;border-radius:0;box-shadow:inset -1px -1px #0a0a0a,inset 1px 1px #fff,inset -2px -2px gray,inset 2px 2px #dfdfdf;box-sizing:border-box;font-size:11px;min-height:23px;min-width:75px;padding:0 12px}button:not(:disabled).active,button:not(:disabled):active{box-shadow:inset -1px -1px #fff,inset 1px 1px #0a0a0a,inset -2px -2px #dfdfdf,inset 2px 2px gray}button.focused,button:focus{outline:1px dotted #000;outline-offset:-4px}button::-moz-focus-inner{border:0}label{align-items:center;display:inline-flex}input,label,textarea{font-family:"Pixelated MS Sans Serif",Arial;font-size:11px}textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:none;border-radius:0;box-sizing:border-box;padding:3px 4px}textarea:focus{outline:none}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-color:#fff;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='none' viewBox='0 0 16 17'%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M15 0H0v16h1V1h14z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M2 1H1v14h1V2h12V1z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M16 17H0v-1h15V0h1z' clip-rule='evenodd'/%3E%3Cpath fill='gray' fill-rule='evenodd' d='M15 1h-1v14H1v1h14z' clip-rule='evenodd'/%3E%3Cpath fill='silver' d='M2 2h12v13H2z'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M11 6H4v1h1v1h1v1h1v1h1V9h1V8h1V7h1z' clip-rule='evenodd'/%3E%3C/svg%3E");background-position:top 2px right 2px;background-repeat:no-repeat;border:none;border-radius:0;box-sizing:border-box;font-family:"Pixelated MS Sans Serif",Arial;font-size:11px;height:21px;padding:3px 32px 3px 4px;position:relative}select:focus{background-color:navy;color:#fff;outline:none}select:focus option{background-color:#fff;color:#000}select:active{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='17' fill='none' viewBox='0 0 16 17'%3E%3Cpath fill='gray' fill-rule='evenodd' d='M0 0h16v17H0V0m1 16h14V1H1z' clip-rule='evenodd'/%3E%3Cpath fill='silver' d='M1 1h14v15H1z'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M12 7H5v1h1v1h1v1h1v1h1v-1h1V9h1V8h1z' clip-rule='evenodd'/%3E%3C/svg%3E")}option{font-family:"Pixelated MS Sans Serif",Arial;font-size:11px}.vertical-bar{background:silver;box-shadow:inset -1px -1px #0a0a0a,inset 1px 1px #fff,inset -2px -2px gray,inset 2px 2px #dfdfdf;height:20px;width:4px}&:disabled,&:disabled+label{color:gray;text-shadow:1px 1px 0 #fff}input[type=radio]{appearance:none;-webkit-appearance:none;-moz-appearance:none;background:0;border:none;margin:0;opacity:0;position:fixed}input[type=radio]+label{line-height:13px;margin-left:18px;position:relative}input[type=radio]+label:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' viewBox='0 0 12 12'%3E%3Cpath fill='gray' fill-rule='evenodd' d='M8 0H4v1H2v1H1v2H0v4h1v2h1V8H1V4h1V2h2V1h4v1h2V1H8z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M8 1H4v1H2v2H1v4h1v1h1V8H2V4h1V3h1V2h4v1h2V2H8z' clip-rule='evenodd'/%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M9 3h1v1H9zm1 5V4h1v4zm-2 2V9h1V8h1v2zm-4 0v1h4v-1zm0 0V9H2v1z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M11 2h-1v2h1v4h-1v2H8v1H4v-1H2v1h2v1h4v-1h2v-1h1V8h1V4h-1z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M4 2h4v1h1v1h1v4H9v1H8v1H4V9H3V8H2V4h1V3h1z' clip-rule='evenodd'/%3E%3C/svg%3E");content:"";display:inline-block;height:12px;left:-18px;margin-right:6px;position:absolute;top:0;width:12px}input[type=radio]:active+label:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' viewBox='0 0 12 12'%3E%3Cpath fill='gray' fill-rule='evenodd' d='M8 0H4v1H2v1H1v2H0v4h1v2h1V8H1V4h1V2h2V1h4v1h2V1H8z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M8 1H4v1H2v2H1v4h1v1h1V8H2V4h1V3h1V2h4v1h2V2H8z' clip-rule='evenodd'/%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M9 3h1v1H9zm1 5V4h1v4zm-2 2V9h1V8h1v2zm-4 0v1h4v-1zm0 0V9H2v1z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M11 2h-1v2h1v4h-1v2H8v1H4v-1H2v1h2v1h4v-1h2v-1h1V8h1V4h-1z' clip-rule='evenodd'/%3E%3Cpath fill='silver' fill-rule='evenodd' d='M4 2h4v1h1v1h1v4H9v1H8v1H4V9H3V8H2V4h1V3h1z' clip-rule='evenodd'/%3E%3C/svg%3E")}input[type=radio]:checked+label:after{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' fill='none' viewBox='0 0 4 4'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M3 0H1v1H0v2h1v1h2V3h1V1H3z' clip-rule='evenodd'/%3E%3C/svg%3E");content:"";display:block;height:4px;left:-14px;position:absolute;top:4px;width:4px}input[type=radio]:focus+label{outline:1px dotted #000}input[type=checkbox]{appearance:none;-webkit-appearance:none;-moz-appearance:none;background:0;border:none;margin:0;opacity:0;position:fixed}input[type=checkbox]+label{line-height:13px;margin-left:19px;position:relative}input[type=checkbox]+label:before{content:"";display:inline-block;height:13px;left:-19px;margin-right:6px;position:absolute;width:13px}input[type=checkbox]:focus+label{outline:1px dotted #000}input[type=checkbox]:active+label:before{background:silver}input[type=checkbox]:checked+label:after{content:"";display:block;height:7px;position:absolute;top:1px;width:7px}input[type=radio][disabled]+label:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' viewBox='0 0 12 12'%3E%3Cpath fill='gray' fill-rule='evenodd' d='M8 0H4v1H2v1H1v2H0v4h1v2h1V8H1V4h1V2h2V1h4v1h2V1H8z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M8 1H4v1H2v2H1v4h1v1h1V8H2V4h1V3h1V2h4v1h2V2H8z' clip-rule='evenodd'/%3E%3Cpath fill='%23dfdfdf' fill-rule='evenodd' d='M9 3h1v1H9zm1 5V4h1v4zm-2 2V9h1V8h1v2zm-4 0v1h4v-1zm0 0V9H2v1z' clip-rule='evenodd'/%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M11 2h-1v2h1v4h-1v2H8v1H4v-1H2v1h2v1h4v-1h2v-1h1V8h1V4h-1z' clip-rule='evenodd'/%3E%3Cpath fill='silver' fill-rule='evenodd' d='M4 2h4v1h1v1h1v4H9v1H8v1H4V9H3V8H2V4h1V3h1z' clip-rule='evenodd'/%3E%3C/svg%3E")}input[type=radio][disabled]:checked+label:after{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' fill='none' viewBox='0 0 4 4'%3E%3Cpath fill='gray' fill-rule='evenodd' d='M3 0H1v1H0v2h1v1h2V3h1V1H3z' clip-rule='evenodd'/%3E%3C/svg%3E")}input[type=checkbox][disabled]+label:before{background:silver}input[type=checkbox][disabled]:checked+label:after{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='7' fill='none' viewBox='0 0 7 7'%3E%3Cpath fill='gray' fill-rule='evenodd' d='M7 0H6v1H5v1H4v1H3v1H2V3H1V2H0v3h1v1h1v1h1V6h1V5h1V4h1V3h1z' clip-rule='evenodd'/%3E%3C/svg%3E")}input[type=text]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1px solid #7f9db9;border-radius:0;box-sizing:border-box;height:21px;line-height:2;padding:3px 4px}input[type=text]:focus{outline:none}input[type=password]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1px solid #7f9db9;border-radius:0;box-sizing:border-box;height:21px;line-height:2;padding:3px 4px}input[type=password]:focus{outline:none}input[type=email]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border:1px solid #7f9db9;border-radius:0;box-sizing:border-box;height:21px;line-height:2;padding:3px 4px}input[type=email]:focus{outline:none}input[type=range]{-webkit-appearance:none;background:transparent;width:100%}input[type=range]:focus{outline:none}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='21' fill='none' viewBox='0 0 11 21'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0v16h2v2h2v2h1v-1H3v-2H1V1h9V0Z' clip-rule='evenodd'/%3E%3Cpath fill='%23c0c7c8' fill-rule='evenodd' d='M1 1v15h1v1h1v1h1v1h2v-1h1v-1h1v-1h1V1Z' clip-rule='evenodd'/%3E%3Cpath fill='%2387888f' fill-rule='evenodd' d='M9 1h1v15H8v2H6v2H5v-1h2v-2h2Z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M10 0h1v16H9v2H7v2H5v1h1v-2h2v-2h2Z' clip-rule='evenodd'/%3E%3C/svg%3E");height:21px;transform:translateY(-8px);width:11px}input[type=range]::-moz-range-thumb{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='21' fill='none' viewBox='0 0 11 21'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0v16h2v2h2v2h1v-1H3v-2H1V1h9V0Z' clip-rule='evenodd'/%3E%3Cpath fill='%23c0c7c8' fill-rule='evenodd' d='M1 1v15h1v1h1v1h1v1h2v-1h1v-1h1v-1h1V1Z' clip-rule='evenodd'/%3E%3Cpath fill='%2387888f' fill-rule='evenodd' d='M9 1h1v15H8v2H6v2H5v-1h2v-2h2Z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M10 0h1v16H9v2H7v2H5v1h1v-2h2v-2h2Z' clip-rule='evenodd'/%3E%3C/svg%3E");border:0;border-radius:0;height:21px;transform:translateY(2px);width:11px}input[type=range]::-webkit-slider-runnable-track{background:#000;border-bottom:1px solid grey;border-right:1px solid grey;box-shadow:1px 0 0 #fff,1px 1px 0 #fff,0 1px 0 #fff,-1px 0 0 darkgrey,-1px -1px 0 darkgrey,0 -1px 0 darkgrey,-1px 1px 0 #fff,1px -1px darkgrey;box-sizing:border-box;height:2px;width:100%}input[type=range]::-moz-range-track{background:#000;border-bottom:1px solid grey;border-right:1px solid grey;box-shadow:1px 0 0 #fff,1px 1px 0 #fff,0 1px 0 #fff,-1px 0 0 darkgrey,-1px -1px 0 darkgrey,0 -1px 0 darkgrey,-1px 1px 0 #fff,1px -1px darkgrey;box-sizing:border-box;height:2px;width:100%}input[type=range].has-box-indicator::-webkit-slider-thumb{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='21' fill='none' viewBox='0 0 11 21'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0v20h1V1h9V0Z' clip-rule='evenodd'/%3E%3Cpath fill='%23c0c7c8' d='M1 1h8v18H1z'/%3E%3Cpath fill='%2387888f' fill-rule='evenodd' d='M9 1h1v19H1v-1h8Z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M10 0h1v21H0v-1h10Z' clip-rule='evenodd'/%3E%3C/svg%3E");transform:translateY(-10px)}input[type=range].has-box-indicator::-moz-range-thumb{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='21' fill='none' viewBox='0 0 11 21'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0v20h1V1h9V0Z' clip-rule='evenodd'/%3E%3Cpath fill='%23c0c7c8' d='M1 1h8v18H1z'/%3E%3Cpath fill='%2387888f' fill-rule='evenodd' d='M9 1h1v19H1v-1h8Z' clip-rule='evenodd'/%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M10 0h1v21H0v-1h10Z' clip-rule='evenodd'/%3E%3C/svg%3E");transform:translateY(0)}.is-vertical{display:inline-block;height:150px;transform:translateY(50%);width:4px}.is-vertical>input[type=range]{height:4px;margin:0 16px 0 10px;transform:rotate(270deg) translateX(calc(-50% + 8px));transform-origin:left;width:150px}.is-vertical>input[type=range]::-webkit-slider-runnable-track{border-bottom:1px solid grey;border-left:1px solid grey;border-right:0;box-shadow:-1px 0 0 #fff,-1px 1px 0 #fff,0 1px 0 #fff,1px 0 0 darkgrey,1px -1px 0 darkgrey,0 -1px 0 darkgrey,1px 1px 0 #fff,-1px -1px darkgrey}.is-vertical>input[type=range]::-moz-range-track{border-bottom:1px solid grey;border-left:1px solid grey;border-right:0;box-shadow:-1px 0 0 #fff,-1px 1px 0 #fff,0 1px 0 #fff,1px 0 0 darkgrey,1px -1px 0 darkgrey,0 -1px 0 darkgrey,1px 1px 0 #fff,-1px -1px darkgrey}.is-vertical>input[type=range]::-webkit-slider-thumb{transform:translateY(-8px) scaleX(-1)}.is-vertical>input[type=range]::-moz-range-thumb{transform:translateY(2px) scaleX(-1)}.is-vertical>input[type=range].has-box-indicator::-webkit-slider-thumb{transform:translateY(-10px) scaleX(-1)}.is-vertical>input[type=range].has-box-indicator::-moz-range-thumb{transform:translateY(0) scaleX(-1)}.window{background:silver;box-shadow:inset -1px -1px #0a0a0a,inset 1px 1px #dfdfdf,inset -2px -2px gray,inset 2px 2px #fff;font-size:11px;padding:3px}.window fieldset{margin-bottom:9px}.title-bar{align-items:center;background:linear-gradient(90deg,navy,#1084d0);display:flex;font-size:11px;justify-content:space-between;padding:3px 2px 3px 3px}.title-bar-text{color:#fff;font-weight:700;letter-spacing:0;margin-right:24px}.title-bar-controls{display:flex}.title-bar-controls button{display:block;min-height:14px;min-width:16px;padding:0}.title-bar-controls button:focus{outline:none}.window-body{margin:8px}.window-body pre{margin:-8px}.status-bar{display:flex;gap:1px;margin:0 1px}.status-bar-field{box-shadow:inset -1px -1px #dfdfdf,inset 1px 1px gray;flex-grow:1;margin:0;padding:2px 3px}ul.tree-view{background:#fff;display:block;font-family:"Pixelated MS Sans Serif",Arial;font-size:11px;margin:0;padding:6px}ul.tree-view li{list-style-type:none;margin-top:3px}ul.tree-view a{color:#000;text-decoration:none}ul.tree-view a:focus{background-color:navy;color:#fff}ul.tree-view ul{border-left:1px dotted gray;margin-left:16px;margin-top:3px;padding-left:16px}ul.tree-view ul>li{position:relative}ul.tree-view ul>li:before{border-bottom:1px dotted gray;content:"";display:block;left:-16px;position:absolute;top:6px;width:12px}ul.tree-view ul>li:last-child:after{background:#fff;bottom:0;content:"";display:block;left:-20px;position:absolute;top:7px;width:8px}ul.tree-view ul details>summary:before{margin-left:-22px;position:relative;z-index:1}ul.tree-view details{margin-top:0}ul.tree-view details>summary:before{background-color:#fff;border:1px solid gray;content:"+";display:block;float:left;height:9px;line-height:9px;margin-right:5px;padding-left:1px;text-align:center;width:8px}ul.tree-view details[open] summary{margin-bottom:0}ul.tree-view details[open]>summary:before{content:"-"}fieldset{border-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='5' height='5' fill='gray' viewBox='0 0 5 5'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h5v5H0V2h2v1h1V2H0' clip-rule='evenodd'/%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V1h1v2h2V1H0' clip-rule='evenodd'/%3E%3C/svg%3E") 2;margin:0;padding:10px;padding-block-start:8px}legend{background:silver}.field-row{align-items:center;display:flex}.field-row>*+*{margin-left:6px}[class^=field-row]+[class^=field-row]{margin-top:6px}.field-row-stacked{display:flex;flex-direction:column}.field-row-stacked *+*{margin-top:6px}menu[role=tablist]{display:flex;list-style-type:none;margin:0 0 -2px;padding-left:3px;position:relative;text-indent:0}menu[role=tablist] button{color:#222;display:block;min-width:unset;text-decoration:none;z-index:1}menu[role=tablist] button[aria-selected=true]{background-color:silver;margin-bottom:1px;margin-left:-3px;margin-top:-2px;padding-bottom:2px;position:relative;z-index:8}menu[role=tablist] button:focus{outline:1px dotted #222;outline-offset:-4px}menu[role=tablist].justified button{flex-grow:1;text-align:center}[role=tabpanel]{background:silver;border:none;clear:both;margin-bottom:9px;padding:14px;position:relative;z-index:2}body{color:#222;font-family:Arial;font-size:12px}@media (not(hover)){button:not(:disabled):hover{box-shadow:inset -1px -1px #fff,inset 1px 1px #0a0a0a,inset -2px -2px #dfdfdf,inset 2px 2px gray}}pre{box-shadow:inset -1px -1px #fff,inset 1px 1px gray,inset -2px -2px #dfdfdf,inset 2px 2px #0a0a0a}.window{font-family:"Pixelated MS Sans Serif",Arial;-webkit-font-smoothing:none}.title-bar-controls button[aria-label=Minimize]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='2' fill='none' viewBox='0 0 6 2'%3E%3Cpath fill='%23000' d='M0 0h6v2H0z'/%3E%3C/svg%3E");background-position:bottom 3px left 4px;background-repeat:no-repeat}.title-bar-controls button[aria-label=Maximize]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9' fill='none' viewBox='0 0 9 9'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M9 0H0v9h9V0M8 2H1v6h7z' clip-rule='evenodd'/%3E%3C/svg%3E");background-position:top 2px left 3px;background-repeat:no-repeat}.title-bar-controls button[aria-label=Help]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='9' fill='none' viewBox='0 0 6 9'%3E%3Cpath fill='%23000' d='M0 1h2v2H0zM1 0h4v1H1zM4 1h2v2H4zM3 3h2v1H3zM2 4h2v2H2zM2 7h2v2H2z'/%3E%3C/svg%3E");background-position:top 2px left 5px;background-repeat:no-repeat}.title-bar-controls button[aria-label=Close]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='7' fill='none' viewBox='0 0 8 7'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M0 0h2v1h1v1h2V1h1V0h2v1H7v1H6v1H5v1h1v1h1v1h1v1H6V6H5V5H3v1H2v1H0V6h1V5h1V4h1V3H2V2H1V1H0z' clip-rule='evenodd'/%3E%3C/svg%3E");background-position:top 3px left 4px;background-repeat:no-repeat;margin-left:2px}input[type=checkbox]+label:before{background:#fff;box-shadow:inset -1px -1px #fff,inset 1px 1px gray,inset -2px -2px #dfdfdf,inset 2px 2px #0a0a0a}input[type=checkbox]:checked+label:after{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='7' fill='none' viewBox='0 0 7 7'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M7 0H6v1H5v1H4v1H3v1H2V3H1V2H0v3h1v1h1v1h1V6h1V5h1V4h1V3h1z' clip-rule='evenodd'/%3E%3C/svg%3E");left:-16px;top:3px}input[type=email],input[type=password],input[type=text],select,textarea{box-shadow:inset -1px -1px #fff,inset 1px 1px gray,inset -2px -2px #dfdfdf,inset 2px 2px #0a0a0a}input,label,option,select,textarea{-webkit-font-smoothing:none}menu[role=tablist] button{background:silver;border-bottom:2px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px;box-shadow:inset 1px 1px #fff,inset 2px 2px #dfdfdf,inset -1px 0 #000,inset -2px 0 #7f7f7f;top:3px}menu[role=tablist] button:first-of-type:before{border-left:1px solid #fff;border-right:1px solid #dfdfdf;content:"";display:block;height:2px;left:0;position:absolute;top:100%;width:0;z-index:-1}menu[role=tablist] button[aria-selected=true]{margin-right:-1px;margin-top:-4px;top:2px}menu[role=tablist] button[aria-selected=true]:first-of-type{left:0}menu[role=tablist] button{padding:3px 12px}[role=tabpanel]{box-shadow:inset 1px 1px #fff,inset 2px 2px #dfdfdf,inset -1px -1px #0a0a0a,inset -2px -2px gray}button{font-family:"Pixelated MS Sans Serif",Arial}button,ul.tree-view{-webkit-font-smoothing:none}ul.tree-view{box-shadow:inset -1px -1px #fff,inset 1px 1px gray,inset -2px -2px #dfdfdf,inset 2px 2px #0a0a0a}
/*# sourceMappingURL=98.css.map */:root {
  --text-color: #000;
  --primary-color: #2f2f2f;
  --background-color: #2f2f2f;

  --mid-color: color-mix(in srgb, #f5f5dc, #2f2f2f 50%);
}

/* Hide content until styles are loaded, but keep menu bar visible */
body:not(.styles-loaded) main,
body:not(.styles-loaded) main *,
body:not(.styles-loaded) main h1,
body:not(.styles-loaded) main h2,
body:not(.styles-loaded) main h3,
body:not(.styles-loaded) main h4,
body:not(.styles-loaded) main h5,
body:not(.styles-loaded) main h6,
body:not(.styles-loaded) main p,
body:not(.styles-loaded) main span,
body:not(.styles-loaded) main label,
body:not(.styles-loaded) main li,
body:not(.styles-loaded) main a,
body:not(.styles-loaded) main button,
body:not(.styles-loaded) main input,
body:not(.styles-loaded) main textarea {
  color: transparent !important;
  opacity: 0 !important;
}

.light-mode-switch {
  border-radius: 2em;
  border: 2px solid var(--text-color);
  font-family: "Courier New", Courier, monospace;
}

@media (prefers-color-scheme: light) {
  :root {
    --text-color: #2f2f2f;
    --primary-color: #f5f5dc;
    --background-color: #f5f5dc;
  }
  .light-mode-switch {
    display: none;
  }
}

/* The page's own background from the very first paint — the default
   browser white must never show while the app CSS (which carries the
   real theme values) fades in. */
html {
  background-color: var(--background-color);
}

body,
#root {
  background-color: var(--mid-color);
  margin: 0;
}

.title-bar-controls button[aria-label="Maximize"] {
  margin-left: 2px;
}

:root {
  font-family: "Courier New", Courier, monospace;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  --background-color: #000;
  --border-size: 0.25em;
  --gap-size: 0.5em;
  --menu-bar-height: calc(2em + 4px);
  --trailer-tail: 8px;
  /* Height of the music page's fixed debug bar (fontSize 0.85rem + 2 * 0.5rem
     padding + 2px border, with a little slack for the generatedAt timestamp
     wrapping). Used to reserve space below .page so the bar never overlaps
     the last row of content. */
  --debug-bar-height: 2.5em;
  --clippy-border-flash-color: transparent;
  font-size: 18px;
}
/* Force title bar text to dark gray for readability */
.title-bar-text,
.title-bar-text a {
  color: #eee !important;
  font-size: 1rem;
}

:root.theme-border-flash .window,
:root.theme-border-flash .window *,
:root.theme-border-flash .window *::before,
:root.theme-border-flash .window *::after {
  border-color: var(--clippy-border-flash-color, #feef69) !important;
}

:root.theme-border-flash .window .title-bar,
:root.theme-border-flash .title-bar {
  background: var(--clippy-border-flash-color, #feef69) !important;
  background-image: none !important;
}

:root.theme-border-flash .window .title-bar-text,
:root.theme-border-flash .window .title-bar a,
:root.theme-border-flash .title-bar-text {
  color: #000 !important;
}

:root.theme-box-shadow-flash .window .title-bar {
  border-color: var(--clippy-border-flash-color, #feef69) !important;
  box-shadow: 0 0 15px 4px #feef69 !important;
}

/* --- Lilac easter-egg theme: purple title bars ---------------------------
 * Activated by a permanent cookie set when "lg355" or "lg355@sussex.ac.uk"
 * is entered as the username on the WoW download page. Turns all window
 * title-bar backgrounds purple (lilac).
 */
:root.theme-lilac .window .title-bar,
:root.theme-lilac .title-bar {
  background: #b060b0 !important;
  background-image: linear-gradient(
    to right,
    #9c4f9c,
    #b060b0,
    #9c4f9c
  ) !important;
}

:root.theme-lilac .window .title-bar-text,
:root.theme-lilac .window .title-bar a,
:root.theme-lilac .title-bar-text {
  color: #fff !important;
}

/* --- Experimental theme: raise-and-wobble effect ----------------------------
 * Sections marked with the "experimental" theme bob up and down gently on
 * hover/focus, and gain a thick double border (2px solid) layered with the
 * 98.css raised-window bevel. The 3D perspective is provided by .page.
 * Driven by the `raiseWobble` flag on the theme definition.
 */
@keyframes theme-experimental-wobble {
  0% {
    transform: translateY(0);
  }
  20% {
    transform: translateY(-6px);
  }
  40% {
    transform: translateY(-3px);
  }
  60% {
    transform: translateY(-6px);
  }
  80% {
    transform: translateY(-2px);
  }
  100% {
    transform: translateY(0);
  }
}

/* Nothing at the window level: experimental sections keep their plain
   sizing/backgrounds — wobbling the frame itself used to clobber the
   lifted window's background and position. */

/* The one-pulse entry glow lives on the TITLE TEXT instead: maximized
   experimental windows flash their heading #FEEF69 exactly once on
   entry into the fullscreen view (highlighting what is being focused). */
.window.maximized[data-theme~="experimental"] .title-bar-text {
  animation: title-flash-feef69 850ms ease-in-out;
}

@keyframes title-flash-feef69 {
  0%,
  100% {
    color: inherit;
  }
  40%,
  75% {
    color: #feef69;
  }
}

/* The WOBBLE identifies an experimental window, ONCE per cursor
   visit. The trigger keys off data-wobble-playing (set by the FIRST
   pointer contact with anywhere on the window - title bar, controls,
   frame, body - in Window.tsx), NOT :hover: a hover-keyed rule
   unmatches the moment the pointer leaves, and the browser cancels an
   animation whose rule stopped matching mid-flight. The marker keeps
   the rule matched for the whole run - the bob ALWAYS finishes, an
   accidental swipe over the element never snips it short, and it never
   restarts. data-wobbled (the finished marker, armed on the root's
   animationend or on outside pointer contact) saturates the
   once-per-CURSOR-VISIT state: the Window.tsx root handler clears it
   on every fresh re-entry, so the wobble replays after the mouse has
   left the window and re-touches it - and never restarts while the
   cursor roams inside (no new pointerenter fires on the root, and a
   play-in-progress is guarded too).

   The bob targets the WINDOW ROOT - frame, background, title bar, body
   and the OK buttons together, one paint. NO paired counter-animation
   on the title bar: two synchronized animations snap out of phase the
   moment one recalculates late, and the title then bounces SEPARATE
   from the buttons - the paired design is what made the ok buttons
   bounce apart.

   Maximised DOM stays flat on every path: the :not(.maximized) root
   guard, and the suppression below for inline experimental children
   under a maximized ancestor (the deepest-playing rule keeps experimental
   ancestors from double-bobbing behind a playing experimental child). */
.window[data-theme~="experimental"]:not(.maximized)[data-wobble-playing] {
  animation: theme-experimental-wobble 0.6s ease-in-out;
}

/* Only the DEEPEST playing window bobs: an experimental ancestor whose
   subtree holds a playing child stays still (its own run never starts;
   the stale-suppression cleanup in Window.tsx tidies the marker). */
.window[data-theme~="experimental"]:not(.maximized)[data-wobble-playing]:has(
    .window[data-wobble-playing]
  ) {
  animation: none;
}

/* Maximised stability: inline experimental children under a
   .window.maximized ancestor never bob in the fullscreen view
   (Maximizing force-expands the whole subtree; an animated child used
   to break the overlay's scroll). An animation: none on a claimed run
   raises animationcancel - the Window.tsx listener clears the marker
   so the overlay's windows do not stay saturated. */
.window.maximized .window[data-theme~="experimental"][data-wobble-playing] {
  animation: none;
}

/* Respect reduced-motion ONLY for the maximized rail's entry glow. The
   wobble itself plays regardless (the slide theme's doctrine here:
   b82cda8 - this environment reports reduced-motion while the user
   actively wants the site's own motion). */
@media (prefers-reduced-motion: reduce) {
  .window.maximized[data-theme~="experimental"] .title-bar-text {
    animation: none;
  }
}

/* Touch devices (Android Firefox, etc.) don't expose a real hover pointer.
   Strip the raise/wobble transitions there so the sticky-hover state can't
   leave a window stuck mid-hover when the user scrolls past it. */
@media (hover: none) {
  .window[data-theme="experimental"] {
    transition: none;
    will-change: auto;
  }
}

/** The "centered" theme centers child windows with space-around distribution. */
.window[data-theme~="centered"] .window-body ul li {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-wrap: wrap;
}

@font-face {
  font-family: "bahnscrift";
  src: url("/BAHNSCHRIFT.TTF");
  font-weight: normal;
  font-style: normal;
}

/* Reset XP.css window styling overrides */
.window {
  min-width: min(480px, 100%);
  height: max-content;
  box-sizing: border-box;
  /* Leave room for the fixed menu bar when scrolling to a window. */
  scroll-margin-top: var(--menu-bar-height);
}

.window:last-child {
  margin-bottom: 0;
}

/* Add extra spacing for nested windows */
.window-body .window {
  margin-bottom: var(--trailer-tail);
}

.window-body .window:last-child {
  margin-bottom: 0;
}

/* Spacing for windows inside lists (addon pages) */
.window-body ul li .window,
.window-body ul .window,
.window-body .addon-wrapper {
  margin-bottom: 0.5em;
}

.window-body ul li:last-child .window,
.window-body ul .window:last-child,
.window-body .addon-wrapper:last-child {
  margin-bottom: 0;
}

/* Hide border on first-level windows to avoid double border */
.page > .window,
.page > .maximized-shell > .window,
main > .page {
  border: none;
}

/* Pin all root-level windows to the same width so a window with wider
   inner content (e.g. a Spotify iframe) can't push itself past the rest
   of the page. Defaults to 768px to match the website's compact reading
   width; only grows when the viewport is narrower than that. */
.page > .window,
.page > .maximized-shell > .window {
  width: min(768px, 100% - (var(--gap-size) * 2));
  max-width: min(768px, 100% - (var(--gap-size) * 2));
}
/* Add more vertical padding to title bars */
.title-bar {
  padding-top: 0.25em !important;
  padding-bottom: 0.25em !important;
}

body {
  margin-top: 0;
  min-height: 100vh;
  min-height: 100dvh;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}

#root {
  display: flex;
  flex: 1;
  flex-direction: column;
  place-items: stretch;
  min-height: 0;
}

body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
span,
label,
input,
textarea,
button,
form,
div,
a,
li {
  color: var(--text-color) !important;
  opacity: 1 !important;
}

.page > ul,
li {
  padding: 0;
  margin: 0;
  list-style: none;
  margin-bottom: var(--gap-size);
}

.page {
  margin: 0 auto;
  display: flex;
  border: 0;
  z-index: 100;
  width: calc(100% - (var(--gap-size) * 2));
  height: 100%;
  box-sizing: border-box;
  flex-direction: column;
  place-items: center;
  min-height: 100%;
  /* NO perspective / transform-style here: the themed raise/wobble
     transforms are plain 2D translateY, and any 3D viewing space would
     turn .page into the containing block for fixed-position descendants
     (maximized windows ride in-tree over the page). */
}

.window.maximized {
  min-height: 100%;
  min-width: 100%;
  margin: 2em;
  box-sizing: content-box;
}

.menu-item {
  margin: 0;
  padding: calc(var(--gap-size) / 2) var(--gap-size);
  background-color: beige;
}

ul > section > h1,
ul > section > h4,
form#form > label {
  margin: 0 calc(var(--gap-size) * 1.75);
  font-family: bahnscrift, system-ui, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  font-size: 0.9rem;
}

.window-body ul {
  margin: 0;
  padding-left: 0;
}

.window-body ul li {
  /* Symmetric gutters: content inside an li lines up on both sides. */
  padding-left: 1.5em;
  padding-right: 1.5em;
}

.window-body {
  min-width: 0;
  max-width: 100%;
  word-break: break-word;
}

.maximized-window-body {
  display: flex;
  flex-direction: column;
}

.maximized-window-body > *:last-child {
  /* The maximize layout column (Body's only rendering) grows to fill the
     window body and hands its height down to the iframe chain. */
  flex: 1;
  min-height: 0;
}

/* Embedded iframes (SoundCloud/Spotify/blogs) grow to fill the available
   window-body height instead of staying at their fixed pixel height.
   Stretches the ancestor chain (ul > li > wrapper) so the flex height
   propagates down to the iframe. Only applies inside `.maximized` so
   inline (non-fullscreen) windows keep their natural sizes. */
.maximized .maximized-window-body:has(iframe) {
  /* allow the body itself to hand height down to its children */
  align-items: stretch;
}

/* The content column (inside the maximize layout) grows to fill the window
   body, and hands its height down to the iframe chain. */
.maximized .maximized-window-body:has(iframe) .maximized-content-col,
.maximized .maximized-window-body:has(iframe) ul,
.maximized .maximized-window-body:has(iframe) ul > li {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.maximized .maximized-window-body iframe {
  /* Fill the available height via flex (NOT height:100%, which resolves to 0
     against an indefinite flex parent). flex-basis auto + grow stretches the
     iframe to its share of the parent's resolved height. */
  flex: 1 1 auto;
  min-height: 200px;
  max-height: 100%;
  box-sizing: border-box;
}

/* In a maximized "centered" window the content hugs the bottom left corner
   and grows upward and rightward as the space allows. */
.window.maximized[data-theme~="centered"] > .maximized-window-body > ul {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.window.maximized[data-theme~="centered"] > .maximized-window-body > ul > li {
  justify-content: flex-start;
}

.resource-embed-scroll {
  width: 100%;
  max-width: 100%;
  height: 100%;
}

.resource-embed-scroll,
.resource-embed-scroll * {
  font-family: "Fira Code", "Courier New", Courier, monospace !important;
}

.resource-embed-scroll pre,
.resource-embed-scroll code {
  max-width: 100%;
  box-sizing: border-box;
}

.resource-embed-scroll pre,
.window-body pre {
  /* Own vertical rhythm: 98.css gives pre no margins, so spacing
   * depends on the NEXT element keeping browser defaults — a p gets
   * its 1em top margin, but this design system zeroes ul margins
   * (.page > ul / .window-body ul { margin: 0 }), which pulls a list
   * hard against the code block. The gap belongs to pre. */
  margin: var(--gap-size) 0;
  /* Code soft-wraps, but NOT the min-content-destroying way:
   * pre-wrap + overflow-wrap:anywhere on a pre inside a flex column
   * reports the box's min-content width as ~one character, so the
   * window can shrink below the code's natural width and every
   * line's last word dangles onto its own line. overflow-wrap:
   * break-word keeps the full natural width as the minimum (the
   * window is sized to the code's longest line) and only breaks a
   * word when the code is genuinely wider than the window. */
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-word;
  max-width: 100%;
  box-sizing: border-box;
}

/* Inline code (markdown backticks): 98.css gives code only a font, so the
 * glyphs sit flush against the neighbouring words — zero gap on either
 * side and no visual separation from the running text. Render it as a
 * terminal-style chip — black background, green bold text, mono — to
 * match the site's terminal aesthetic; `:not(pre) >` keeps fenced blocks
 * (pre > code) untouched, and em units track the smaller window-body
 * text size. */
:not(pre) > code {
  margin: 0 0.15em;
  padding: 0.1em 0.3em;
  background-color: #e8d8a0;
  color: #000;
  font-weight: 700;
}

/* 98.css pins window text to its Win98 chrome size: .window{font-size:11px}.
   Content windows read like documents — body text follows the root size,
   the same 1rem the pre blocks above (98.css) already render at. */
.window-body {
  font-size: 0.78rem;
}

/* xp.css sets very large heading sizes; keep markdown headings readable in content windows. */
.window-body h1 {
  font-size: 1.2rem !important;
  line-height: 1.25;
  margin: 0.25em 0;
}

.window-body h2 {
  text-align: right;
  color: var(--button-shadow) !important;
}

.window-body > ul:first-child {
  margin-top: 0;
}

.music-track-title {
  font-size: 1.08em;
  font-weight: 600;
  margin: 0 0 0.35em 0;
}

.music-track-window .window-body {
  padding: 0.5em;
}

.music-track-window ul {
  min-width: 100%;
}
/* .music-track-window li, */
.music-track-window p {
  width: max-content;
}
.music-track-window iframe {
  border-radius: 4px;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}

/* Favourite-link windows mirror the per-track styling so their Spotify
   iframe doesn't push the window wider than the rest of the page. */
.music-favourite-link-window .window-body {
  padding: 0.5em;
}

/* Desktop keeps the liked fixed-height card; mobile squares the player
   at the player's own footprint size, so square can never balloon tall
   (spotify favourite-link players included via .embed). */
@media (max-width: 768px) {
  .embed,
  [data-testid="embed-iframe"] {
    display: block;
    width: min(100%, 250px);
    aspect-ratio: 1 / 1;
    height: auto;
    margin: 0 auto;
  }
}

/* YouTube players are 16:9 — size them from their width instead of a
   fixed pixel height (or the mobile square above, which letterboxes
   video). Declared after the media block so it wins the cascade. */
.embed-youtube {
  aspect-ratio: 16 / 9;
  height: auto;
}

.music-favourite-link-window ul {
  min-width: 100%;
}

.music-favourite-link-window p {
  width: max-content;
}

.music-favourite-link-window iframe {
  display: block;
  border-radius: 4px;
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
  max-width: 100%;
}

/* Artist Profile Picture - 1:1 aspect ratio background image */
.artist-profile-picture {
  width: 80px;
  height: 80px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 4px;
  margin-right: 0.5em;
  vertical-align: middle;
}

/* Artist Profile Background - stretched background under text */
.artist-profile-background {
  width: 100%;
  min-height: 36em;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 4px;
  margin-bottom: 0.5em;
}

/* Menu Bar - Windows XP style */

.menu-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  background: linear-gradient(to bottom, #f0f0f0 0%, #e0e0e0 100%);
  border-bottom: 1px solid #808080;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  padding: 0;
  margin: 0;
}

.menu-bar menu {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: 0;
}

.menu-bar menu li {
  margin: 0;
  padding: 0;
}

.menu-bar menu a {
  display: block;
  padding: 4px 8px;
  text-decoration: none;
  color: #000 !important;
  font-size: 18px;
  font-family: "Tahoma", "MS Sans Serif", sans-serif;
  white-space: nowrap;
  transition: background-color 0.1s ease;
}

.menu-bar menu a.clippy-drop-active {
  background-color: #feef69;
  outline: 2px dashed #808080;
}

@media (hover: hover) {
  .menu-bar menu a:hover {
    background-color: rgba(51, 153, 255, 0.2);
  }
}

.menu-bar menu a:active {
  background-color: rgba(51, 153, 255, 0.4);
}

.menu-bar .menu-underline {
  text-decoration: underline;
}

/* Minimized Sections Menu */

body main {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  min-height: 100%;
}

/* Fixed bars (menu bar, debug bar, unhide chinbar) overlay the viewport.
   Reserve space on the body itself via padding so every flow descendant —
   not just main — starts below them. */
body {
  padding-top: calc(var(--menu-bar-height) + var(--trailer-tail));
  padding-bottom: var(--trailer-tail);
}

body.feef69-page {
  background: #feef69 !important;
  margin: 0 !important;
  padding: 0 !important;
  min-height: 100vh !important;
  height: 100vh !important;
  display: block !important;
}

/* Account for the fixed debug bar on the music page (sits below menu bar) */
body:has([data-debug-bar]) .page {
  padding-bottom: calc(var(--debug-bar-height) + var(--trailer-tail));
}

/* Leave room for the fixed unhide chinbar on desktop */
@media (prefers-color-scheme: light) {
  :root {
    --background-color: #fefefe;
  }
}

@media screen and (max-width: 768px) {
  :root {
    --gap-size: 1.26em;
    --debug-bar-height: 3.25em;
  }
  body:has([data-debug-bar]) .page {
    padding-bottom: calc(var(--debug-bar-height) + var(--trailer-tail));
  }
  .page {
    width: 100%;
  }

  body {
    max-width: 100vw;
    /* The menu bar is sticky/in-flow on mobile — only the trailer tail needs
       reserving; the bar's own height is occupied by normal document flow. */
    padding-top: 0;
  }

  main {
    width: 100%;
    max-width: 100vw;
    box-sizing: border-box;
    padding: 0;
    margin: 0;
  }

  .page {
    margin: 0 auto;
    padding: 0;
    box-sizing: border-box;
    justify-items: center;
  }

  /* Constrain window width on mobile */
  .window {
    max-width: 100%;
    box-sizing: border-box;
  }

  .window-body {
    max-width: 100%;
  }

  /* Mobile menu bar: sits in the flow (sticky) so its height grows with
     wrapped rows instead of reserving a fixed one-line height. */
  .menu-bar {
    position: sticky;
    top: 0;
    overflow-x: visible;
  }

  /* Wrap to the next line when the items overflow — no horizontal scroll. */
  .menu-bar menu {
    flex-wrap: wrap;
    justify-content: flex-start;
  }

  .menu-bar menu a {
    padding: 6px 12px;
    font-size: 16px;
    white-space: nowrap;
  }

  /* Increase touch target size for better mobile usability */
  .menu-bar menu li {
    display: flex;
    align-items: center;
  }

  /* Permalink scrolling: the wrapped menu bar can reach ~2 rows on mobile,
     so leave a bit more headroom than the desktop one-line bar needs. */
  .window {
    scroll-margin-top: calc(2 * var(--menu-bar-height) + 1em);
  }

  /* Fullscreened windows fill the viewport edge-to-edge: no side margins,
     no window-body padding — exactly 100vw x 100dvh. The body prefix
     outranks the desktop maximized rules in extras.css, which load later
     with equal specificity. dvh (with vh fallback) tracks the *visible*
     viewport, so the Android Firefox/Chrome navigation bar cannot hide the
     window's top edge underneath it. */
  body .window.maximized {
    margin: 0;
    width: 100vw;
    max-width: 100vw;
    /* Content-hugging like desktop, NO viewport-height clamp: taller
       content extends the window and the dimmed shell scrolls it as one
       piece (the artist background survives the scroll, the rail lands
       at the true geometry end). */
    min-width: 0;
    min-height: 0;
    scrollbar-width: none;
  }
  body .window.maximized::-webkit-scrollbar,
  body .window.maximized > .window-body::-webkit-scrollbar,
  body .window.maximized > .maximized-window-body::-webkit-scrollbar {
    display: none;
  }
  body .window.maximized > .window-body,
  body .window.maximized > .maximized-window-body {
    scrollbar-width: none;
  }

  body .window.maximized > .window-body,
  body .window.maximized > .maximized-window-body {
    margin: 0;
    padding: 0;
    scrollbar-width: none;
  }

  /* Leaf windows (no nested sections) keep their natural size on maximize
     instead of ballooning to the viewport; auto margins center them
     horizontally, docked to the top edge. */
  body .window.maximized.content-size {
    margin: 0 auto;
    width: min(100vw, 1100px);
    max-width: 100vw;
    min-height: 0;
  }

  /* Give the unhide button a visible chinbar on mobile so it isn't
     clobbered by on-screen content. */
  .minimized-menu-container {
    left: 0;
    bottom: 0;
    right: 0;
    padding: 2px 20px;
    align-items: stretch;
  }

  .minimized-menu-button {
    top: var(--menu-bar-height);
    font-size: 16px !important;
    text-align: center;
    z-index: 98;
  }

  .minimized-menu-container .dropdown-menu {
    bottom: 100%;
    left: 0;
    right: 0;
    min-width: unset;
    width: 100%;
  }
}

@media screen and (max-width: 480px) {
  :root {
    --menu-bar-height: 24px;
  }
  .page > .window,
  .page > .maximized-shell > .window {
    box-sizing: border-box;
    margin: 0 auto;
    /* width: fit-content; */
    width: 100%;
  }

  /* Extra small screens - even more compact */
  .menu-bar menu a {
    padding: 5px 10px;
  }
}

.Toastify__toast-container {
  z-index: 9999 !important;
}

.Toastify__toast {
  max-width: 100%;
  width: 100%;
}
.Toastify__close-button {
  place-self: center;
  bottom: 6px;
  right: 6px;
}
.ok-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  /* The OK button wraps below the content (margin-left: auto line) but the
     row is full-width (the display:contents maximize-shell leaves the
     window as a direct flex item of .page), so wrapped flex lines would
     stack flush against the last window above. Row-gap = the window-body
     padding gives the buttons the same vertical separation as windows. */
  row-gap: 8px;
}

.ok-row > button {
  margin-left: auto;
}

/* Buttons get the window padding above them via .ok-row's row-gap
   (above); no page-specific spacing needed. */
.ok-row-bottom {
  align-items: flex-end;
}

.maximized-backdrop {
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: rgba(0, 0, 0, 0.6);
  /* The dimmed shell is the scroll surface: a maximized window taller
     than the viewport scrolls as a whole (title bar and all), keeping
     one consistent piece of geometry for content, banners and rail. */
  overflow-y: auto;
  display: flex;
  /* SAFE centering: a fitting window centers in the scrollport; an
     overflowing one docks to the top edge instead (auto margins misbehave
     on an overflowing scroll surface — bottom-anchoring the window). */
  align-items: safe center;
  justify-content: safe center;
}

/* Every window is wrapped by the shell: display: contents keeps the
   window's own flow box while inline; the SAME wrapper becomes the
   fixed darkened backdrop when the window lifts — the window element
   stays inside the background element in every state. */
.maximized-shell {
  display: contents;
}

.maximized-backdrop:not(.page-slide-backdrop-ride):not(
    .page-slide-backdrop-close
  ) {
  /* The darkening fades rather than blinking in when the window lifts
     over the page (its removal always rides the fade-out clone,
     whose own mount animation must not be overridden by this one). */
  animation: page-slide-backdrop-in 180ms ease-out;
}

@keyframes page-slide-backdrop-in {
  from {
    opacity: 0;
    background: rgba(0, 0, 0, 0);
  }
}

/* The maximized window flows INSIDE its shell (the backdrop): hugging
   content — no viewport clamp, no auto margins (they misalign on the
   overflowing scroll surface). Alignment is the shell's safe centering.
   Taller content extends the window; the dimmed shell scrolls it as one
   piece (banner, posts and rail keep their DOM order and geometry). */
.window.maximized {
  width: max-content;
  max-width: 96vw;
  min-width: min(800px, 96vw);
  height: fit-content;
  min-height: 0;
  max-height: none;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
}

.window-minimized-body {
  padding: 0.5em;
}

.window-minimized-body .ok-row {
  margin: 0;
}

.artist-background {
  display: block;
  padding: 1.5em 1em;
  color: #fff;
  background-size: cover;
  background-position: center;
  min-height: 120px;
}

.debug-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 999;
  padding: 0.5rem 1rem;
  font-family: "Fira Code", monospace;
  font-size: 0.85rem;
  background: #fff;
  border-top: 1px solid #808080;
  color: #2f2f2f;
  /* The bar rides out with its page (a page-slide-bar clone drops it);
     each arrival — the music page remounts it on every navigation —
     slides it back in from the bottom edge. */
  animation: debug-bar-enter 240ms ease-out backwards;
}

@keyframes debug-bar-enter {
  from {
    opacity: 0;
    transform: translateY(100%);
  }
}

.debug-bar strong {
  font-family: inherit;
}

.music-as-of-uploading .window-body li strong {
  display: block;
  margin: 0.75em 0;
  text-align: center;
}

.music-as-of-uploading .window-body li p {
  margin-bottom: 0;
}

/* Maximized window column: content grows from the top; the OK + Permalink
   rail comes after it in normal flow. margin-top: auto pushes the rail to
   the bottom edge only when the window is taller than its content, so
   content never scrolls underneath the buttons. */
.maximized-content-col {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.maximized-content-col > .ok-row {
  flex: 0 0 auto;
  margin-top: auto;
  justify-content: flex-end;
  gap: 0.5em;
  /* Same separation from the content as the window body's own padding. */
  padding-top: 8px;
}

/* Both rail buttons cluster on the right; no auto-margin split between
   them (the base .ok-row > button rule is superseded here). */
.maximized-content-col > .ok-row > button {
  margin-left: 0;
}

/* Leaf windows (no nested section nodes) size purely to their inner content
   on maximize — no 800px floor below. */
.window.maximized.content-size {
  min-width: 0;
}

.window.maximized > .window-body {
  /* Drop the 8px XP window-body margin so the scrollbar hugs the window
     frame; the inset moves to padding, which scrollbars do not overlap. */
  margin: 0;
  padding: 8px;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}

body:has(.maximized-backdrop) {
  overflow: hidden;
}
/* The maximized window is an in-tree fixed child of .page; the z-index
   raise lets the dimmed shell cover the menu bar like the old body-level
   portal did. (.page itself carries no perspective/transform-style — see
   window.css — so fixed descendants stay viewport-anchored.) */
body:has(.maximized-backdrop) .page {
  z-index: 3000;
}
body:has(.maximized-backdrop) .debug-bar {
  display: none;
}

.addon-status {
  font-weight: bold;
}

.form-feedback {
  margin: 0.5em 0;
}

.resume-window {
  max-width: 768px;
  margin: 0 auto;
}

.resume-frame {
  width: 100%;
  height: 70vh;
  border: 1px inset #808080;
  background: #fff;
}

.resume-controls {
  display: flex;
  gap: 1em;
  align-items: center;
  margin-bottom: 0.5em;
}

.title-bar-controls button[aria-label="Restore"] {
  /* Match the Maximize button's gap (the 98 theme only gaps Close, so the
     maximized triplet Minimize/Restore/Close would lose the Minimize to
     Restore spacing). */
  margin-left: 2px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='8' height='9' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23000' d='M2 0h6v6H7V5H7V1H2zM0 3h6v6H0zm1 2v3h4V5z' fill-rule='evenodd'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: top 2px left 3px;
}

.clippy {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 10000;
  display: flex;
  /* Speech bubble sits BESIDE the avatar, bottom-aligned. */
  flex-direction: row;
  align-items: flex-end;
  gap: 0.4rem;
  /* No persistent animation here: base animations restart whenever the
     class list changes, which made every reposition dip and glide. The
     arrival bob is an explicit .clippy-arrive class instead. */
}

.clippy-image {
  width: 120px;
  max-width: 28vw;
  height: auto;
  cursor: pointer;
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.4));
  /* Pointer dragging needs raw touch events; page scrolling must not steal. */
  touch-action: none;
  user-select: none;
  -webkit-user-drag: none;
}

/* Corner anchoring: br is the default; the other three mirror it. */
.clippy[data-corner="br"] {
  left: auto;
  top: auto;
  right: 1rem;
  bottom: 1rem;
}

.clippy[data-corner="bl"] {
  left: 1rem;
  top: auto;
  right: auto;
  bottom: 1rem;
  /* Avatar hugs the left edge; the bubble lands on its right side. */
  flex-direction: row-reverse;
}

.clippy[data-corner="tl"] {
  left: 1rem;
  top: 1rem;
  right: auto;
  bottom: auto;
  flex-direction: row-reverse;
  align-items: flex-start;
}

.clippy[data-corner="tr"] {
  left: auto;
  top: 1rem;
  right: 1rem;
  bottom: auto;
  align-items: flex-start;
}

/* Bubbles beside the avatar point at it horizontally. */
.clippy[data-corner="br"] .clippy-bubble::after,
.clippy[data-corner="tr"] .clippy-bubble::after {
  top: 50%;
  bottom: auto;
  left: auto;
  right: -8px;
  translate: 0 -50%;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-left: 8px solid #aaa;
  border-right: none;
}

.clippy[data-corner="bl"] .clippy-bubble::after,
.clippy[data-corner="tl"] .clippy-bubble::after {
  top: 50%;
  bottom: auto;
  right: auto;
  left: -8px;
  translate: 0 -50%;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-right: 8px solid #aaa;
  border-left: none;
}

/* Reposition bob: Clippy rises into place like in its arrive animation
   (fade + upward glide). Applied by the component during repositioning
   only, so clicks can't re-trigger it. */
.clippy-arrive {
  animation: clippy-bob 600ms ease-out;
}

@keyframes clippy-bob {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
}

.clippy-bubble {
  position: relative;
  max-width: 180px;
  padding: 0.5rem 0.75rem;
  background: #fffde7;
  border: 2px solid #aaa;
  border-radius: 8px;
  box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25);
  font-size: 0.8rem;
  line-height: 1.4;
}

.clippy-bubble::after {
  content: "";
  position: absolute;
  right: 20px;
  bottom: -8px;
  border-top: 8px solid #aaa;
  border-right: 8px solid transparent;
  border-left: 8px solid transparent;
}

body.feef69-page {
  min-height: 100vh;
  height: 100vh;
  margin: 0;
  padding: 0;
  background: #feef69 !important;
}

main.feef69-page {
  height: 100vh;
  background: #feef69;
}

/* The unhide bubble: shell keeps the .clippy-bubble chrome (incl. its
   outside tail*); the list scrolls inside it. The cap lives on the shell
   so long headings can't push the mascot (or itself) off screen —
   row-reverse corners extend the row to the right, into the viewport edge. */
.clippy-unhide-bubble {
  box-sizing: border-box;
  width: min(12rem, 45vw);
}

.clippy-unhide {
  list-style: none;
  margin: 0;
  padding: 6px;
  max-height: 60vh;
  overflow-y: auto;
}

/* Defensive: the inner list owns no chrome, so it must carry no tail. */
.clippy-unhide::after {
  display: none;
}

.clippy-unhide button {
  display: block;
  width: 100%;
  margin: 2px 0;
  word-break: break-word;
}

/* Inline image sizes in content must not exceed the window body. */
.window-body img {
  max-width: 100% !important;
  object-fit: contain;
}

/* Same for inline iframe embeds in content (markdown YouTube links carry
   their own responsive style; this also caps hand-written <iframe>s in raw
   HTML). Music-page players are sized by their own rules and excluded. */
.window-body iframe:not(.embed) {
  max-width: 100%;
}

/* Content tables: GFM pipe tables via remark-gfm (added to the Md
   pipeline) — content files stay plain markdown. td/th are missing from
   window.css's color list — color inherits fine, but UA styles leave the
   cells plain (and th centered): give them terminal-style rules and
   even, padded cells. */
.window-body table {
  border-collapse: collapse;
  width: 100%;
  margin: var(--gap-size) 0;
  font-size: inherit;
}

.window-body th,
.window-body td {
  border: 1px solid var(--text-color);
  padding: 0.25em 0.5em;
  text-align: left;
  vertical-align: top;
}

/* highlight.js token colors for rehype-highlight output (src/lib/markdown.tsx).
 * Hand-rolled instead of a stock theme so the palette sits on the 98.css/xp.css
 * light window backgrounds — pre/code render on the window body, not a dark panel.
 * Only the classes rehype-highlight emits are styled; untagged text keeps the
 * inherited body color.
 *
 * Every color is !important: window.css enforces `span { color:
 * var(--text-color) !important }` site-wide, and at equal importance the
 * higher-specificity token selectors here win the cascade. */

.hljs-comment,
.hljs-quote {
  color: #007b00 !important;
  font-style: italic;
}

.hljs-keyword,
.hljs-selector-tag,
.hljs-doctag,
.hljs-meta .hljs-keyword,
.hljs-template-tag {
  color: #a31515 !important;
}

.hljs-string,
.hljs-regexp,
.hljs-meta .hljs-string,
.hljs-addition {
  color: #0451a5 !important;
}

.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-attr,
.hljs-attribute {
  color: #b26700 !important;
}

.hljs-title,
.hljs-title.function_,
.hljs-section,
.hljs-name {
  color: #795e26 !important;
}

.hljs-type,
.hljs-title.class_,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-class,
.hljs-selector-id {
  color: #267f99 !important;
}

.hljs-meta,
.hljs-symbol,
.hljs-bullet,
.hljs-link {
  color: #6f5ea8 !important;
}

.hljs-deletion {
  color: #a31515 !important;
}

.hljs-emphasis {
  font-style: italic;
}

.hljs-strong {
  font-weight: bold;
}

/* shell-session ("console"/"shell") blocks: the prompt marker arrives as
 * `hljs-meta prompt_`, and each session's input lines (prompt path +
 * command) arrive as nested `bash` spans; command output stays plain.
 * Prompts are muted purple, terminal input is green — so typing reads
 * differently from the ls/cat output between commands. */
.hljs-meta.prompt_,
.hljs-meta .hljs-prompt_ {
  color: #6f5ea8 !important;
  user-select: none;
}

.language-console .bash,
.language-shellsession .bash,
.language-shell .bash {
  color: #007b00 !important;
}
/*
 * Inert until lightdni-jssas-toggle adds `theme-page-slide` to <html> and
 * sets --ps-* variables and data-page-slide on it.
 *
 * Exit choreography: the page drops straight out of the bottom while the
 * windows fold - the top-level windows squash down onto the taskbar line
 * (XP-style outline squash, linear over --ps-minimize) IN STEP with the
 * whole-page drop over --ps-drop-dur: squeeze while moving down, fully
 * off screen, no stationary beat first.
 */
html.theme-page-slide {
  overflow-x: clip;
}

/* No fill-forward: a lingering `translate` would trap fixed descendants.
   `backwards` fill parks the incoming page at its off-viewport start. */
html.theme-page-slide[data-page-slide] main:not(.page-slide-ghost) {
  animation: page-slide-in var(--ps-duration) ease-in-out backwards;
}

html.theme-page-slide .page-slide-ghost {
  position: fixed;
  z-index: 1;
  margin: 0;
  overflow: hidden;
  pointer-events: none;
  /* Two beats: the rapid fold snaps first (--ps-drop-delay == the fold
     beat), THEN the collapsed page glides off gently - ease in and out,
     not a slam. */
  animation: page-slide-drop var(--ps-drop-dur) ease-in-out
    var(--ps-drop-delay) forwards;
}

/* The live <main> has an indefinite height, so its page sizes to content
   (a nested .page in the ghost's fixed-height main, or main itself - the
   normalized shell makes main the page); the ghost must not stretch the
   page to fill its box. */
html.theme-page-slide .page-slide-ghost > .page,
html.theme-page-slide .page-slide-ghost.page {
  height: auto;
  min-height: 0;
}

/* Fixed footer bars of the old page exit on their own beat: the slide
   starts immediately (no fold-delay) and fades as it goes, so the bar
   visibly rides out instead of blinking away at the route commit. */
html.theme-page-slide .page-slide-bar {
  pointer-events: none;
  animation: page-slide-bar-exit var(--ps-drop-dur, 280ms) ease-in-out
    var(--ps-drop-delay, 0ms) forwards;
}

@keyframes page-slide-bar-exit {
  to {
    translate: 0 100vh;
    opacity: 0;
  }
}

/* The live bar yields to its ride-out clone for the exit (visibility -
   display would collapse the reserved body padding row). */
html.theme-page-slide .page-slide-bar-hide {
  visibility: hidden;
}

/* The new page eases from the old page's bottom padding to its own, so its
   window moves in step with the footer leaving. (JS sequencing in
   themes/pageSlide applies the theme only after the exit, so this starts
   right as the destination page begins its slide-in.) */
html.theme-page-slide[data-page-slide] main:not(.page-slide-ghost) > .page {
  animation: page-slide-pad var(--ps-duration) ease-in-out backwards;
}

/* XP minimise: EVERY window in the ghost squashes IN PLACE - top-level
   or not. The old selectors matched only direct children of the page,
   and any window wrapped one node deeper (or nested inside another
   window) escaped the fold entirely: it rode the page drop at full
   height beside fully-squashing siblings - the child read as falling
   slower than the main window, the whole exit as the window
   disassembling. One rule, every window. No translation: the window
   slides out FROM ITS CURRENT POSITION, carried by the whole-page drop
   (the old fold translated each window down to the taskbar line, which
   read as a teleport below the arriving page's windows). Contents ride
   the squash, as in XP itself.

   Timing split: the BASE fold is LINEAR - nested children ride flat, so
   the parent's acceleration does not compound through them - while the
   MAIN windows (the top-level rule below) carry the gravity: their
   squash is the one EASE-IN beat, accelerating shut. The stagger comes
   from --ps-fold-delay, set per main window in JS and INHERITED: a
   nested window folds in its parent's stage, not against it.
   !important holds the base fold against the window stylesheets inside
   the inert ghost - the exit stays exactly one motion. */
html.theme-page-slide .page-slide-ghost .window {
  transform-origin: bottom center;
  animation: page-slide-fold var(--ps-minimize, 98ms) linear forwards !important;
  animation-delay: var(--ps-fold-delay, 0ms);
}

/* MULTI-window pages exit FLAT: the ghost runs NO window animations -
   not even the base fold. Every differential beat the cascade family
   tried (staggered, simultaneous, raised-over) tore at the exit -
   revealed sections collapsed at their own pace against the page's
   slide-out. The whole page slides out as one piece instead: no
   per-window motion whatsoever, only the page's drop. Single-window
   pages keep the shrink (the base + gravity rules above). */
html.theme-page-slide .page-slide-ghost.page-slide-ghost-flat .window,
html.theme-page-slide
  .page-slide-ghost.page-slide-ghost-flat
  .window
  .window {
  animation: none !important;
  transform: none !important;
}

/* MAIN windows: the top-level folds are the gravity beat (ease-in).
   The same maximized-shell/backdrop pair as the JS cascade: the
   maximized wrapper swaps its class to .maximized-backdrop while the
   window is lifted, and missing it there keeps maximized windows on
   the flat base beat while everything else falls with gravity. */
html.theme-page-slide .page-slide-ghost > .window,
html.theme-page-slide .page-slide-ghost > .maximized-shell > .window,
html.theme-page-slide .page-slide-ghost > .maximized-backdrop > .window,
html.theme-page-slide .page-slide-ghost .page > .window,
html.theme-page-slide .page-slide-ghost .page > .maximized-shell > .window,
html.theme-page-slide .page-slide-ghost .page > .maximized-backdrop > .window {
  animation-timing-function: ease-in !important;
}

/* Fallback look for the swapped-out iframes (music embeds etc.) - the
   PAGE's background colour: the placeholder is what shows through while
   the page fades/slides, and a white flash reads as visual pollution
   against the themed background. Document frames that carry their own
   opaque background (the white resume preview) adopt it in JS instead -
   a dark placeholder there reads as the PDF unloading mid-exit. */
html.theme-page-slide .page-slide-ghost .page-slide-placeholder-frame {
  background: var(--background-color);
}

@keyframes page-slide-fold {
  to {
    /* The fold is a squash AND A FALL: the window's bottom edge travels
       its measured distance to the taskbar line (--page-fold-dy, set per
       main window in JS) while the outline collapses - gravity, ease-in,
       oneXP minimize. */
    transform: translateY(var(--page-fold-dy, 0px)) scaleY(0.06);
  }
}

/* The fall distance belongs to the top-level window alone: nested
   windows reset it, so they squash in the parent's stage and RIDE the
   parent's fall instead of compounding it. */
html.theme-page-slide .page-slide-ghost .window .window {
  --page-fold-dy: 0px;
}

@keyframes page-slide-drop {
  to {
    /* Measured in JS (leaveGhost): the ghost's top edge must pass the
       viewport bottom - 100vh MINUS the old page's scroll offset (a
       fixed 110vh left the tail on screen for scrolled pages) - plus
       margin to clear the folded sliver. */
    translate: 0 var(--ps-drop-distance, 110vh);
  }
}

/* Departing dimmed overlays: the dark level fades in step with the
   departing windows (slide-off on dismissal, page fold on navigation);
   ride clones keep the fade going even when the portals unmount
   mid-animation. The ride carries its OWN full backdrop look - it must
   drop the .maximized-backdrop class, or body:has() would keep the page
   raised over the menu bar for the ride's whole lifetime. */
.maximized-backdrop.page-slide-backdrop-close,
.page-slide-backdrop-ride {
  position: fixed;
  inset: 0;
  z-index: 4000;
  pointer-events: none;
  background: rgba(0, 0, 0, 0.6);
  /* The ride carries its OWN flex alignment that used to come from
     .maximized-backdrop (dropped from the clone): without it, the
     window copy block-docks left of the page. */
  display: flex;
  align-items: safe center;
  justify-content: safe center;
  animation: page-slide-backdrop-fade 180ms ease-out forwards;
}

@keyframes page-slide-backdrop-fade {
  to {
    opacity: 0;
    background: rgba(0, 0, 0, 0);
  }
}

/* Departing exit on a closing maximized window: MaximizedWindow adds
   .window-minimize to the ride-out clone and measures --page-fold-dy to
   the taskbar line exactly like the ghost clone above, and the squish
   plays the same fold: the bottom edge slides to the taskbar line while
   the top edge falls to meet it. The !important outranks theme
   animations (theme-experimental-flash on maximized experimental windows
   would otherwise replace the fold); the clone paints above the raised
   page, above its fading dimmed layer. Since the ride clone is inert and
   the real window returns to the flow the moment the fold starts, the
   collapsing content never glitches beneath the squash. */
html.theme-page-slide .window.window-minimize {
  z-index: 4001;
  transform-origin: bottom center;
  animation: window-minimize-exit var(--ps-minimize, 98ms) ease-in forwards !important;
}

@keyframes window-minimize-exit {
  to {
    /* Fold onto the taskbar line, then clear it: without the extra
       100% offset the squashed sliver would rest exactly AT the bottom
       edge (the page drop that normally carries it away doesn't exist
       for a standalone dismissal). */
    transform: translateY(calc(var(--page-fold-dy, 80vh) + 100%)) scaleY(0.06);
  }
}

/* Parked (a hyperlink/maximized seed load): play NOTHING. Aiming the
   entrance at zero distance would still emit translate(0,0) - a live
   transform on <main> makes it the containing block for the in-tree
   position:fixed maximized backdrop, which then centers against main's
   box instead of the viewport. No animation = no transform = the
   backdrop stays viewport-anchored. First navigation applies a real
   direction (update of data-page-slide) and restarts the animations. */
html.theme-page-slide[data-page-slide="parked"] main:not(.page-slide-ghost) {
  animation: none;
}

/* The parked "none" must also hold for the normalized shell (main.page),
   whose combined entrance animation above declares at higher specificity
   than the un-classed main rule: this later, equal-specificity rule wins
   and keeps main transform-free - the maximized backdrop stays
   viewport-anchored. */
html.theme-page-slide[data-page-slide="parked"]
  main:not(.page-slide-ghost).page,
html.theme-page-slide[data-page-slide="parked"]
  main:not(.page-slide-ghost)
  > .page {
  animation: none;
}

@keyframes page-slide-pad {
  from {
    padding-bottom: var(--ps-pad-from, 0px);
  }
}

@keyframes page-slide-in {
  from {
    translate: var(--ps-in-x) var(--ps-in-y);
  }
}
