JavaScript must be enabled to play.
Browser lacks capabilities required to play.
Upgrade or switch to another browser.
Loading…
<<script>> PolicyManager.register({ name: "Affordable Housing Acquisition Fund Program", costUpfront: 20, duration: 999999, immediate(v) { v.publicOpinion -= 15; v.inflowBase = Math.max(0, v.inflowBase - 10); // Represents about 40 people/year prevented from homelessness v.unitsPreserved = (v.unitsPreserved || 0) + 300; // Track preserved units }, tick(v, age) { if (age >= 5) { v.budget += 1; } // Ongoing small reduction in certain vulnerable inflow categories due to stable housing from fund. if (age > 0 && age % 4 === 0) { // Annually v.inflowShare.withChildren = Math.max(0.01, v.inflowShare.withChildren - 0.005); v.inflowShare.racialized = Math.max(0.01, v.inflowShare.racialized - 0.005); window.Util.renorm(v.inflowShare); v.publicOpinion += 1; // Sustained positive effect of visible action } } }); <</script>> <div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">Units Preserved</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">300</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> units</span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Long-Term Savings</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">4</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> M / year</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Upfront Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">20</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> M</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em"> In November 2024, Ottawa City Council voted down a proposal to create an Affordable Housing Acquisition Fund. The plan would have redirected Vacant Unit Tax revenue to help non-profits purchase aging rentals and preserve affordability. By enacting the the policy, you perserve around 300 units and save an estimated $4 M annually in redevelopment costs, though it requires a $20 M up‑front investment. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('BuildCanadaHomes');">Continue</button> <button class="continue-button" onclick="SugarCube.Engine.backward();">Back</button> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title" style="white-space: nowrap; display: inline;">Affordable Housing</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">500</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Long-Term Savings</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">2<span class="impact-subtitle"> M / year</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Upfront Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">20<span class="impact-subtitle"> M</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em"> With land scarce and demand high, you chose to densify. Construction begins on mid-rise apartments designed for affordability, sustainability, and access to transit. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreServiceFunding');">Continue</button> </div>
<<set $turn += 1>> <<set $year += 1>> <<set $turn += 1>> <<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <<script>> // run on *every* time this passage finishes displaying $(document).on(':passagedisplay', function (ev) { // 1) inject the CSS if we haven't already if (!$('head').find('link[href*="swiper-bundle.min.css"]').length) { $('head').append( '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">' ); } // 2) helper to actually init the carousel function setupSwiper() { const el = ev.content.querySelector('.mySwiper'); if (!el) return; if (window.mySwiper) { window.mySwiper.destroy(true, true); } // ← here’s the only change: add observer + observeParents window.mySwiper = new Swiper(el, { effect: 'cards', grabCursor: true, observer: true, observeParents: true }); } // 3) load Swiper.js if needed, otherwise just init if (typeof Swiper === 'undefined') { $.getScript('https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js') .done(setupSwiper) .fail(() => console.error('Failed to load Swiper.')); } else { setupSwiper(); } }); <</script>> <div class="passageContent"> <!-- Demo styles --> <style> :root { zoom: 1.1; } .passageContent { margin-top: 0rem !important } html, body { position: relative; height: 100%; } .swiper-slide li { margin-bottom: 0.25em; /* reduce spacing between items */ padding-left: 0.5em; /* optional: for visual clarity */ margin-top: -.7em; } body { background: #eee; font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 14px; color: #000; margin: 0; padding: 0; } body { background: #fff; font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 14px; color: #000; margin: 0; padding: 0; } html, body { position: relative; height: 100%; } body { display: flex; justify-content: center; align-items: center; } .swiper { width: 23rem !important; max-width: 900px; height: 44em !important; max-height: 1000px; } @media screen and (max-width: 600px) { .swiper { width: 42vw !important; } .swiper-slide { font-size: 0.9em; padding: 1rem 0.75rem; } } @media screen and (max-width: 979px) { .swiper { width: 38vw !important; margin-left: 2rem !important; } .button { margin-left: 2rem !important; } } .swiper-slide-shadow { background: rgba(0, 0, 0, 0.05) !important; } @media screen and (max-width: 979px) { .swiper { width: 38vw !important; margin-left: 2rem !important; } .button { margin-left: 2rem !important; } .swiper-slide { font-size: 0.85em; padding: 1rem 0.75rem; } .impacts { display: none; } } .swiper-slide { display: flex; align-items: center; justify-content: flex-start; padding: 20px; font-size: 14px; background-color: #fff; border-radius: 14.130435% / 10.483871%; border-color: rgba(156, 162, 166, 0.32); border-left-width: 1.5px; border-right-width: 1.5px; border-style: solid; border-top-width: 1.5px; border-bottom-width: 1.5px; } .swiper-slide { pointer-events: none; } .slider-wrapper { display: flex; flex-direction: column; align-items: center; /* center slider + button together */ margin: 2rem auto; /* vertical spacing + center wrapper in page */ } .slider-wrapper .swiper { margin: 0; /* reset any old margins */ margin-top: 3rem; */ } .swiper-slide.swiper-slide-active { pointer-events: auto; border-color: rgba(156, 162, 166, 0.18); border-left-width: 1.5px; border-right-width: 1.5px; border-style: solid; border-top-width: 1.5px; border-bottom-width: 1.5px; } .break { display: block; content: ""; } .small-break { display: block; content: ""; } .bill-header { text-align: center; margin-bottom: 20px; line-height: 1.2; border-bottom: 1px solid #ccc; padding-bottom: 0.5em; margin-bottom: 1em; } .bill-header div { font-variant: small-caps; letter-spacing: 1px; font-size: 0.85em; } .bill-header .bill-number { font-size: 1.2em; margin-top: 6px; } .swiper-slide h2 { margin: 0 0 16px; font-size: 1.3em; text-align: center; font-variant: small-caps; letter-spacing: 0.5px; } .swiper-slide p { line-height: 1.5; font-size: 0.95em; flex: 1; } button { background-color: #ffffff !important; color: #111827 !important; border: 1px solid #e5e7eb !important; padding: 0.5rem 0.75rem !important; border-radius: 0.5rem !important; font-size: 0.875rem !important; font-weight: 500 !important; line-height: 1.25 !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 0.3rem !important; position: relative !important; top: -0.25rem !important; text-align: center !important; margin: 0.3rem auto; /* centers it */ position: relative; } .meta { text-align: right; font-size: 0.8em; color: #555; margin-top: -10px; margin-right: 6px; font-family: Arial, sans-serif; } @font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}:host{position:relative;display:block;margin-left:auto;margin-right:auto;z-index:1}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1;display:block}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;transition-timing-function:var(--swiper-wrapper-transition-timing-function,initial);box-sizing:content-box}.swiper-android .swiper-slide,.swiper-ios .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-horizontal{touch-action:pan-y}.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform;display:block}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d{perspective:1200px}.swiper-3d .swiper-cube-shadow,.swiper-3d .swiper-slide{transform-style:preserve-3d}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-css-mode.swiper-horizontal>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-css-mode.swiper-vertical>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-css-mode.swiper-free-mode>.swiper-wrapper{scroll-snap-type:none}.swiper-css-mode.swiper-free-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:none}.swiper-css-mode.swiper-centered>.swiper-wrapper::before{content:'';flex-shrink:0;order:9999}.swiper-css-mode.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center;scroll-snap-stop:always}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper::before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper::before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader,.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.swiper-virtual .swiper-slide{-webkit-backface-visibility:hidden;transform:translateZ(0)}.swiper-virtual.swiper-css-mode .swiper-wrapper::after{content:'';position:absolute;left:0;top:0;pointer-events:none}.swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after{height:1px;width:var(--swiper-virtual-size)}.swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after{width:1px;height:var(--swiper-virtual-size)}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:var(--swiper-navigation-top-offset,50%);width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next svg,.swiper-button-prev svg{width:100%;height:100%;object-fit:contain;transform-origin:center}.swiper-rtl .swiper-button-next svg,.swiper-rtl .swiper-button-prev svg{transform:rotate(180deg)}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:var(--swiper-navigation-sides-offset,10px);right:auto}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-lock{display:none}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:initial;line-height:1}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:'next'}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-pagination-disabled>.swiper-pagination,.swiper-pagination.swiper-pagination-disabled{display:none!important}.swiper-horizontal>.swiper-pagination-bullets,.swiper-pagination-bullets.swiper-pagination-horizontal,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:var(--swiper-pagination-bottom,8px);top:var(--swiper-pagination-top,auto);left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:var(--swiper-pagination-bullet-width,var(--swiper-pagination-bullet-size,8px));height:var(--swiper-pagination-bullet-height,var(--swiper-pagination-bullet-size,8px));display:inline-block;border-radius:var(--swiper-pagination-bullet-border-radius,50%);background:var(--swiper-pagination-bullet-inactive-color,#000);opacity:var(--swiper-pagination-bullet-inactive-opacity, .2)}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:var(--swiper-pagination-bullet-opacity, 1);background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-pagination-vertical.swiper-pagination-bullets,.swiper-vertical>.swiper-pagination-bullets{right:var(--swiper-pagination-right,8px);left:var(--swiper-pagination-left,auto);top:50%;transform:translate3d(0px,-50%,0)}.swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:var(--swiper-pagination-bullet-vertical-gap,6px) 0;display:block}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 var(--swiper-pagination-bullet-horizontal-gap,4px)}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-horizontal.swiper-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-fraction{color:var(--swiper-pagination-fraction-color,inherit)}.swiper-pagination-progressbar{background:var(--swiper-pagination-progressbar-bg-color,rgba(0,0,0,.25));position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-horizontal>.swiper-pagination-progressbar,.swiper-pagination-progressbar.swiper-pagination-horizontal,.swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite,.swiper-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:var(--swiper-pagination-progressbar-size,4px);left:0;top:0}.swiper-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-vertical,.swiper-vertical>.swiper-pagination-progressbar{width:var(--swiper-pagination-progressbar-size,4px);height:100%;left:0;top:0}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:var(--swiper-scrollbar-border-radius,10px);position:relative;touch-action:none;background:var(--swiper-scrollbar-bg-color,rgba(0,0,0,.1))}.swiper-scrollbar-disabled>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-disabled{display:none!important}.swiper-horizontal>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-horizontal{position:absolute;left:var(--swiper-scrollbar-sides-offset,1%);bottom:var(--swiper-scrollbar-bottom,4px);top:var(--swiper-scrollbar-top,auto);z-index:50;height:var(--swiper-scrollbar-size,4px);width:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar.swiper-scrollbar-vertical,.swiper-vertical>.swiper-scrollbar{position:absolute;left:var(--swiper-scrollbar-left,auto);right:var(--swiper-scrollbar-right,4px);top:var(--swiper-scrollbar-sides-offset,1%);z-index:50;width:var(--swiper-scrollbar-size,4px);height:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:var(--swiper-scrollbar-drag-bg-color,rgba(0,0,0,.5));border-radius:var(--swiper-scrollbar-border-radius,10px);left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move;touch-action:none}.swiper .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-grid>.swiper-wrapper{flex-wrap:wrap}.swiper-grid-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-fade.swiper-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-fade .swiper-slide-active{pointer-events:auto}.swiper-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper.swiper-cube{overflow:visible}.swiper-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-cube.swiper-rtl .swiper-slide{transform-origin:100% 0}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-next,.swiper-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-cube .swiper-slide-next+.swiper-slide{pointer-events:auto;visibility:visible}.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-bottom,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-left,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-right,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper.swiper-flip{overflow:visible}.swiper-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-flip .swiper-slide-active,.swiper-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-bottom,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-left,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-right,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-creative .swiper-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;transition-property:transform,opacity,height}.swiper.swiper-cards{overflow:visible}.swiper-cards .swiper-slide{transform-origin:center bottom;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden} </style> <div class="slider-wrapper"> <div class="swiper mySwiper"> <div class="swiper-wrapper"> <div class="swiper-slide"> <div class="bill-header"> <div>Province of Ontario</div> <div>April 30<sup>th</sup>, 2025</div> <div>Bill 6</div> <div class="bill-number">Premier Doug Ford</div> </div> <h2>Safer Municipalities Act</h2> <p> Whereas encampments and public drug use in parks and transit areas have grown visibly; and,<br /> Whereas the Province proposes stricter public‐space controls via the Safer Municipalities Act;<br /><br /> </p> <p> <em>Therefore be it enacted that</em> municipalities may issue no‐trespass orders against encampments on public land, and provincial officers may arrest or fine (up to $10,000 or 6 months’ jail) anyone in possession of an illegal substance in a public space. </p> <div class="impacts"> <p>Anticipated impacts include:</p> <ul> <li><p>↑ Enforcement costs and police involvement in encampment areas</p></li> <li><p>↓ Trust from service providers and advocates (seen as punitive)</p></li> <li><p>↑ Access to provincial grants if the city complies with enforcement provisions</p></li> <li><p>Legal challenges over Charter rights and potential court injunctions</p></li> </ul> </div> <div class="meta"> Ontario Legislature </div> </div> </div> </div> <div class="decision-row"> <div class="tabs" id="decisionTabs"> <button class="tab active" data-choice="Comply" style="view-transition-name: tab-1">Comply</button> <button class="tab" data-choice="Delay" style="view-transition-name: tab-2">Delay</button> <button class="tab" data-choice="Oppose" style="view-transition-name: tab-3">Oppose</button> </div> <button class="confirm-button" onclick="confirmBill6Choice()">Continue</button> </div> <style> .decision-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 12px; margin: 2rem auto; flex-direction: row; } .tabs { display: flex; gap: 8px; background: #f4f4f5; border: 1px solid #cfcfcf; border-radius: 24px; box-shadow: rgba(0, 0, 0, 0.05) 0px 4px 6px; padding: 4px; } .tab { padding: 10px 14px; border: none; background: none; border-radius: 20px; font-size: 14px; color: #71717a; cursor: pointer; position: relative; font-weight: 500; transition: color 0.2s ease, background-color 0.2s ease; z-index: 1; } .tab.active { color: #ffffff !important; z-index: 2; } .tab.active::before { content: ""; position: absolute; height: 100%; inset: 0; background: #000; border-radius: inherit; z-index: -1; transition: background 0.3s ease; } .confirm-button { padding: 10px 16px; background: black; color: white; border: none; border-radius: 12px; font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.2s ease; } .confirm-button:hover { background: #333; } ::view-transition-group(*) { animation-duration: 0.4s; animation-timing-function: ease-in-out; } .tab::before { view-transition-name: tab-background; } ::view-transition-old(*), ::view-transition-new(*) { height: 100%; } </style> <script> const tabs = document.querySelectorAll('.tab'); let selectedChoice = "Comply"; // default tabs.forEach(tab => { tab.addEventListener('click', () => { document.startViewTransition(() => { document.querySelector('.tab.active')?.classList.remove('active'); tab.classList.add('active'); selectedChoice = tab.getAttribute('data-choice'); }); }); }); function confirmBill6Choice() { SugarCube.Engine.play('Bill6_' + selectedChoice); } </script>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Unsheltered Homelessness</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">15<span class="impact-subtitle"> %</span></span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Incarceration Inflow</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">18<span class="impact-subtitle"> %</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Service Provider Trust</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">30<span class="impact-subtitle"> pts</span></span> </div> </div> </div> </div> <p class="description"> Adopting the full <em>Safer Municipalities Act</em> quickly reduces unsheltered homelessness as encampments are cleared, but at the cost of an 18% rise in incoming incarceration. Trust with service providers drops sharply. The city qualifies for the full $8M in provincial enforcement-linked funding. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreZoning');">Continue</button> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Unsheltered Homelessness</div> <div class="impact-content"> <span class="impact-icon">↮ </span> <span class="impact-value">--</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-icon">↓</span> <span class="impact-value">10<span class="impact-subtitle"> pts</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Provincial Funding Access</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">50<span class="impact-subtitle"> %</span></span> </div> </div> </div> </div> <p class="description"> Delaying full enforcement keeps the city in a holding pattern. Encampments remain visible and trust declines mildly as uncertainty frustrates both partners, advocates and the public. You receive only partial funding from the province while legal outcomes remain in flux. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreZoning');">Continue</button> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">25<span class="impact-subtitle"> pts</span></span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Unsheltered Homelessness</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">2<span class="impact-subtitle"> %</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Political Cost</div> <div class="impact-content"> <span style="color: #06C; font-size: 5em; padding-right: 10px;">●</span><span style="color: #06C; font-size: 5em;">●</span> </div> </div> </div> <p class="description"> Rejecting Bill 6 preserves dignity and avoids punitive enforcement. Incarceration does not increase, and trust with frontline providers surges as the public tentatively supports the disobedience. However, visible homelessness rises moderately as encampments grow. The city forfeits all provincial funding tied to compliance. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreZoning');">Continue</button> </div>
<<script>> // run on *every* time this passage finishes displaying $(document).on(':passagedisplay', function (ev) { // 1) inject the CSS if we haven't already if (!$('head').find('link[href*="swiper-bundle.min.css"]').length) { $('head').append( '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">' ); } // 2) helper to actually init the carousel function setupSwiper() { const el = ev.content.querySelector('.mySwiper'); if (!el) return; if (window.mySwiper) { window.mySwiper.destroy(true, true); } // ← here’s the only change: add observer + observeParents window.mySwiper = new Swiper(el, { effect: 'cards', grabCursor: true, observer: true, observeParents: true }); } // 3) load Swiper.js if needed, otherwise just init if (typeof Swiper === 'undefined') { $.getScript('https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js') .done(setupSwiper) .fail(() => console.error('Failed to load Swiper.')); } else { setupSwiper(); } }); <</script>> <div class="passageContent"> <!-- Demo styles --> <style> :root { zoom: 1.1; } html, body { position: relative; height: 100%; } body { background: #eee; font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 14px; color: #000; margin: 0; padding: 0; } body { background: #fff; font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 14px; color: #000; margin: 0; padding: 0; } html, body { position: relative; height: 100%; } body { display: flex; justify-content: center; align-items: center; } .swiper { width: 23rem !important; max-width: 900px; height: 40em !important; max-height: 1000px; } @media screen and (max-width: 600px) { .swiper { width: 42vw !important; } .swiper-slide { font-size: 0.9em; padding: 1rem 0.75rem; } } @media screen and (max-width: 979px) { .swiper { width: 38vw !important; margin-left: 2rem !important; } .button { margin-left: 2rem !important; } } .swiper-slide-shadow { background: rgba(0, 0, 0, 0.05) !important; } .swiper-slide { display: flex; align-items: center; justify-content: flex-start; padding: 20px; font-size: 14px; background-color: #fff; font-family: Georgia, serif !important; border-radius: 14.130435% / 10.483871%; border-color: rgba(156, 162, 166, 0.32); border-left-width: 1.5px; border-right-width: 1.5px; border-style: solid; border-top-width: 1.5px; border-bottom-width: 1.5px; } .swiper-slide { pointer-events: none; } .slider-wrapper { display: flex; flex-direction: column; align-items: center; /* center slider + button together */ margin: 2rem auto; /* vertical spacing + center wrapper in page */ } .slider-wrapper .swiper { margin: 0; /* reset any old margins */ margin-left: -1rem; /* <<–– shift the whole slider a bit left */ */ } .swiper-slide.swiper-slide-active { pointer-events: auto; border-color: rgba(156, 162, 166, 0.18); border-left-width: 1.5px; border-right-width: 1.5px; border-style: solid; border-top-width: 1.5px; border-bottom-width: 1.5px; } .bill-header { text-align: center; margin-bottom: 20px; line-height: 1.2; border-bottom: 1px solid #ccc; padding-bottom: 0.5em; margin-bottom: 1em; } .bill-header div { font-variant: small-caps; letter-spacing: 1px; font-size: 0.85em; } .bill-header .bill-number { font-size: 1.2em; margin-top: 6px; } .swiper-slide h2 { margin: 0 0 16px; font-size: 1.3em; text-align: center; font-variant: small-caps; letter-spacing: 0.5px; } .swiper-slide p { line-height: 1.5; font-size: 0.95em; flex: 1; } button { background-color: #ffffff !important; color: #111827 !important; border: 1px solid #e5e7eb !important; padding: 0.5rem 0.75rem !important; border-radius: 0.5rem !important; font-size: 0.875rem !important; font-weight: 500 !important; line-height: 1.25 !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 0.3rem !important; position: relative !important; top: -0.25rem !important; text-align: center !important; margin: 0.3rem auto; /* centers it */ position: relative; } .meta { text-align: right; font-size: 0.8em; color: #555; margin-top: 20px; font-family: Arial, sans-serif; } @font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}:host{position:relative;display:block;margin-left:auto;margin-right:auto;z-index:1}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1;display:block}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;transition-timing-function:var(--swiper-wrapper-transition-timing-function,initial);box-sizing:content-box}.swiper-android .swiper-slide,.swiper-ios .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-horizontal{touch-action:pan-y}.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform;display:block}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d{perspective:1200px}.swiper-3d .swiper-cube-shadow,.swiper-3d .swiper-slide{transform-style:preserve-3d}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-css-mode.swiper-horizontal>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-css-mode.swiper-vertical>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-css-mode.swiper-free-mode>.swiper-wrapper{scroll-snap-type:none}.swiper-css-mode.swiper-free-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:none}.swiper-css-mode.swiper-centered>.swiper-wrapper::before{content:'';flex-shrink:0;order:9999}.swiper-css-mode.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center;scroll-snap-stop:always}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper::before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper::before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader,.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.swiper-virtual .swiper-slide{-webkit-backface-visibility:hidden;transform:translateZ(0)}.swiper-virtual.swiper-css-mode .swiper-wrapper::after{content:'';position:absolute;left:0;top:0;pointer-events:none}.swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after{height:1px;width:var(--swiper-virtual-size)}.swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after{width:1px;height:var(--swiper-virtual-size)}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:var(--swiper-navigation-top-offset,50%);width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next svg,.swiper-button-prev svg{width:100%;height:100%;object-fit:contain;transform-origin:center}.swiper-rtl .swiper-button-next svg,.swiper-rtl .swiper-button-prev svg{transform:rotate(180deg)}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:var(--swiper-navigation-sides-offset,10px);right:auto}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-lock{display:none}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:initial;line-height:1}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:'next'}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-pagination-disabled>.swiper-pagination,.swiper-pagination.swiper-pagination-disabled{display:none!important}.swiper-horizontal>.swiper-pagination-bullets,.swiper-pagination-bullets.swiper-pagination-horizontal,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:var(--swiper-pagination-bottom,8px);top:var(--swiper-pagination-top,auto);left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:var(--swiper-pagination-bullet-width,var(--swiper-pagination-bullet-size,8px));height:var(--swiper-pagination-bullet-height,var(--swiper-pagination-bullet-size,8px));display:inline-block;border-radius:var(--swiper-pagination-bullet-border-radius,50%);background:var(--swiper-pagination-bullet-inactive-color,#000);opacity:var(--swiper-pagination-bullet-inactive-opacity, .2)}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:var(--swiper-pagination-bullet-opacity, 1);background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-pagination-vertical.swiper-pagination-bullets,.swiper-vertical>.swiper-pagination-bullets{right:var(--swiper-pagination-right,8px);left:var(--swiper-pagination-left,auto);top:50%;transform:translate3d(0px,-50%,0)}.swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:var(--swiper-pagination-bullet-vertical-gap,6px) 0;display:block}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 var(--swiper-pagination-bullet-horizontal-gap,4px)}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-horizontal.swiper-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-fraction{color:var(--swiper-pagination-fraction-color,inherit)}.swiper-pagination-progressbar{background:var(--swiper-pagination-progressbar-bg-color,rgba(0,0,0,.25));position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-horizontal>.swiper-pagination-progressbar,.swiper-pagination-progressbar.swiper-pagination-horizontal,.swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite,.swiper-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:var(--swiper-pagination-progressbar-size,4px);left:0;top:0}.swiper-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-vertical,.swiper-vertical>.swiper-pagination-progressbar{width:var(--swiper-pagination-progressbar-size,4px);height:100%;left:0;top:0}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:var(--swiper-scrollbar-border-radius,10px);position:relative;touch-action:none;background:var(--swiper-scrollbar-bg-color,rgba(0,0,0,.1))}.swiper-scrollbar-disabled>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-disabled{display:none!important}.swiper-horizontal>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-horizontal{position:absolute;left:var(--swiper-scrollbar-sides-offset,1%);bottom:var(--swiper-scrollbar-bottom,4px);top:var(--swiper-scrollbar-top,auto);z-index:50;height:var(--swiper-scrollbar-size,4px);width:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar.swiper-scrollbar-vertical,.swiper-vertical>.swiper-scrollbar{position:absolute;left:var(--swiper-scrollbar-left,auto);right:var(--swiper-scrollbar-right,4px);top:var(--swiper-scrollbar-sides-offset,1%);z-index:50;width:var(--swiper-scrollbar-size,4px);height:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:var(--swiper-scrollbar-drag-bg-color,rgba(0,0,0,.5));border-radius:var(--swiper-scrollbar-border-radius,10px);left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move;touch-action:none}.swiper .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-grid>.swiper-wrapper{flex-wrap:wrap}.swiper-grid-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-fade.swiper-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-fade .swiper-slide-active{pointer-events:auto}.swiper-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper.swiper-cube{overflow:visible}.swiper-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-cube.swiper-rtl .swiper-slide{transform-origin:100% 0}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-next,.swiper-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-cube .swiper-slide-next+.swiper-slide{pointer-events:auto;visibility:visible}.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-bottom,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-left,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-right,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper.swiper-flip{overflow:visible}.swiper-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-flip .swiper-slide-active,.swiper-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-bottom,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-left,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-right,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-creative .swiper-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;transition-property:transform,opacity,height}.swiper.swiper-cards{overflow:visible}.swiper-cards .swiper-slide{transform-origin:center bottom;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden} </style> <div class="slider-wrapper"> <div class="swiper mySwiper"> <div class="swiper-wrapper"> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">H.R. 5501</div> </div> <h2>Bill 1</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Environmental Justice Act of 2025.”<br /><br /> <strong>Section 2. Findings.</strong><br /> (1) Communities of color and low-income communities face disproportionate environmental burdens.<br /> (2) <em>Additional findings to be inserted…</em> </p> <div class="meta"> Sponsor: Rep. Jane Doe | Introduced: March 5, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">S. 2202</div> </div> <h2>Bill 2</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Clean Water for All Act.”<br /><br /> <strong>Section 2. Purpose.</strong><br /> (1) To ensure access to safe and affordable drinking water for all Americans.<br /> (2) <em>Further provisions to follow…</em> </p> <div class="meta"> Sponsor: Sen. John Smith | Introduced: February 12, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">H.R. 3305</div> </div> <h2>Bill 3</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Affordable Housing Expansion Act.”<br /><br /> <strong>Section 2. Findings.</strong><br /> (1) There is a national shortage of affordable housing.<br /> (2) <em>Additional findings to be inserted…</em> </p> <div class="meta"> Sponsor: Rep. Maria Lee | Introduced: January 20, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">S. 1457</div> </div> <h2>Bill 4</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “National Broadband Expansion Act.”<br /><br /> <strong>Section 2. Findings.</strong><br /> (1) Universal broadband access is essential for economic opportunity.<br /> (2) <em>Further findings to be inserted…</em> </p> <div class="meta"> Sponsor: Sen. Alex Kim | Introduced: March 10, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">H.R. 2077</div> </div> <h2>Bill 5</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Student Debt Relief Act.”<br /><br /> <strong>Section 2. Purpose.</strong><br /> (1) To provide relief for federal student loan borrowers.<br /> (2) <em>Additional provisions to be inserted…</em> </p> <div class="meta"> Sponsor: Rep. Linda Tran | Introduced: February 28, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">S. 3890</div> </div> <h2>Bill 6</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Climate Resilience Infrastructure Act.”<br /><br /> <strong>Section 2. Findings.</strong><br /> (1) Infrastructure must be modernized to withstand climate change.<br /> (2) <em>Further findings to be inserted…</em> </p> <div class="meta"> Sponsor: Sen. Priya Patel | Introduced: January 30, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">H.R. 9999</div> </div> <h2>Bill 7</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Universal Childcare Access Act.”<br /><br /> <strong>Section 2. Purpose.</strong><br /> (1) To expand access to affordable childcare services.<br /> (2) <em>Further provisions to be inserted…</em> </p> <div class="meta"> Sponsor: Rep. Samuel Green | Introduced: March 2, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">S. 1010</div> </div> <h2>Bill 8</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Healthcare Equity Advancement Act.”<br /><br /> <strong>Section 2. Findings.</strong><br /> (1) Persistent disparities exist in healthcare access and outcomes.<br /> (2) <em>Additional findings to be inserted…</em> </p> <div class="meta"> Sponsor: Sen. Emily Carter | Introduced: January 15, 2025 </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>117<sup>th</sup> Congress</div> <div>1<sup>st</sup> Session</div> <div class="bill-number">H.R. 8888</div> </div> <h2>Bill 9</h2> <p> Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled:<br /><br /> <strong>Section 1. Short title.</strong><br /> This Act may be cited as the “Small Business Recovery Act.”<br /><br /> <strong>Section 2. Purpose.</strong><br /> (1) To support small businesses in recovering from economic downturns.<br /> (2) <em>Further provisions to follow…</em> </p> <div class="meta"> Sponsor: Rep. Olivia Brown | Introduced: February 10, 2025 </div> </div> </div> </div> <div style="display: flex; flex-direction: column; align-items: center; margin-top: 2em;"> <<button "OK">> <<run (()=>{ const map = [ "Bill 1", "Bill 2", "Bill 3", "Bill 4" ]; // get the active slide index const idx = window.mySwiper.activeIndex; // play the corresponding passage Engine.play(map[idx]); })()>> <</button>> </div> </div> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .drawer-content { padding: 10px; padding-left: 25%; padding-right: 25%; } .button-wrapper { display: inline-flex; align-items: center; width: 100%; max-width: 420px; margin: 0.9rem auto; position: relative; top: -0.25rem; } .button { flex: 1; background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); } .button:focus { outline: none; box-shadow: 0 0 0 3px rgba(58, 122, 217, 0.5); border-color: rgb(44, 123, 202); } .button:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; border-color: #2a5a87; } .button:disabled:hover, .button:disabled:active { pointer-events: none; box-shadow: none; } .button:first-of-type { margin-top: 1rem; } .info-btn { background: none; border: none; padding: 0; margin-left: 0.5rem; cursor: pointer; } .info-btn img { width: 1.2rem; height: 1.2rem; opacity: 0.8; transition: opacity 0.2s ease; } .info-btn:hover img { opacity: 1; } </style> <!-- Intro Text & Buttons --> <div class="passage-content"> <div class="build-intro"> <p> The federal government launches <strong>Build Canada Homes</strong>, its most ambitious housing program since WWII. The program offers billions in financing, standardized housing designs, and land access for cities ready to build. Your city has been invited to submit a proposal. Do you focus on mass affordable units, starter homes, or innovative pilots? </p> <div class="button-wrapper"> <button class="button" onclick="buildAffordable();"> Densify existing neighbourhoods with new mid-rise apartments </button> <button id="info-affordable" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> <div class="button-wrapper"> <button class="button" onclick="buildStarter();"> Expand suburban development with starter homes for low and middle-income families </button> <button id="info-starter" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> <div class="button-wrapper"> <button class="button" onclick="buildModular();"> Accelerate housing construction through prefabricated, factory-built homes </button> <button id="info-modular" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> </div> </div> <!-- Drawers --> <sl-drawer placement="bottom" label=" " class="drawer-affordable"> <div class="drawer-content"> <h3>Mass-Transit & Community</h3> <p> Building mid-rise apartments near transit hubs reduces car dependence, promotes mixed-use sidewalks, and shortens construction time in dense zones. </p> <h4>Missing Middle Housing</h4> <p> “Missing middle” refers to housing types between detached homes and high-rises—such as triplexes, townhomes, and mid-rise apartments. These help meet demand while blending with existing neighborhoods. </p> <h4>Transit-Oriented Development</h4> <p> Concentrating homes near bus and rail stations reduces sprawl, increases transit ridership, and encourages walkable, vibrant streetscapes. These areas often see higher retail activity and better air quality. </p> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-starter"> <div class="drawer-content"> <h3>Suburban Starter Homes</h3> <p> New subdivisions on the city’s edge offer turnkey homes for working families—with parks, schools, and roads built into the plan. </p> <h4>Starter Homes</h4> <p> Starter homes are smaller, affordable detached houses aimed at first-time buyers. These homes provide paths to ownership and long-term stability. </p> <h4>Incremental Growth</h4> <p> Inspired by post-war developments like Levittown, these neighborhoods prioritize fast approvals and modular planning, balancing cost with liveability. </p> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-modular"> <div class="drawer-content"> <h3>Factory-Built Efficiency</h3> <p> Prefabricated housing shortens timelines and lowers costs. Homes are assembled in factories, then installed on-site—ideal for both suburban and urban infill. </p> <h4>Modular & Panel Systems</h4> <p> Components like walls and plumbing come prebuilt, allowing rapid assembly. Projects range from tiny homes to multi-storey apartment blocks and office conversions. </p> <h4>Digital Manufacturing</h4> <p> Modern factories use digital design, robotic cutting, and climate-controlled precision to reduce waste and labor dependency—especially useful in remote or high-demand areas. </p> </div> </sl-drawer> <!-- Wiring & Policy Logic --> <script> // Info-icon → drawer document.getElementById('info-affordable').addEventListener('click', () => { document.querySelector('.drawer-affordable').show(); }); document.getElementById('info-starter').addEventListener('click', () => { document.querySelector('.drawer-starter').show(); }); document.getElementById('info-modular').addEventListener('click', () => { document.querySelector('.drawer-modular').show(); }); // Drawer close buttons document.querySelectorAll('sl-drawer sl-button[slot="footer"]').forEach(btn => { btn.addEventListener('click', () => btn.closest('sl-drawer').hide()); }); // Policy registration functions function buildAffordable() { PolicyManager.register({ name: "Build City Affordable Housing (Mid-Rise Densification)", costUpfront: 10, duration: 999999, immediate(v) { v.publicOpinion -= 10; }, tick(v, age) { let operationalCost = 2; if (age >= 8) operationalCost = 1; if (age >= 20) operationalCost = 0.5; if (age >= 28) operationalCost = -0.5; v.budget -= operationalCost; if (age >= 4 && age < 44) { if (age % 4 === 0) { const newUnitsThisYear = 50; window.Util.exitPeople(newUnitsThisYear, { sheltered: 0.5, transitional: 0.3, unsheltered: 0.2 }); v.systemCapacity += 2; } } if (age >= 20 && age < 60) { if (age % 4 === 0 && v.serviceCoordination < 85) v.serviceCoordination += 1; } } }); SugarCube.Engine.play("Affordable"); } function buildStarter() { PolicyManager.register({ name: "Starter Homes Expansion (Suburban)", costUpfront: 10, duration: 999999, immediate(v) { }, tick(v, age) { let cost = 0.5; v.budget -= cost; if (age >= 4 && age < 24) { if (age % 4 === 0) { v.inflowBase = Math.max(0, v.inflowBase - 5); window.Util.exitPeople(20, { withChildren: 0.6, sheltered: 0.4 }); if (v.systemCapacity < 150) v.systemCapacity += 1; } } if (age >= 20) { if (age % 4 === 0 && v.serviceCoordination > 40) v.serviceCoordination -= 1; if (age % 8 === 0 && v.publicOpinion > 30) v.publicOpinion -= 2; } } }); SugarCube.Engine.play("Starter"); } function buildModular() { PolicyManager.register({ name: "Modular Housing & Office Conversions", costUpfront: 5, duration: 999999, immediate(v) { v.publicOpinion += 2; v.systemCapacity += 5; }, tick(v, age) { let cost = 0.5; if (age >= 8) cost = 0.1; if (age >= 16) cost = -0.25; v.budget -= cost; if (age >= 2 && age < 12) { const newUnitsThisTurn = 50; window.Util.exitPeople(newUnitsThisTurn, { transitional: 0.4, sheltered: 0.4, unsheltered: 0.2 }); v.systemCapacity += 2; } if (age >= 12 && age < 28) { if (age % 4 === 0 && v.serviceCoordination < 85) v.serviceCoordination += 1; } } }); SugarCube.Engine.play("Modular"); } </script>
<div class="policy-impact-screen" > <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Economic Growth</div> <div class="impact-content"> <span class="impact-number">+0.3%</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-number">+8%</span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Service Coordination</div> <div class="impact-content"> <div class="impact-widget">★ ★ ★ ★ ☆</div> </div> </div> </div> </div> <h3>Project Impact</h3> <p> This student-led event raises awareness and funds for youth homelessness in Toronto through an engaging, community-based fundraiser. It strengthens relationships between students and local organizations, encourages grassroots advocacy, and increases visibility around homelessness as a public health and policy issue. </p> <div class="break"> </div> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT1');">Continue</button> <button class="continue-button" onclick="SugarCube.Engine.backward();">Back</button> </div>
<div class="policy-impact-screen" > <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Economic Growth</div> <div class="impact-content"> <span class="impact-number">+0.1%</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-number">+5%</span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Service Coordination</div> <div class="impact-content"> <div class="impact-widget">★ ★ ★ ☆ ☆</div> </div> </div> </div> </div> <h3>Project Impact</h3> <p> This initiative provides emergency backpack kits with weather-resistant supplies for people experiencing homelessness, particularly during climate-related events. It addresses immediate survival needs and promotes climate-resilient planning in urban homeless services. </p> <div class="break"> </div> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT1');">Continue</button> <button class="continue-button" onclick="SugarCube.Engine.backward();">Back</button> </div>
<div class="policy-impact-screen" > <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Economic Growth</div> <div class="impact-content"> <span class="impact-number">+0.5%</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-number">+6%</span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Service Coordination</div> <div class="impact-content"> <div class="impact-widget">★ ★ ★ ★ ☆</div> </div> </div> </div> </div> <h3>Project Impact</h3> <p> This project connects local farmers with shelters and community kitchens to redistribute unsold produce. It reduces food waste while improving nutrition and food security for unhoused populations, building a stronger local food ecosystem in the process. </p> <div class="break"> </div> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT1');">Continue</button> <button class="continue-button" onclick="SugarCube.Engine.backward();">Back</button> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .drawer-content { padding: 10px; padding-left: 25%; padding-right: 25%; } .button-wrapper { display: inline-flex; align-items: center; width: 100%; max-width: 420px; margin: 0.9rem auto; position: relative; top: -0.25rem; } .button { flex: 1; background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); } .button:focus { outline: none; box-shadow: 0 0 0 3px rgba(58, 122, 217, 0.5); border-color: rgb(44, 123, 202); } .button:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; border-color: #2a5a87; } .button:disabled:hover, .button:disabled:active { pointer-events: none; box-shadow: none; } .button:first-of-type { margin-top: 1rem; } .info-btn { background: none; border: none; padding: 0; margin-left: 0.5rem; cursor: pointer; } .info-btn img { width: 1.2rem; height: 1.2rem; opacity: 0.8; transition: opacity 0.2s ease; } .info-btn:hover img { opacity: 1; } </style> <!-- Intro Text & Buttons --> <div class="passage-content"> <div class="build-intro"> <p> The federal government launches <strong>Build Canada Homes</strong>, its most ambitious housing program since WWII. The program offers billions in financing, standardized housing designs, and land access for cities ready to build. Your city has been invited to submit a proposal. Do you focus on mass affordable units, starter homes, or innovative pilots? </p> <div class="button-wrapper"> <button class="button" onclick="buildAffordable();"> Densify existing neighbourhoods with new mid-rise apartments </button> <button id="info-affordable" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> <div class="button-wrapper"> <button class="button" onclick="buildStarter();"> Expand suburban development with starter homes for low and middle-income families </button> <button id="info-starter" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> <div class="button-wrapper"> <button class="button" onclick="buildModular();"> Accelerate housing construction through prefabricated, factory-built homes </button> <button id="info-modular" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> </div> </div> <!-- Drawers --> <sl-drawer placement="bottom" label=" " class="drawer-affordable"> <div class="drawer-content"> <h3>Mass-Transit & Community</h3> <p> Building mid-rise apartments near transit hubs reduces car dependence, promotes mixed-use sidewalks, and shortens construction time in dense zones. </p> <h4>Missing Middle Housing</h4> <p> “Missing middle” refers to housing types between detached homes and high-rises—such as triplexes, townhomes, and mid-rise apartments. These help meet demand while blending with existing neighborhoods. </p> <h4>Transit-Oriented Development</h4> <p> Concentrating homes near bus and rail stations reduces sprawl, increases transit ridership, and encourages walkable, vibrant streetscapes. These areas often see higher retail activity and better air quality. </p> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-starter"> <div class="drawer-content"> <h3>Suburban Starter Homes</h3> <p> New subdivisions on the city’s edge offer turnkey homes for working families—with parks, schools, and roads built into the plan. </p> <h4>Starter Homes</h4> <p> Starter homes are smaller, affordable detached houses aimed at first-time buyers. These homes provide paths to ownership and long-term stability. </p> <h4>Incremental Growth</h4> <p> Inspired by post-war developments like Levittown, these neighborhoods prioritize fast approvals and modular planning, balancing cost with liveability. </p> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-modular"> <div class="drawer-content"> <h3>Factory-Built Efficiency</h3> <p> Prefabricated housing shortens timelines and lowers costs. Homes are assembled in factories, then installed on-site—ideal for both suburban and urban infill. </p> <h4>Modular & Panel Systems</h4> <p> Components like walls and plumbing come prebuilt, allowing rapid assembly. Projects range from tiny homes to multi-storey apartment blocks and office conversions. </p> <h4>Digital Manufacturing</h4> <p> Modern factories use digital design, robotic cutting, and climate-controlled precision to reduce waste and labor dependency—especially useful in remote or high-demand areas. </p> </div> </sl-drawer> <!-- Wiring & Policy Logic --> <script> // Info-icon → drawer document.getElementById('info-affordable').addEventListener('click', () => { document.querySelector('.drawer-affordable').show(); }); document.getElementById('info-starter').addEventListener('click', () => { document.querySelector('.drawer-starter').show(); }); document.getElementById('info-modular').addEventListener('click', () => { document.querySelector('.drawer-modular').show(); }); // Drawer close buttons document.querySelectorAll('sl-drawer sl-button[slot="footer"]').forEach(btn => { btn.addEventListener('click', () => btn.closest('sl-drawer').hide()); }); // Policy registration functions function buildAffordable() { PolicyManager.register({ name: "Build City Affordable Housing", costUpfront: 10, duration: 999999, immediate(v) { v.publicOpinion -= 15; }, tick(v, age) { let cost = 8; if (age >= 2) cost = 0.42; if (age >= 5) cost = 0.12; if (age >= 7) cost = -0.5; v.budget -= cost; if (age >= 2) window.window.Util.exitPeople(100 + Math.floor(25 * (age - 2))); if (age >= 10 && age < 30) v.serviceCoordination += 2; } }); SugarCube.Engine.play("Affordable"); } function buildStarter() { PolicyManager.register({ name: "Starter Homes Expansion", costUpfront: 8, duration: 999999, immediate(v) { v.publicOpinion += 10; v.inflow -= 5; }, tick(v) { let cost = 5; if (v.age >= 3) cost = 1; if (v.age >= 6) cost = -0.25; v.budget -= cost; if (v.age >= 3) v.housingSupply += 150; } }); SugarCube.Engine.play("Starter"); } function buildModular() { PolicyManager.register({ name: "Modular Housing & Office Conversions", costUpfront: 6, duration: 999999, immediate(v) { v.publicOpinion += 5; v.systemCapacity += 10; }, tick(v, age) { let cost = 3; if (age >= 2) cost = 0.8; if (age >= 4) cost = 0.2; v.budget -= cost; if (age >= 2) window.window.Util.exitPeople(80 + Math.floor(15 * (age - 2))); if (age >= 6) v.systemCapacity += 3; } }); SugarCube.Engine.play("Modular"); } </script>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .button { background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); /* ✔ Correct border color */ border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; display: block; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; -webkit-user-select: none; touch-action: manipulation; width: 100%; max-width: 420px; margin: 0.9rem auto; position: relative; top: -0.25rem; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); /* ✔ Reinforce border on hover */ } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); /* ✔ Reinforce border on click */ } .button:focus { outline: none; box-shadow: 0 0 0 3px rgba(58, 122, 217, 0.5); border-color: rgb(44, 123, 202); /* ✔ Keep border consistent on focus */ } .button:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; border-color: #2a5a87; /* ✔ Still visible, but dimmed */ } .button:disabled:hover, .button:disabled:active { pointer-events: none; box-shadow: none; } .button:first-of-type { margin-top: 1rem; } </style> <!-- Intro Text --> <!-- Intro Text --> <div class="passage-content"> <div class="build-intro"> <p> As part of a <strong>COIL (Collaborative Online International Learning)</strong> initiative, students from York University, University of Ottawa, University of Guelph, Laurier, and Unisinos in Brazil have partnered with local organizations to develop community-based housing and homelessness solutions. Each project offers a different approach, grounded, collaborative, and designed for tangible real-world impact. You’ve been invited to support one. Which project do you choose to enact in your city? </p> <!-- Buttons with individual project policy activation --> <button class="button" onclick="project1();">In Person Fundraiser</button> <button class="button" onclick="project2();">Climate Crisis Backpack Kit </button> <button class="button" onclick="project3();">Farmer Product Connection</button> </div> <!-- Project Scripts --> <script> function project1() { SugarCube.Engine.play("C4Project1"); } function project2() { SugarCube.Engine.play("C4Project2"); } function project3() { SugarCube.Engine.play("C4Project3"); } </script> </div>
<<set $history = []>> <<set $turn = 1>> <<set $year= 1>> <<set $currentYear= 2024>> <<set $budget = 215>> <<set $homelessTotal = 2952>> <!-- Demographic breakdown of CURRENT homeless population --> <<set $pop = { // Housing status unsheltered: 472, // 16% of 2952 sheltered: 1269, // 43% transitional: 708, // 24% institutional: 59, // 2% chronic: 1446, // 49% withChildren: 295, // 10% fosterCareHistory: 561, // 19% // Identity demographics male: Math.round(2952 * 0.56), female: Math.round(2952 * 0.36), transNB: Math.round(2952 * 0.02), age25to49: Math.round(2952 * 0.58), youth: Math.round(2952 * 0.13), lgbq: Math.round(2952 * 0.11), lgbtqYouth: Math.round(2952 * 0.11 * 0.21), racialized: Math.round(2952 * 0.56), indigenous: 479, // exact number provided veteran: Math.round(2952 * 0.04), immigrant: Math.round(2952 * 0.42) }>> <!-- INFLOW system --> <<set $inflowBase = 80>> <!-- Number of new homeless per turn --> <!-- Percentage distribution of INCOMING new homelessness cases --> <<set $inflowShare = { // Housing status (default to unsheltered for new arrivals) unsheltered: 0.16, sheltered: 0.43, transitional: 0.24, institutional: 0.02, chronic: 0.49, withChildren: 0.10, fosterCareHistory: 0.19, // Identity demographics male: 0.56, female: 0.36, transNB: 0.02, lgbtqYouth: 0.21, age25to49: 0.58, lgbq: 0.11, racialized: 0.56, indigenous: 479 / 2952, // ~16.2% veteran: 0.04, immigrant: 0.42 }>> <!-- System + policy levers --> <<set $publicOpinion = 50>> <<set $serviceCoordination = 60>> <<set $systemCapacity = 100>> <<set $politicalCapital = 3>> <<set $unitsPreserved = 0>> <<script>> // Create an initial snapshot representing the state *before* year 1 begins. // This helps in comparing the first year's results against something. const v = State.variables; const initialPopSnapshot = {}; // Deep copy $pop for (const key in v.pop) { if (Object.prototype.hasOwnProperty.call(v.pop, key)) { initialPopSnapshot[key] = v.pop[key]; } } const initialSnapshot = { year: 0, // Conceptual "Year 0" or pre-game state turn: 0, // Conceptual "Turn 0" homelessTotal: v.homelessTotal, pop: initialPopSnapshot, budget: v.budget, publicOpinion: v.publicOpinion, serviceCoordination: v.serviceCoordination, systemCapacity: v.systemCapacity, politicalCapital: v.politicalCapital }; v.history.push(initialSnapshot); <</script>> <<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <<script>> // run on *every* time this passage finishes displaying $(document).on(':passagedisplay', function (ev) { // 1) inject the CSS if we haven't already if (!$('head').find('link[href*="swiper-bundle.min.css"]').length) { $('head').append( '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">' ); } // 2) helper to actually init the carousel function setupSwiper() { const el = ev.content.querySelector('.mySwiper'); if (!el) return; if (window.mySwiper) { window.mySwiper.destroy(true, true); } // ← here’s the only change: add observer + observeParents window.mySwiper = new Swiper(el, { effect: 'cards', grabCursor: true, observer: true, observeParents: true }); } // 3) load Swiper.js if needed, otherwise just init if (typeof Swiper === 'undefined') { $.getScript('https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js') .done(setupSwiper) .fail(() => console.error('Failed to load Swiper.')); } else { setupSwiper(); } }); <</script>> <div class="passageContent"> <!-- Demo styles --> <style> :root { zoom: 1.1; } .passageContent { margin-top: 0rem !important } html, body { position: relative; height: 100%; } body { background: #eee; font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 14px; color: #000; margin: 0; padding: 0; } body { background: #fff; font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 14px; color: #000; margin: 0; padding: 0; } html, body { position: relative; height: 100%; } body { display: flex; justify-content: center; align-items: center; } .swiper { width: 23rem !important; max-width: 900px; height: 40em !important; max-height: 1000px; } @media screen and (max-width: 600px) { .swiper { width: 42vw !important; } .swiper-slide { font-size: 0.9em; padding: 1rem 0.75rem; } } @media screen and (max-width: 979px) { .swiper { width: 38vw !important; margin-left: 2rem !important; } .button { margin-left: 2rem !important; } } .swiper-slide-shadow { background: rgba(0, 0, 0, 0.05) !important; } @media screen and (max-width: 979px) { .swiper { width: 38vw !important; margin-left: 2rem !important; } .button { margin-left: 2rem !important; } .swiper-slide { font-size: 0.85em; padding: 1rem 0.75rem; } .impacts { display: none; } } .swiper-slide { display: flex; align-items: center; justify-content: flex-start; padding: 20px; font-size: 14px; background-color: #fff; border-radius: 14.130435% / 10.483871%; border-color: rgba(156, 162, 166, 0.32); border-left-width: 1.5px; border-right-width: 1.5px; border-style: solid; border-top-width: 1.5px; border-bottom-width: 1.5px; } .swiper-slide { pointer-events: none; } .slider-wrapper { display: flex; flex-direction: column; align-items: center; /* center slider + button together */ margin: 2rem auto; /* vertical spacing + center wrapper in page */ } .slider-wrapper .swiper { margin: 0; /* reset any old margins */ margin-left: -1rem; /* <<–– shift the whole slider a bit left */ */ } .swiper-slide.swiper-slide-active { pointer-events: auto; border-color: rgba(156, 162, 166, 0.18); border-left-width: 1.5px; border-right-width: 1.5px; border-style: solid; border-top-width: 1.5px; border-bottom-width: 1.5px; } .break { display: block; content: ""; } .small-break { display: block; content: ""; } .bill-header { text-align: center; margin-bottom: 20px; line-height: 1.2; border-bottom: 1px solid #ccc; padding-bottom: 0.5em; margin-bottom: 1em; } .bill-header div { font-variant: small-caps; letter-spacing: 1px; font-size: 0.85em; } .bill-header .bill-number { font-size: 1.2em; margin-top: 6px; } .swiper-slide h2 { margin: 0 0 16px; font-size: 1.3em; text-align: center; font-variant: small-caps; letter-spacing: 0.5px; } .swiper-slide p { line-height: 1.5; font-size: 0.95em; flex: 1; } button { background-color: #ffffff !important; color: #111827 !important; border: 1px solid #e5e7eb !important; padding: 0.5rem 0.75rem !important; border-radius: 0.5rem !important; font-size: 0.875rem !important; font-weight: 500 !important; line-height: 1.25 !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 0.3rem !important; position: relative !important; top: -0.25rem !important; text-align: center !important; margin: 0.3rem auto; /* centers it */ position: relative; } .meta { text-align: right; font-size: 0.8em; color: #555; margin-top: 20px; font-family: Arial, sans-serif; } @font-face{font-family:swiper-icons;src:url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA');font-weight:400;font-style:normal}:root{--swiper-theme-color:#007aff}:host{position:relative;display:block;margin-left:auto;margin-right:auto;z-index:1}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;list-style:none;padding:0;z-index:1;display:block}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;transition-timing-function:var(--swiper-wrapper-transition-timing-function,initial);box-sizing:content-box}.swiper-android .swiper-slide,.swiper-ios .swiper-slide,.swiper-wrapper{transform:translate3d(0px,0,0)}.swiper-horizontal{touch-action:pan-y}.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform;display:block}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d{perspective:1200px}.swiper-3d .swiper-cube-shadow,.swiper-3d .swiper-slide{transform-style:preserve-3d}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-css-mode.swiper-horizontal>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-css-mode.swiper-vertical>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-css-mode.swiper-free-mode>.swiper-wrapper{scroll-snap-type:none}.swiper-css-mode.swiper-free-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:none}.swiper-css-mode.swiper-centered>.swiper-wrapper::before{content:'';flex-shrink:0;order:9999}.swiper-css-mode.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center;scroll-snap-stop:always}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper::before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper::before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-bottom,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color,var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader,.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color:#fff}.swiper-lazy-preloader-black{--swiper-preloader-color:#000}@keyframes swiper-preloader-spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.swiper-virtual .swiper-slide{-webkit-backface-visibility:hidden;transform:translateZ(0)}.swiper-virtual.swiper-css-mode .swiper-wrapper::after{content:'';position:absolute;left:0;top:0;pointer-events:none}.swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after{height:1px;width:var(--swiper-virtual-size)}.swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after{width:1px;height:var(--swiper-virtual-size)}:root{--swiper-navigation-size:44px}.swiper-button-next,.swiper-button-prev{position:absolute;top:var(--swiper-navigation-top-offset,50%);width:calc(var(--swiper-navigation-size)/ 44 * 27);height:var(--swiper-navigation-size);margin-top:calc(0px - (var(--swiper-navigation-size)/ 2));z-index:10;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--swiper-navigation-color,var(--swiper-theme-color))}.swiper-button-next.swiper-button-disabled,.swiper-button-prev.swiper-button-disabled{opacity:.35;cursor:auto;pointer-events:none}.swiper-button-next.swiper-button-hidden,.swiper-button-prev.swiper-button-hidden{opacity:0;cursor:auto;pointer-events:none}.swiper-navigation-disabled .swiper-button-next,.swiper-navigation-disabled .swiper-button-prev{display:none!important}.swiper-button-next svg,.swiper-button-prev svg{width:100%;height:100%;object-fit:contain;transform-origin:center}.swiper-rtl .swiper-button-next svg,.swiper-rtl .swiper-button-prev svg{transform:rotate(180deg)}.swiper-button-prev,.swiper-rtl .swiper-button-next{left:var(--swiper-navigation-sides-offset,10px);right:auto}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-lock{display:none}.swiper-button-next:after,.swiper-button-prev:after{font-family:swiper-icons;font-size:var(--swiper-navigation-size);text-transform:none!important;letter-spacing:0;font-variant:initial;line-height:1}.swiper-button-prev:after,.swiper-rtl .swiper-button-next:after{content:'prev'}.swiper-button-next,.swiper-rtl .swiper-button-prev{right:var(--swiper-navigation-sides-offset,10px);left:auto}.swiper-button-next:after,.swiper-rtl .swiper-button-prev:after{content:'next'}.swiper-pagination{position:absolute;text-align:center;transition:.3s opacity;transform:translate3d(0,0,0);z-index:10}.swiper-pagination.swiper-pagination-hidden{opacity:0}.swiper-pagination-disabled>.swiper-pagination,.swiper-pagination.swiper-pagination-disabled{display:none!important}.swiper-horizontal>.swiper-pagination-bullets,.swiper-pagination-bullets.swiper-pagination-horizontal,.swiper-pagination-custom,.swiper-pagination-fraction{bottom:var(--swiper-pagination-bottom,8px);top:var(--swiper-pagination-top,auto);left:0;width:100%}.swiper-pagination-bullets-dynamic{overflow:hidden;font-size:0}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transform:scale(.33);position:relative}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main{transform:scale(1)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev{transform:scale(.33)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next{transform:scale(.66)}.swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next{transform:scale(.33)}.swiper-pagination-bullet{width:var(--swiper-pagination-bullet-width,var(--swiper-pagination-bullet-size,8px));height:var(--swiper-pagination-bullet-height,var(--swiper-pagination-bullet-size,8px));display:inline-block;border-radius:var(--swiper-pagination-bullet-border-radius,50%);background:var(--swiper-pagination-bullet-inactive-color,#000);opacity:var(--swiper-pagination-bullet-inactive-opacity, .2)}button.swiper-pagination-bullet{border:none;margin:0;padding:0;box-shadow:none;-webkit-appearance:none;appearance:none}.swiper-pagination-clickable .swiper-pagination-bullet{cursor:pointer}.swiper-pagination-bullet:only-child{display:none!important}.swiper-pagination-bullet-active{opacity:var(--swiper-pagination-bullet-opacity, 1);background:var(--swiper-pagination-color,var(--swiper-theme-color))}.swiper-pagination-vertical.swiper-pagination-bullets,.swiper-vertical>.swiper-pagination-bullets{right:var(--swiper-pagination-right,8px);left:var(--swiper-pagination-left,auto);top:50%;transform:translate3d(0px,-50%,0)}.swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets .swiper-pagination-bullet{margin:var(--swiper-pagination-bullet-vertical-gap,6px) 0;display:block}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{top:50%;transform:translateY(-50%);width:8px}.swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-vertical>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{display:inline-block;transition:.2s transform,.2s top}.swiper-horizontal>.swiper-pagination-bullets .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet{margin:0 var(--swiper-pagination-bullet-horizontal-gap,4px)}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic{left:50%;transform:translateX(-50%);white-space:nowrap}.swiper-horizontal>.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet,.swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s left}.swiper-horizontal.swiper-rtl>.swiper-pagination-bullets-dynamic .swiper-pagination-bullet{transition:.2s transform,.2s right}.swiper-pagination-fraction{color:var(--swiper-pagination-fraction-color,inherit)}.swiper-pagination-progressbar{background:var(--swiper-pagination-progressbar-bg-color,rgba(0,0,0,.25));position:absolute}.swiper-pagination-progressbar .swiper-pagination-progressbar-fill{background:var(--swiper-pagination-color,var(--swiper-theme-color));position:absolute;left:0;top:0;width:100%;height:100%;transform:scale(0);transform-origin:left top}.swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill{transform-origin:right top}.swiper-horizontal>.swiper-pagination-progressbar,.swiper-pagination-progressbar.swiper-pagination-horizontal,.swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite,.swiper-vertical>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite{width:100%;height:var(--swiper-pagination-progressbar-size,4px);left:0;top:0}.swiper-horizontal>.swiper-pagination-progressbar.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite,.swiper-pagination-progressbar.swiper-pagination-vertical,.swiper-vertical>.swiper-pagination-progressbar{width:var(--swiper-pagination-progressbar-size,4px);height:100%;left:0;top:0}.swiper-pagination-lock{display:none}.swiper-scrollbar{border-radius:var(--swiper-scrollbar-border-radius,10px);position:relative;touch-action:none;background:var(--swiper-scrollbar-bg-color,rgba(0,0,0,.1))}.swiper-scrollbar-disabled>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-disabled{display:none!important}.swiper-horizontal>.swiper-scrollbar,.swiper-scrollbar.swiper-scrollbar-horizontal{position:absolute;left:var(--swiper-scrollbar-sides-offset,1%);bottom:var(--swiper-scrollbar-bottom,4px);top:var(--swiper-scrollbar-top,auto);z-index:50;height:var(--swiper-scrollbar-size,4px);width:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar.swiper-scrollbar-vertical,.swiper-vertical>.swiper-scrollbar{position:absolute;left:var(--swiper-scrollbar-left,auto);right:var(--swiper-scrollbar-right,4px);top:var(--swiper-scrollbar-sides-offset,1%);z-index:50;width:var(--swiper-scrollbar-size,4px);height:calc(100% - 2 * var(--swiper-scrollbar-sides-offset,1%))}.swiper-scrollbar-drag{height:100%;width:100%;position:relative;background:var(--swiper-scrollbar-drag-bg-color,rgba(0,0,0,.5));border-radius:var(--swiper-scrollbar-border-radius,10px);left:0;top:0}.swiper-scrollbar-cursor-drag{cursor:move}.swiper-scrollbar-lock{display:none}.swiper-zoom-container{width:100%;height:100%;display:flex;justify-content:center;align-items:center;text-align:center}.swiper-zoom-container>canvas,.swiper-zoom-container>img,.swiper-zoom-container>svg{max-width:100%;max-height:100%;object-fit:contain}.swiper-slide-zoomed{cursor:move;touch-action:none}.swiper .swiper-notification{position:absolute;left:0;top:0;pointer-events:none;opacity:0;z-index:-1000}.swiper-free-mode>.swiper-wrapper{transition-timing-function:ease-out;margin:0 auto}.swiper-grid>.swiper-wrapper{flex-wrap:wrap}.swiper-grid-column>.swiper-wrapper{flex-wrap:wrap;flex-direction:column}.swiper-fade.swiper-free-mode .swiper-slide{transition-timing-function:ease-out}.swiper-fade .swiper-slide{pointer-events:none;transition-property:opacity}.swiper-fade .swiper-slide .swiper-slide{pointer-events:none}.swiper-fade .swiper-slide-active{pointer-events:auto}.swiper-fade .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper.swiper-cube{overflow:visible}.swiper-cube .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1;visibility:hidden;transform-origin:0 0;width:100%;height:100%}.swiper-cube .swiper-slide .swiper-slide{pointer-events:none}.swiper-cube.swiper-rtl .swiper-slide{transform-origin:100% 0}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-cube .swiper-slide-active,.swiper-cube .swiper-slide-next,.swiper-cube .swiper-slide-prev{pointer-events:auto;visibility:visible}.swiper-cube .swiper-cube-shadow{position:absolute;left:0;bottom:0px;width:100%;height:100%;opacity:.6;z-index:0}.swiper-cube .swiper-cube-shadow:before{content:'';background:#000;position:absolute;left:0;top:0;bottom:0;right:0;filter:blur(50px)}.swiper-cube .swiper-slide-next+.swiper-slide{pointer-events:auto;visibility:visible}.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-bottom,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-left,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-right,.swiper-cube .swiper-slide-shadow-cube.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper.swiper-flip{overflow:visible}.swiper-flip .swiper-slide{pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden;z-index:1}.swiper-flip .swiper-slide .swiper-slide{pointer-events:none}.swiper-flip .swiper-slide-active,.swiper-flip .swiper-slide-active .swiper-slide-active{pointer-events:auto}.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-bottom,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-left,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-right,.swiper-flip .swiper-slide-shadow-flip.swiper-slide-shadow-top{z-index:0;-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-creative .swiper-slide{-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden;transition-property:transform,opacity,height}.swiper.swiper-cards{overflow:visible}.swiper-cards .swiper-slide{transform-origin:center bottom;-webkit-backface-visibility:hidden;backface-visibility:hidden;overflow:hidden} </style> <div class="slider-wrapper"> <div class="swiper mySwiper"> <div class="swiper-wrapper"> <div class="swiper-slide"> <div class="bill-header"> <div>City of Ottawa</div> <div>February 5<sup>th</sup>, 2025</div> <div> ACS2025-OCC-CCS-0002 </div> <div class="bill-number"> Councillor Jeff Leiper </div> </div> <h2>Motion: Shelter Use in all Zones</h2> <p> Whereas the Official Plan calls for shelters in all urban zones; and,<br /> Whereas outdated zoning still blocks shelters in many areas; and,<br /> Whereas delays risk missing new funding and worsen the housing crisis;<br /><br /> </p> <p> <em>Therefore be it resolved that</em> shelters be permitted in all urban zones immediately, using the current definition in By-law 2008-250 until updated.<br /><br /> </p> <div class="impacts"> <p>Should this motion pass, the City anticipates the following impacts:</p> <ul> <li><p>Increased system capacity</p></li> <li><p>Decreased unsheltered inflow</p></li> <li><p>Enabling access to time-sensitive federal/provincial funding</p></li> </ul> </div> <div class="meta"> Planning and Housing Committee </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>City of Ottawa</div> <div>ACS2024-OCC-CCS-0045</div> <div class="bill-number">Councillor Ariel Troster</div> </div> <h2>Motion: Renovictions</h2> <p> Whereas renovictions are displacing tenants and eroding affordable housing, and the City must act in the absence of sufficient provincial enforcement; and,<br /> Whereas the City of Hamilton has adopted Ontario’s first anti-renoviction by-law with licensing and enforcement measures;<br /><br /> </p> <p> <em>Therefore be it resolved that</em> landlords are required to obtain a renovation license, prove necessity for vacant possession, and provide relocation assistance for affected tenants. </p> <div class="impacts"> <p>Should this motion pass, the City anticipates the following impacts:</p> <ul> <li><p>Reduced eviction-driven inflow</p></li> <li><p>Preservation of existing affordable units</p></li> </ul> </div> <div class="meta"> Planning and Housing Committee </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>City of Ottawa</div> <div>November 20<sup>th</sup>, 2024</div> <div> ACS2024- OCC-CCS- 0035</div> <div class="bill-number">Councillor Laine Johnson</div> </div> <h2>Motion: Affordable Housing Acquisition Fund</h2> <p> Whereas the City is losing affordable housing faster than it can build new supply; and,<br /> Whereas acquisition funds in cities like Toronto and Montreal have helped preserve affordable units through non-profit ownership; and,<br /> Whereas relying solely on new construction risks missing opportunities to protect existing stock;<br /><br /> </p> <p> <em>Therefore be it resolved that</em> the City allocate surplus revenues from the Vacant Unit Tax to establish an acquisition fund for the preservation of at-risk affordable housing. </p> <div class="impacts"> <p>Should this motion pass, the City anticipates the ability to further protect and maintain existing affordable units.</p> </div> <div class="meta"> Planning and Housing Committee </div> </div> <div class="swiper-slide"> <div class="bill-header"> <div>City of Ottawa</div> <div>March 2024</div> <div>Motion No. 2024-32-11</div> <div class="bill-number">Councillor Ariel Troster</div> </div> <h2>Motion: Leasing Office Space for Transitional Housing</h2> <p> Whereas unprecedented shelter demand has forced the use of recreation centres as overflow sites, which must return to public use; and,<br /> Whereas commercial properties present an opportunity to rapidly expand transitional housing capacity; </p> <p> <em>Therefore be it resolved that</em> Council approve long-term leases to convert vacant office space into transitional housing, with capital support for renovations. </p> <div class="impacts"> <p>Should this motion pass, the City anticipates the following impacts:</p> <ul> <li><p>Increased transitional housing capacity</p></li> <li><p>Reduced reliance on emergency overflow centres</p></li> <li><p>More dignified interim housing for shelter clients</p></li> </ul> </div> <div class="meta"> Emergency Shelter Crisis Taskforce </div> </div> </div> <div style="display: flex; flex-direction: column; align-items: center; margin-top: 2em;"> <<button "Adopt">> <<run (()=>{ const map = [ "ShelterZones", "Renovictions", "AcquisitionFund", "LeaseSpace" ]; // get the active slide index const idx = window.mySwiper.activeIndex; // play the corresponding passage Engine.play(map[idx]); })()>> <</button>> </div> </div> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .drawer-content { padding: 10px; padding-left: 25%; padding-right: 25%; } .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .info-btn { background: none; border: none; padding: 0; margin-left: 0.5rem; cursor: pointer; vertical-align: middle; } .info-btn img { width: 1.2rem; height: 1.2rem; opacity: 0.7; transition: opacity 0.2s ease; } .info-btn:hover img { opacity: 1; } .button { flex: 1; background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); } </style> <div class="passage-content"> <div class="build-intro"> <p> With PIT results in hand, your city must now decide on a major strategic investment. Each option shapes the long-term capacity and focus of your homelessness response. What direction do you choose? </p> <!-- Supportive Housing Delivery Office --> <div style="margin-bottom:1rem; text-align:center;"> <button class="button" onclick="launchSupportiveOffice();"> Create a permanent Supportive Housing Delivery Office </button> <button class="info-btn" id="info-supportive"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info"> </button> </div> <!-- Housing & Homelessness Data Lab --> <div style="margin-bottom:1rem; text-align:center;"> <button class="button" onclick="launchDataLab();"> Launch a Housing & Homelessness Data Lab </button> <button class="info-btn" id="info-datalab"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info"> </button> </div> <!-- Encampment Response Task Force --> <div style="margin-bottom:1rem; text-align:center;"> <button class="button" onclick="launchEncampmentTaskForce();"> Form a Multi-Agency Encampment Response Task Force </button> <button class="info-btn" id="info-encampment"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info"> </button> </div> </div> </div> <!-- Drawers --> <sl-drawer placement="bottom" label=" " class="drawer-supportive"> <div class="drawer-content"> <h3>Supportive Housing Coordination in Canada</h3> <p> Since 2018, Toronto’s <strong>Housing Secretariat</strong> has coordinated over 1,200 permanent supportive units, partnering with CMHA and Indigenous agencies.<br> In Vancouver, BC’s <strong>Provincial Supportive Housing Tax Credit</strong> encouraged non-profits to build 800+ supportive homes by 2022.<br> Edmonton’s <strong>Homeward Trust</strong> runs a by-name list and case-management pipeline—key best-practice models that your office can emulate. </p> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-datalab"> <div class="drawer-content"> <h3>Municipal Data Hubs in Canada</h3> <p> Edmonton pioneered real-time tracking with its By-Name List, reducing chronic homelessness by 30% since 2017.<br> Calgary’s <strong>Homeless Hub</strong> publishes monthly dashboards showing inflow, length of shelter stay, and service gaps.<br> The CMHC’s 2023 <em>National Housing Data Lab</em> grant program offers funding to standardize metrics across provinces. </p> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-encampment"> <div class="drawer-content"> <h3>Encampment Responses Across Canada</h3> <p> Vancouver’s controversial 2021 encampment sweeps drove calls for a <strong>Safe Encampment Strategy</strong>, blending outreach and designated sites.<br> Montreal’s <strong>Social Safety Zones</strong> provide warming centers in winter and cooling sites in summer, balancing enforcement with services.<br> The Region of Waterloo’s 2024 by-law includes mandatory transition plans—offering lessons on lean, humane task forces. </p> </div> </sl-drawer> <!-- JS Wiring & Policy Logic --> <<script>> // Advance the turn window.PolicyManager.tick(); // INFO buttons (delegated—fires even if #info-supportive isn't in DOM yet) $(document).on('click', '#info-supportive', () => { document.querySelector('.drawer-supportive').show(); }); $(document).on('click', '#info-datalab', () => { document.querySelector('.drawer-datalab').show(); }); $(document).on('click', '#info-encampment', () => { document.querySelector('.drawer-encampment').show(); }); // Policy functions (now safely declared) window.launchSupportiveOffice = function() { PolicyManager.register({ name: "Supportive Housing Delivery Office", costUpfront: 5, costPerTurn: 0.3, duration: 999999, immediate(v) { v.publicOpinion += 2; if (v.serviceCoordination < 90) v.serviceCoordination += 10; }, tick(v, age) { if (v.pop.chronic > 0) { const exits = Math.min(v.pop.chronic, 150); window.Util.exitPeople(exits, { chronic: 0.7, institutional: 0.2, unsheltered: 0.1 }); } else if (v.homelessTotal > 0 && age > 4) { window.Util.exitPeople(20); } if (age > 0 && age % 4 === 0 && v.serviceCoordination < 90) { v.serviceCoordination += 2; } } }); SugarCube.Engine.play("SupportiveOffice"); }; window.launchDataLab = function() { PolicyManager.register({ name: "Housing & Homelessness Data Lab", costUpfront: 2, costPerTurn: 0.125, duration: 999999, immediate(v) { v.publicOpinion += 1; if (v.serviceCoordination < 95) v.serviceCoordination += 5; }, tick(v, age) { if (age >= 4) { window.Util.exitPeople(15, { chronic: 0.3, transitional: 0.3, withChildren: 0.2, youth: 0.2 }); if (age % 4 === 0 && v.serviceCoordination < 85) v.serviceCoordination += 3; if (age % 8 === 0) v.budget += 0.1; } } }); SugarCube.Engine.play("DataLab"); }; window.launchEncampmentTaskForce = function() { PolicyManager.register({ name: "Encampment Response Task Force", costUpfront: 2, costPerTurn: 0.25, duration: 999999, immediate(v) { v.publicOpinion += 10; const reduction = Math.round(v.pop.unsheltered * 0.10); v.pop.unsheltered -= reduction; v.pop.sheltered += Math.round(reduction * 0.7); v.inflowShare.unsheltered += 0.01; v.inflowShare.chronic += 0.01; window.Util.renorm(v.inflowShare); if (v.serviceCoordination > 35) v.serviceCoordination -= 5; }, tick(v, age) { if (age > 0 && age % 2 === 0 && v.pop.unsheltered > 20) { let moved = Math.min(v.pop.unsheltered, 15); v.pop.unsheltered -= moved; v.pop.sheltered += moved; if (v.publicOpinion < 70) v.publicOpinion += 1; } if (age > 0 && age % 4 === 0 && v.serviceCoordination > 30) { v.serviceCoordination -= 1; } } }); SugarCube.Engine.play("EncampmentTaskForce"); }; <</script>>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Policy Effectiveness</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">20<span class="impact-subtitle"> %</span></span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Annual Lab Budget</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">0.5<span class="impact-subtitle"> M / year</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Data-Driven Placements</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">20<span class="impact-subtitle"> %</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em"> Your new Data Lab aggregates shelter, hospital, and outreach data in real time—surfacing service gaps, equity blind spots, and predictive risk indicators. It enables evidence-based decision-making for all homelessness programs. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('SnowStorm');">Continue</button> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <div class="passageContent"> <h2>Fund Local Services</h2> <p>You have <strong>$<<= $budget >>M</strong> to distribute among the following Ottawa initiatives:</p> <div> <label for="salvation">The Salvation Army</label> <input type="range" id="salvation" min="0" max="100" step="1" value="0"> <span id="salvation-val">0</span>M </div> <div> <label for="shepherd">Good Shepherd Ministries</label> <input type="range" id="shepherd" min="0" max="100" step="1" value="0"> <span id="shepherd-val">0</span>M </div> <div> <label for="felix">St. Felix Centre</label> <input type="range" id="felix" min="0" max="100" step="1" value="0"> <span id="felix-val">0</span>M </div> <hr> <p><strong>Total Allocated:</strong> <span id="total">0</span>M of $<<= $budget >>M</p> <p id="warning" style="color: red; display: none;">⚠️ You've reached your funding limit.</p> <p>* </p> </div> <<script>> $(document).on(':passagedisplay', function () { setTimeout(() => { const maxBudget = State.variables.budget; const sliders = ['salvation', 'shepherd', 'felix']; function updateFunding() { let total = 0; sliders.forEach(id => { const val = parseInt(document.getElementById(id).value, 10); total += val; }); // Enforce budget cap if (total > maxBudget) { document.getElementById('warning').style.display = 'block'; const active = document.activeElement; if (sliders.includes(active.id)) { const overage = total - maxBudget; const curVal = parseInt(active.value, 10); const newVal = curVal - overage; active.value = Math.max(newVal, 0); total -= overage; } } else { document.getElementById('warning').style.display = 'none'; } // Update visuals sliders.forEach(id => { document.getElementById(id + '-val').textContent = document.getElementById(id).value; }); document.getElementById('total').textContent = total; } sliders.forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', updateFunding); } }); }, 0); }); <</script>>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Unsheltered Population</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">10<span class="impact-subtitle"> %</span></span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">15<span class="impact-subtitle"> pts</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Enforcement Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">1<span class="impact-subtitle"> M</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em"> The Task Force has deployed outreach teams alongside designated sweeps—reducing visible encampments and winning a short-term boost in public satisfaction. It forces some into shelters but increases long-term and enforcement costs. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('SnowStorm');">Continue</button> </div>
<<set $finalScore = 823>> <<set $insights = [ "Your city reduced homeless inflow by 34% over 10 years.", "You met 5 of 8 targeted SDGs, including Clean Water, Affordable Housing, and Reduced Inequalities.", "Homeless population dropped from 8,500 to 2,800.", "Public trust in government rose after early setbacks.", "Infrastructure investments helped unlock rapid housing construction." ]>> <<set $leaderboard = [ { rank: 1, name: "Quinn", score: 923 }, { rank: 2, name: "Natasha", score: 894 }, { rank: 3, name: "YOU", score: $finalScore }, { rank: 4, name: "zzz", score: 820 }, { rank: 5, name: "yyy", score: 791 } ]>> <div class="endgame-screen"> <div class="your-score"> <div class="score-label">FINAL SCORE</div> <div class="score-value"><<=$finalScore>></div> </div> <div class="insights"> <div class="section-title">Game Highlights</div> <ul> <<for _insight range $insights>> <li><<= _insight >></li> <</for>> </ul> </div> <div class="leaderboard"> <div class="section-title">Leaderboard</div> <table> <thead> <tr><th>Rank</th><th>City</th><th>Score</th></tr> </thead> <tbody> <<for _entry range $leaderboard>> <tr> <td><<= _entry.rank >></td> <td><<= _entry.name >></td> <td><<= _entry.score >></td> </tr> <</for>> </tbody> </table> </div> <button class="button" onclick="SugarCube.Engine.play('MainMenu')">Back to Menu</button> </div> <style> /* Container */ .endgame-screen { max-width: 800px; margin: 5vh auto; padding: 2rem; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; box-sizing: border-box; font-family: "Inter", sans-serif; } /* Sections */ .your-score, .insights, .leaderboard { margin-bottom: 2rem; } /* Score */ .score-label { font-family: "Inter", sans-serif; font-weight: 400; font-size: 1.25em; } .score-value { font-family: "Spectral", serif; font-weight: 600; font-size: 4em; color: #0066cc; text-align: center; } /* Section titles */ .section-title { font-family: "Inter", sans-serif; font-weight: 400; font-size: 1.1em; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px dotted rgba(0,0,0,0.8); padding-bottom: 0.25em; } /* Insights list */ .insights ul { list-style-type: disc; padding-left: 1.5em; font-family: "Inter", sans-serif; line-height: 1.4; } /* Leaderboard table */ .leaderboard table { width: 100%; border-collapse: collapse; font-family: "Inter", sans-serif; } .leaderboard th, .leaderboard td { padding: 0.75em; text-align: left; border-bottom: 1px solid #e2e8f0; } .leaderboard th { background: #f9fafb; font-weight: 600; } /* “Back to Menu” button reuses your .button style */ </style>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <<script>> if (PolicyManager.has("Build City Affordable Housing (Mid-Rise Densification)")) { State.variables.bchChoice = "affordable"; } else if (PolicyManager.has("Starter Homes Expansion (Suburban)")) { State.variables.bchChoice = "starter"; } else if (PolicyManager.has("Modular Housing & Office Conversions")) { State.variables.bchChoice = "modular"; } <</script>> <style> @import url('https://fonts.googleapis.com/css2?family=Gochi+Hand&family=Homemade+Apple&family=Nothing+You+Could+Do&display=swap'); .font-gochi { font-family: 'Gochi Hand', cursive; } .font-homemade { font-family: 'Homemade Apple', cursive; } .font-nothing { font-family: 'Nothing You Could Do', cursive; } .break { content: ""; display: block; height: 1px; } .container { max-width: 950px; margin: 0 auto; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); } /* Base airmail letter container */ .airmail-letter { position: relative; padding: 40px; background-color: #fff; box-sizing: border-box; width: 100%; min-height: 300px; border: 15px solid transparent; background-clip: padding-box; font-family: "Courier New", serif; margin: 20px; color: #333; box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.05); } /* The striped border */ .airmail-letter::before { content: ""; position: absolute; top: -15px; left: -15px; right: -15px; bottom: -15px; z-index: -1; background: repeating-linear-gradient( 45deg, #e53935, /* red */ #e53935 10px, white 10px, white 20px, #1e88e5, /* blue */ #1e88e5 30px, white 30px, white 40px ); } /* Paper texture overlay */ .airmail-letter::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z' fill='%23000000' fill-opacity='0.02' fill-rule='evenodd'/%3E%3C/svg%3E"); pointer-events: none; z-index: 1; opacity: 0.8; } /* Letter header with postmark and stamp */ .letter-header { display: flex; justify-content: space-between; margin-bottom: 30px; border-bottom: 1px solid #ddd; padding-bottom: 15px; position: relative; z-index: 2; } /* Circular postmark */ .postmark { width: 100px; height: 100px; background-color: rgba(0, 0, 0, 0.05); border-radius: 50%; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; } .postmark::before { content: "AIRMAIL"; position: absolute; font-weight: bold; font-size: 12px; transform: rotate(-30deg); } .postmark::after { content: ""; position: absolute; width: 80px; height: 1px; background-color: #333; transform: rotate(-30deg); } /* Postage stamp */ .stamp { width: 50px; height: 70px; background: linear-gradient(45deg, #f5f5f5, #e0e0e0); border: 1px solid #ccc; position: relative; display: flex; align-items: center; justify-content: center; font-size: 14px; text-align: center; overflow: hidden; margin-left: 20px; } .stamp::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: repeating-linear-gradient( transparent, transparent 3px, rgba(0, 0, 0, 0.05) 3px, rgba(0, 0, 0, 0.05) 4px ); } /* Letter content styling */ .letter-content { line-height: 1.6; text-align: justify; position: relative; z-index: 2; } .letter-date { text-align: right; margin-bottom: 20px; font-style: italic; } .greeting { margin-bottom: 20px; } .signature { margin-top: 40px; text-align: right; font-style: italic; } /* Media queries for responsive design */ @media screen and (max-width: 768px) { .airmail-letter { padding: 30px; border-width: 10px; } .airmail-letter::before { top: -10px; left: -10px; right: -10px; bottom: -10px; background: repeating-linear-gradient( 45deg, #e53935, #e53935 8px, white 8px, white 16px, #1e88e5, #1e88e5 24px, white 24px, white 32px ); } .letter-header { flex-direction: column; align-items: flex-end; } .postmark { width: 80px; height: 80px; margin-bottom: 10px; } } @media screen and (max-width: 480px) { .airmail-letter { padding: 20px; border-width: 8px; } .airmail-letter::before { top: -8px; left: -8px; right: -8px; bottom: -8px; background: repeating-linear-gradient( 45deg, #e53935, #e53935 6px, white 6px, white 12px, #1e88e5, #1e88e5 18px, white 18px, white 24px ); } .postmark { width: 60px; height: 60px; } .stamp { width: 40px; height: 50px; } } .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .button { background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); /* ✔ Correct border color */ border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; display: block; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; -webkit-user-select: none; touch-action: manipulation; width: 100%; max-width: 420px; margin: 0.9rem auto; position: relative; top: -0.25rem; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); /* ✔ Reinforce border on hover */ } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); /* ✔ Reinforce border on click */ } .button:focus { outline: none; box-shadow: 0 0 0 3px rgba(58, 122, 217, 0.5); border-color: rgb(44, 123, 202); /* ✔ Keep border consistent on focus */ } .button:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; border-color: #2a5a87; /* ✔ Still visible, but dimmed */ } .button:disabled:hover, .button:disabled:active { pointer-events: none; box-shadow: none; } .button:first-of-type { margin-top: 1rem; } </style> <div class="container"> <div class="airmail-letter"> <div class="letter-header"> <div class="postmark"></div> <div class="stamp">AIRMAIL</div> </div> <<if $bchChoice is "affordable">> <div class="letter-content font-homemade"> <div class="letter-date">April 9, 2025</div> <div class="greeting">To Whom It May Concern,</div> <p> We, the residents of Old Town, are writing to express our deep concern regarding the recent apartment development plans announced in our neighbourhood. These buildings are far too large for our quiet streets and threaten to change the character of our community. </p> <p> We understand the need for housing, but this kind of density will bring traffic, noise, and infrastructure strain. We urge you to reconsider or at least consult us meaningfully before moving forward. </p> <div class="signature">Respectfully,<div class="break"> </div>The Old Town Residents’ Association</div> </div> <</if>> <<if $bchChoice is "starter">> <div class="letter-content font-nothing"> <div class="letter-date">April 9, 2025</div> <div class="greeting">To Whom It May Concern,</div> <p> We are concerned about the continued expansion into the suburbs at the cost of our environment and future generations. Car-dependent sprawl is not the answer to the housing crisis, it only deepens our reliance on fossil fuels and fragments communities. </p> <p> We urge your office to prioritize density, transit, and sustainability, not 1980s-style subdivisions. </p> <div class="signature">In Solidarity,<div class="break"> </div>Green Growth Coalition</div> </div> <</if>> <<if $bchChoice is "modular">> <div class="letter-content font-gochi"> <div class="letter-date">April 9, 2025</div> <div class="greeting">To Whom It May Concern,</div> <p> We appreciate the urgency to build, but the prefabricated buildings being built in our area are a step backwards. Their one size fits all appearance is boxy, out of place, and lacking in dignity. Their mere existence is deteriorating our property values. Is this really what our community deserves? </p> <p> Please don’t sacrifice quality in the name of speed. Let’s work together on something that fits into the character of our neighbourhood. </p> <div class="signature">Sincerely,<div class="break"> </div>Concerned Neighbours of Cedar Lane</div> </div> <</if>> <button class="button" onclick="SugarCube.Engine.play('ChooseProjectProposal1');">Continue</button> </div> </div>
<div id="gantt"></div> <<script>> (function () { // 1) inject CSS & base styling (unchanged) if (!document.querySelector('link[href*="frappe-gantt"]')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.css'; document.head.appendChild(link); } if (!document.getElementById('GanttStyle')) { const style = document.createElement('style'); style.id = 'GanttStyle'; style.textContent = ` #gantt { width: 100%; height: auto; /* let the library size it */ margin: 1em 0; } `; document.head.appendChild(style); } let currentGantt = null; setTimeout(() => { const ganttEl = document.getElementById('gantt'); if (!ganttEl) return; const renderGantt = () => { const MS_DAY = 86400000; const fmt = d => d.toISOString().slice(0,10); const placeholder = [ { id: '1', name: 'Alpha', duration: 60, progress: 10 }, { id: '2', name: 'Beta', duration: 45, progress: 0 }, { id: '3', name: 'Gamma', duration: 30, progress: 50 }, { id: '4', name: 'Delta', duration: 75, progress: 25 }, { id: '5', name: 'El', duration: 75, progress: 25 }, { id: '6', name: 'El', duration: 75, progress: 25 }, { id: '7', name: 'El', duration: 75, progress: 25 }, ]; let cursor = new Date(); cursor.setHours(0,0,0,0); const tasks = placeholder.map(p => { const start = new Date(cursor); const end = new Date(start.getTime() + p.duration * MS_DAY); cursor = new Date(end.getTime() + MS_DAY); return { id: p.id, name: p.name, start: fmt(start), end: fmt(end), progress: p.progress }; }); // 2) clean up old chart if (currentGantt && typeof currentGantt.unmount === 'function') { currentGantt.unmount(); } // 3) compute a container height: e.g. (rows × (bar+padding)) + header const rowHeight = 60; // bar_height + padding const padding = 40; const headerHeight = 100; // upper + lower header const chartHeight = placeholder.length * rowHeight + headerHeight; try { currentGantt = new Gantt('#gantt', tasks, { view_mode: 'Year', date_format: 'YYYY-MM-DD', // ↑ make each bar taller bar_height: 30, // px of the bar itself padding: 10, // px of space around each bar // ↑ force the svg container to this px height container_height: chartHeight, // keep the left/right ends tight infinite_padding: false, scroll_to: 'start', popup_on: 'hover', }); } catch (err) { console.error("⚠️ Gantt rendering failed:", err); } }; if (window.Gantt) { renderGantt(); } else { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/frappe-gantt/dist/frappe-gantt.umd.js'; s.onload = renderGantt; document.head.appendChild(s); } }, 0); })(); <</script>>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <canvas id="sunCanvas"></canvas> <style> /* Highlight on focus/hover */ html[data-outlines] input:focus, html[data-outlines] select:focus, html[data-outlines] textarea:focus, input:hover, select:hover, textarea:hover { background-color: rgba(255,166,0,0.5) !important; } /* Base pastel background */ body, html { background: #fff6dc !important; transition: background 0.8s linear; } /* Sun canvas */ canvas#sunCanvas { position: fixed; top: 0; left: 0; width: 100vw; height: 220px; z-index: 0; pointer-events: none; background: transparent; } /* Timeline bar */ #timeline { background: #fff6dc !important; } /* Button */ .button { background-color: rgb(255,183,44); background-image: linear-gradient(90deg,#ffc14d,#ffb94d,#ff9336); border: 1px solid #ffbb2a; border-radius: 12px; color: #fff; padding: .65rem 1rem; font-size: 1rem; display: block; margin: .9rem auto; cursor: pointer; max-width: 420px; } .button:hover { background-image: linear-gradient(90deg,#ffd17a,#ffc05e,#ffb300); border-color: #ffbb2a; } /* Checkbox */ label.checkbox { display: flex; align-items: center; margin: .75em 0; cursor: pointer; user-select: none; } label.checkbox input { -webkit-appearance: none; appearance: none; margin: 0; width: 1.2em; height: 1.2em; border: 2px solid #e0ddca; border-radius: .25rem; transition: background-color .2s, border-color .2s; display: inline-block; outline: none; position: relative; } label.checkbox input:checked { background-color: #ffba2b; border-color: #ffba2b; } label.checkbox input:focus { border-color: #ffba2b !important; box-shadow: 0 0 0 2px #ffeab7; } label.checkbox input:checked::after { content: ""; position: absolute; top: .15em; left: .35em; width: .25em; height: .6em; border: solid white; border-width: 0 .2em .2em 0; transform: rotate(45deg); } label.checkbox span { margin-left: .5em; font-size: 1em; color: #6b3700; } /* Impact boxes */ .impact-container { display: flex; justify-content: space-around; margin-top: 1.5rem; } .impact-box { background-color: rgba(255,255,255,0.2); padding: 1rem; border-radius: 8px; text-align: center; min-width: 110px; } .impact-title { font-weight: bold; margin-bottom: .5rem; } .impact-content { font-size: 1.5rem; } #balance-meter .balance .dollar-sign { color: #FDA121 !important; } .balance-meter .balance .bar-fill, .balance-meter .circle.filled, #balance-meter .balance .unit, #balance-meter .balance .dollar-sign, \::selection { color: #FDA121 !important; } ::selection { color: #FDA121 !important; } .impact-unit { font-weight: 600 !important; color: #FDA121 !important; } /* No-movement placeholder for tick */ @keyframes tick { from {} to {} } .animate-number { display: inline-block; } </style> <div class="policy-impact-screen" id="heatMetrics"> <div class="impact-container"> <div class="impact-box"> <div class="impact-title">Lives at Risk Averted</div> <div class="impact-content"> <span class="impact-value" id="metric-lives">0</span> <span class="impact-subtitle"> people</span> </div> </div> <div class="impact-box"> <div class="impact-title">System Strain</div> <div class="impact-content"> <span class="impact-unit">% </span> <span class="impact-value" id="metric-strain">0</span> </div> </div> <div class="impact-box"> <div class="impact-title">Vulnerable Reach</div> <div class="impact-content"> <span class="impact-unit">% </span> <span class="impact-value" id="metric-reach">0</span> </div> </div> </div> <div class="question-wrapper"> <p>A heatwave is bearing down—what emergency measures will you implement?</p> <form id="heatChoices"> <label class="checkbox"> <input type="checkbox" id="heat-opt1" /> <span>Open libraries and community centres as cooling shelters with extended hours</span> </label> <label class="checkbox"> <input type="checkbox" id="heat-opt2" /> <span>Deploy mobile cooling stations to high-risk areas</span> </label> <label class="checkbox"> <input type="checkbox" id="heat-opt3" /> <span>Provide portable fans and ice packs</span> </label> <label class="checkbox"> <input type="checkbox" id="heat-opt4" /> <span>Subsidize window.Utility bills to support increased air-conditioning use</span> </label> <label class="checkbox"> <input type="checkbox" id="heat-opt5" /> <span>Implement a citywide SMS heat alert and health-tip broadcast</span> </label> </form> </div> <div id="warning" style="display: none; color: #b91c1c; margin-top: 1em"> ⚠️ You must choose at least one response before continuing. </div> <button class="button" onclick="handleContinue()">Continue</button> </div> <<script>> // Sun glow & background animation setTimeout(function(){ const canvas = document.getElementById('sunCanvas'), ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = 220; const cx = canvas.width/2, cy = 110, baseR = 68; let t = 0; function pastel(h) { return `hsl(${h},92%,94%)`; } function drawSun(time){ const hue = 45 + Math.sin(time/2400)*10; ctx.clearRect(0,0,canvas.width,canvas.height); // Glow const grad = ctx.createRadialGradient(cx,cy,15,cx,cy,baseR*1.8); grad.addColorStop(0, `hsl(${hue},98%,60%)`); grad.addColorStop(0.15,`hsl(${hue},98%,80%)`); grad.addColorStop(0.5, `hsla(${hue},98%,70%,.22)`); grad.addColorStop(1, `hsla(${hue},98%,97%,0)`); ctx.beginPath(); ctx.arc(cx,cy,baseR*1.6,0,2*Math.PI); ctx.fillStyle = grad; ctx.fill(); // Core ctx.beginPath(); ctx.arc(cx,cy,baseR,0,2*Math.PI); ctx.fillStyle = `hsl(${hue},98%,56%)`; ctx.shadowColor = `hsl(${hue},98%,70%)`; ctx.shadowBlur = 40; ctx.fill(); ctx.shadowBlur = 0; // BG document.body.style.background = pastel(hue); const tl = document.getElementById('timeline'); if(tl) tl.style.background = pastel(hue); } function animateFrame(){ t += 16; drawSun(t); requestAnimationFrame(animateFrame); } animateFrame(); window.addEventListener('resize', ()=> canvas.width = window.innerWidth); // Number-tick helper function animateNumber(el, start, end){ if(start===end){ el.textContent = end; return; } let cur = start; const step = end>start?1:-1, dur = 500, iv = Math.max(Math.floor(dur/Math.abs(end-start)),16); el.classList.add('animate-number'); const timer = setInterval(()=>{ cur += step; el.textContent = cur; if(cur===end){ clearInterval(timer); el.classList.remove('animate-number'); } }, iv); } // Heatwave impacts const heatImpacts = { "heat-opt1": { lives: 60, strain: 35, reach: 45 }, "heat-opt2": { lives: 50, strain: 35, reach: 55 }, "heat-opt3": { lives: 25, strain: 20, reach: 35 }, "heat-opt4": { lives: 30, strain: 15, reach: 30 }, "heat-opt5": { lives: 10, strain: 5, reach: 30 } }; function updateHeatMetrics(){ const eL = document.getElementById('metric-lives'), eS = document.getElementById('metric-strain'), eR = document.getElementById('metric-reach'); const pL = parseInt(eL.textContent,10), pS = parseInt(eS.textContent,10), pR = parseInt(eR.textContent,10); let tL=0, tS=0, tR=0; Object.entries(heatImpacts).forEach(([id,imp])=>{ if(document.getElementById(id).checked){ tL += imp.lives; tS += imp.strain; tR += imp.reach; } }); tS = Math.min(tS,100); tR = Math.min(Math.max(tR,0),100); animateNumber(eL,pL,tL); animateNumber(eS,pS,tS); animateNumber(eR,pR,tR); document.querySelector('#metric-strain').nextElementSibling.textContent = ' %'; document.querySelector('#metric-reach').nextElementSibling.textContent = ' %'; } // Hook checkbox changes document.querySelectorAll('#heatChoices input[type=checkbox]').forEach(cb=>{ cb.addEventListener('change', e=>{ const w = document.getElementById('warning'); w.style.display = 'none'; if(document.querySelectorAll('#heatChoices input:checked').length > 3){ e.target.checked = false; w.textContent = '⚠️ You can only select up to 3 options.'; w.style.display = 'block'; setTimeout(()=>w.style.display='none',3000); } updateHeatMetrics(); }); }); // Initial tick updateHeatMetrics(); // Continue logic window.handleContinue = function(){ const w = document.getElementById('warning'); if(!document.querySelectorAll('#heatChoices input:checked').length){ w.style.display = 'block'; } else { SugarCube.Engine.play('Heatwave_Response_Centres'); } }; }, 0); <</script>>
history of heatwaves in canadian cities. what impact do they have on people who are currently unsheltered. what measures have various cities taken and their effectivness.
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title" style="white-space: nowrap; display: inline;">Affordable Housing</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">500</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Long-Term Savings</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">2<span class="impact-subtitle"> M / year</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Upfront Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">20<span class="impact-subtitle"> M</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em"> With land scarce and demand high, you chose to densify. Construction begins on mid-rise apartments designed for affordability, sustainability, and access to transit. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreServiceFunding');">Continue</button> </div>
<<script>> PolicyManager.register({ name: "Office Conversion to Transitional Housing", costUpfront: 6, costPerTurn: 0.5, duration: 999999, immediate(v) { // Added window.Util v.publicOpinion += 5; if (v.systemCapacity < 180) v.systemCapacity += 10; let toMove = 120; let fromUnsheltered = Math.min(v.pop.unsheltered, Math.round(toMove * 0.4)); v.pop.unsheltered -= fromUnsheltered; v.pop.transitional += fromUnsheltered; toMove -= fromUnsheltered; let fromSheltered = Math.min(v.pop.sheltered, toMove); v.pop.sheltered -= fromSheltered; v.pop.transitional += fromSheltered; // homelessTotal does not change here, as they move between categories of homelessness. v.inflowShare.unsheltered = Math.max(0.01, v.inflowShare.unsheltered - 0.02); v.inflowShare.sheltered = Math.min(0.9, v.inflowShare.sheltered + 0.02); window.Util.renorm(v.inflowShare); }, tick(v, age) { if (age > 0 && v.pop.transitional > 0) { const exitsFromTransitional = Math.min(v.pop.transitional, 8); // ~32/year window.Util.exitPeople(exitsFromTransitional, { transitional: 1.0 }); } if (age > 0 && age % 4 === 0) { if (v.serviceCoordination < 90) v.serviceCoordination += 1; } } }); <</script>> <div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">Transitional Capacity</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">120</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> beds</span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Overflow Relief</div> <div class="impact-content"> <span class="impact-value">1,200</span> <span class="impact-subtitle" style="font-size: 1.1em; margin-bottom: -3px;"> nights / year</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Renovation Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">6</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> M</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em"> In March 2024, Council approved leasing under‑used downtown offices for conversion to transitional housing, adding 120 staffed beds and freeing recreation centres for public use. Retrofits cost around $6 M but create a template for wider office‑to‑housing conversions post‑pandemic. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('BuildCanadaHomes');">Continue</button> <button class="continue-button" onclick="SugarCube.Engine.backward();">Back</button> </div>
<<script>>window.PolicyManager.tick();<</script>> <div class="passageContent"> <<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .button { background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); /* ✔ Correct border color */ border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; display: block; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; -webkit-user-select: none; touch-action: manipulation; width: 100%; max-width: 420px; margin: 0.9rem auto; position: relative; top: -0.25rem; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); /* ✔ Reinforce border on hover */ } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); /* ✔ Reinforce border on click */ } .button:focus { outline: none; box-shadow: 0 0 0 3px rgba(58, 122, 217, 0.5); border-color: rgb(44, 123, 202); /* ✔ Keep border consistent on focus */ } .button:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; border-color: #2a5a87; /* ✔ Still visible, but dimmed */ } .button:disabled:hover, .button:disabled:active { pointer-events: none; box-shadow: none; } .button:first-of-type { margin-top: 1rem; } </style> <div style="margin:2rem 0; padding:1em; background:#fdfdfd; border:1px solid #ccc; font-family:monospace; font-size:0.9em; line-height:1.4;"> <strong>🔍 FULL DEBUG SNAPSHOT</strong> <hr> <strong>⏱️ Game Time</strong><br> Turn: <<print $turn || 0>><br> Year: <<print $year || 0>><br> Calendar Year: <<print $currentYear || '–'>><br><br> <strong>💰 Budget & Inflow</strong><br> Budget: $<<print $budget || 0>><br> Inflow Base: <<print $inflowBase || 0>><br><br> <strong>📈 Inflow Counts This Turn</strong><br> <<for _demo range Object.keys($inflowShare)>> <<set _share = $inflowShare[_demo] || 0>> <<set _count = Math.round($inflowBase * _share)>> • <<print _demo>>: <<print _count>><br> <</for>> <br> <strong>🚸 Homeless Population</strong><br> Total: <<print $homelessTotal || 0>><br> <<for _demo range Object.keys($pop)>> • <<print _demo>>: <<print ($pop[_demo] || 0)>><br> <</for>> <br> <strong>⚙️ System Metrics</strong><br> System Capacity: <<print ($systemCapacity != null ? systemCapacity.toFixed(2) + '%' : '0%')>><br> Service Coordination: <<print $serviceCoordination || 0>><br> Public Opinion: <<print $publicOpinion || 0>><br> Political Capital: <<print $politicalCapital || 0>><br><br> <strong>📋 Active Policies</strong><br> <<script>> const policies = window.PolicyManager.active; const html = policies.length ? policies.map(p => `• ${p.name} (age ${p.age}, costUpfront ${p.costUpfront}, costPerTurn ${p.costPerTurn||0})` ).join('<br>') : '<em>None</em>'; jQuery('.debug-active').html(html); <</script>> <div class="debug-active" style="margin-left:1em;"></div> </div> <<button "Loop">> <<run (()=>{ Engine.play("Loop"); })()>> <</button>> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title" style="white-space: nowrap; display: inline;">Housing Supply</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">500</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Long-Term Savings</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">1<span class="impact-subtitle"> M / year</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Upfront Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">5<span class="impact-subtitle"> M</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em;">Your team selects prefabricated modular homes to jumpstart supply. Quick to build and cost-efficient, they’ll deliver rapid housing relief in both urban and suburban zones. </p> <div class="break"> </div> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreServiceFunding');">Continue</button> </div> <style> .policy-impact-screen { position: relative; margin: 10vh auto 2em; content */ border-radius: 14px; width: 80%; max-width: 800px; max-height: 90vh; overflow-y: auto; padding: 1.5em; font-family: "Inter", sans-serif; box-sizing: border-box; } /* Close button still sits inside relative parent */ .policy-impact-screen .impact-close { position: absolute; right: 0.75em; background: none; border: none; font-size: 1.25em; cursor: pointer; line-height: 1; } /* Flex row of impacts */ .policy-impact-screen .impact-container { display: flex; gap: 1.5em; justify-content: center; } /* On small screens: stack and bring even higher */ @media (max-width: 979px) { .policy-impact-screen { margin: 5vh auto 1.5em; /* only 5 vh top margin */ width: 90%; padding: 1em; } .policy-impact-screen .impact-container { flex-direction: column; gap: 1em; } } /* Individual impact box */ .impact-box { background: #fff; border: 1px solid #e2e2e8; border-radius: 8px; flex: 1; min-width: 170px; padding: 1em; display: flex; flex-direction: column; align-items: center; } /* Subtitle styling (uppercase Inter with a bottom dotted line) */ .impact-title { font-family: "Inter", sans-serif; font-weight: 400; font-size: 1em; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5em; text-align: center; border-bottom: 1px dotted rgba(0, 0, 0, 0.8); padding-bottom: 0.25em; } /* Content area */ .impact-content { flex: 1; display: flex; align-items: center; justify-content: center; width: 100%; } .impact-number { font-family: "Spectral", serif; font-weight: 600; font-size: 2em; color: #0066cc; } .impact-graph { width: 100%; height: 120px; } .impact-widget { font-size: 2em; line-height: 1; } </style>
<<script>> PolicyManager.register({ name: "Renoviction Awareness", costUpfront: 4, costPerTurn: 1, duration: 3, // heavy up-front push immediate(v) { }, tick(v, age) { // Nothing for now } }); <</script>> <div class="passageContent"> <p>Info </p> [[Continue|Dish out funding]] </div>
<<set $turn += 1>> <<set $year += 1>> <div class="passageContent"> <h2>End of Year 2024 Summary</h2> <p>After twelve months under your chosen strategy:</p> <ul> <li><strong>Total Homeless Population:</strong> <<= $homelessTotal >> (change of <<= Math.round((($homelessTotal - 2952)/2952)*100) >>%)</li> <li><strong>Budget Remaining:</strong> $<<print $budget>>M</li> <li><strong>Public Trust:</strong> <<= $publicOpinion >></li> <li><strong>Service Coordination:</strong> <<= $systemCoordination >></li> <li><strong>System Capacity:</strong> <<= $systemCapacity >></li> <li><strong>Political Capital:</strong> <<= $politicalCapital >></li> </ul> <p> You’ve done ...... and ...... to ..... </p> <p> </p> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <div class="passageContent"> <h2>📍 Point-in-Time Snapshot, 2024</h2> <p>This report reflects the current state of homelessness and system readiness. </p> <h3>🏠</h3> <ul> <li><strong>Unsheltered:</strong> <<= $pop.unsheltered >></li> <li><strong>Sheltered:</strong> <<= $pop.sheltered >></li> <li><strong>Transitional Housing:</strong> <<= $pop.transitional >></li> <li><strong>Institutional Settings:</strong> <<= $pop.institutional >></li> <li><strong>Chronically Homeless:</strong> <<= $pop.chronic >></li> <li><strong>With Children:</strong> <<= $pop.withChildren >></li> <li><strong>Foster Care History:</strong> <<= $pop.fosterCareHistory >></li> </ul> <h3>🧑🤝🧑</h3> <ul> <li><strong>Male:</strong> <<= $pop.male >></li> <li><strong>Female:</strong> <<= $pop.female >></li> <li><strong>Trans / Non-binary:</strong> <<= $pop.transNB >></li> <li><strong>Age 25–49:</strong> <<= $pop.age25to49 >></li> <li><strong>LGBQ:</strong> <<= $pop.lgbq >></li> <li><strong>LGBTQ+ Youth:</strong> <<= $pop.lgbtqYouth >></li> <li><strong>Racialized:</strong> <<= $pop.racialized >></li> <li><strong>Indigenous:</strong> <<= $pop.indigenous >></li> <li><strong>Veterans:</strong> <<= $pop.veteran >></li> <li><strong>Immigrants / Refugees:</strong> <<= $pop.immigrant >></li> </ul> <p><em>This snapshot will help guide your next set of decisions in the evolving response. </em></p> <p>📌 [[Continue|NextYear]]</p> <p>📌 [[PIT1|PIT1]]</p> </div>
<<set $turn += 1>> <!-- ————————————————————————————————————————————— ————————————————————————————————————————————— --> <div class="chart-section"> <h3>Housing Status Change</h3> <canvas id="housingStackedBar" height="160"></canvas> </div> <div class="chart-section"> <h3>Homelessness Trend</h3> <canvas id="homelessLineChart" height="160"></canvas> </div> <div class="chart-section"> <h3>Identity Demographics</h3> </div> <div class="chart-section"> <div class="demo-nav"> <button id="prevDemo">◀</button> <canvas id="demoBarChart" height="160"></canvas> <button id="nextDemo">▶</button> </div> </div> <style> .chart-section canvas{padding:12px!important;} /* make nav buttons line up with the bar chart nicely */ .demo-nav{ display:flex;align-items:center;gap:0.5em;justify-content:center; } .demo-nav button{ font-size:1.2em;border:none;border-radius:4px;padding:0.2em 0.5em; background-color: #06C; color: #fff; } </style> <<script>> // Global font settings for all charts Chart.defaults.font.family = "'Inter', sans-serif"; Chart.defaults.font.size = 16; Chart.defaults.font.style = 'normal'; Chart.defaults.font.weight = 'normal'; Chart.defaults.font.lineHeight = 1.5; // window.Utility: get first snapshot per year function snapshotsByYear(history) { const out = []; const seen = new Set(); history.forEach(s => { if (s.year >= 0 && !seen.has(s.year)) { out.push(s); seen.add(s.year); } }); return out; } /* ───────────────────────────────────────────── 1. build or update CHARTS after each passage ───────────────────────────────────────────── */ $(document).one(':passagedisplay', function(){ const V = State.variables; const hist = V.history || []; const before = hist[0]; const after = hist[1] ?? null; /* ------------------------- HOUSING‑STATUS STACKED BAR (absolute counts this time) ------------------------- */ /* ------------------------- HOUSING‑STATUS STACKED BAR (year → year) ------------------------- */ (function(){ const ctx = document.getElementById('housingStackedBar').getContext('2d'); const cats = ['unsheltered','sheltered','transitional','institutional']; const colours=['#FF6384','#36A2EB','#FFCE56','#4BC0C0']; const byYear = snapshotsByYear(hist); const labels = byYear.map((s, i) => `Year ${i + 1}`); const datasets = cats.map((k, i) => ({ label: k.charAt(0).toUpperCase() + k.slice(1), data: byYear.map(y => y.pop[k] ?? 0), // one value per year backgroundColor: colours[i] })); new Chart(ctx,{ type:'bar', data:{labels,datasets}, options:{ responsive:true, layout:{padding:16}, scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true}} } }); })(); /* ------------------------- HOMELESS TOTAL LINE (turn → turn, unchanged) ------------------------- */ (function(){ const ctx = document.getElementById('homelessLineChart').getContext('2d'); const labels = hist.map(s=>`Turn ${s.turn}`); const data = hist.map(s=>s.homelessTotal); const grad=ctx.createLinearGradient(0,0,0,200); grad.addColorStop(0,'rgba(0,102,204,0.5)'); grad.addColorStop(1,'rgba(0,102,204,0)'); new Chart(ctx,{ type:'line', data:{labels,datasets:[{ label:'Total Homeless', data,fill:true,backgroundColor:grad, borderColor:'rgba(0,102,204,1)',tension:0.4,pointRadius:3 }]}, options:{responsive:true,layout:{padding:16}} }); })(); /* ------------------------- CYCLABLE DEMOGRAPHIC BAR (year → year, colour #06C) ------------------------- */ (function(){ const ctx = document.getElementById('demoBarChart').getContext('2d'); const groups = [ {key:'male', label:'Male'}, {key:'female', label:'Female'}, {key:'transNB', label:'Trans & NB'}, {key:'lgbq', label:'LGBTQ'}, {key:'lgbtqYouth', label:'LGBTQ Youth'}, {key:'age25to49', label:'Ages 25‑49'}, {key:'racialized', label:'Racialized'}, {key:'indigenous', label:'Indigenous'}, {key:'veteran', label:'Veteran'}, {key:'immigrant', label:'Immigrant'} ]; let index = 0; const yearly = snapshotsByYear(hist); const labels = yearly.map((s, i) => `Year ${i + 1}`); const makeDataset = g => yearly.map(s=>s.pop[g.key]); const chart = new Chart(ctx,{ type:'bar', data:{ labels, datasets:[{ label:groups[0].label, data :makeDataset(groups[0]), backgroundColor:'#06C' }] }, options:{ responsive:true, layout:{padding:16}, scales:{y:{beginAtZero:true}} } }); function updateChart(dir){ index = (index+dir+groups.length)%groups.length; const g = groups[index]; chart.data.datasets[0].label = g.label; chart.data.datasets[0].data = makeDataset(g); chart.update(); } $('#prevDemo').on('click',()=>updateChart(-1)); $('#nextDemo').on('click',()=>updateChart(+1)); })(); }); <</script>> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('Event1');">Continue</button> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- ————————————————————————————————————————————— ————————————————————————————————————————————— --> <div class="chart-section"> <h3>Housing Status Change</h3> <canvas id="housingStackedBar" height="160"></canvas> </div> <div class="chart-section"> <h3>Homelessness Trend</h3> <canvas id="homelessLineChart" height="160"></canvas> </div> <div class="chart-section"> <h3>Identity Demographics</h3> </div> <div class="chart-section"> <div class="demo-nav"> <button id="prevDemo">◀</button> <canvas id="demoBarChart" height="160"></canvas> <button id="nextDemo">▶</button> </div> </div> <style> .chart-section canvas{padding:12px!important;} /* make nav buttons line up with the bar chart nicely */ .demo-nav{ display:flex;align-items:center;gap:0.5em;justify-content:center; } .demo-nav button{ font-size:1.2em;border:none;border-radius:4px;padding:0.2em 0.5em; background-color: #06C; color: #fff; } </style> <<script>> // Global font settings for all charts Chart.defaults.font.family = "'Inter', sans-serif"; Chart.defaults.font.size = 16; Chart.defaults.font.style = 'normal'; Chart.defaults.font.weight = 'normal'; Chart.defaults.font.lineHeight = 1.5; // window.Utility: get first snapshot per year function snapshotsByYear(history) { const out = []; const seen = new Set(); history.forEach(s => { if (s.year >= 0 && !seen.has(s.year)) { out.push(s); seen.add(s.year); } }); return out; } /* ───────────────────────────────────────────── 1. build or update CHARTS after each passage ───────────────────────────────────────────── */ $(document).one(':passagedisplay', function(){ const V = State.variables; const hist = V.history || []; const before = hist[0]; const after = hist[1] ?? null; console.groupCollapsed("DEBUG: Chart State Snapshot"); console.log("Current Variables (V):", V); console.log("History Array (V.history):", hist); console.groupEnd(); console.group("DEBUG: Expanded Chart State"); console.dir(V); console.dir(hist); console.groupEnd(); /* ------------------------- HOUSING‑STATUS STACKED BAR (absolute counts this time) ------------------------- */ /* ------------------------- HOUSING‑STATUS STACKED BAR (year → year) ------------------------- */ (function(){ const ctx = document.getElementById('housingStackedBar').getContext('2d'); const cats = ['unsheltered','sheltered','transitional','institutional']; const colours=['#FF6384','#36A2EB','#FFCE56','#4BC0C0']; const byYear = snapshotsByYear(hist); const labels = byYear.map((s, i) => `Year ${i + 1}`); const datasets = cats.map((k, i) => ({ label: k.charAt(0).toUpperCase() + k.slice(1), data: byYear.map(y => y.pop[k] ?? 0), // one value per year backgroundColor: colours[i] })); new Chart(ctx,{ type:'bar', data:{labels,datasets}, options:{ responsive:true, layout:{padding:16}, scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true}} } }); })(); /* ------------------------- HOMELESS TOTAL LINE (turn → turn, unchanged) ------------------------- */ (function(){ const ctx = document.getElementById('homelessLineChart').getContext('2d'); const labels = hist.map(s=>`Turn ${s.turn}`); const data = hist.map(s=>s.homelessTotal); const grad=ctx.createLinearGradient(0,0,0,200); grad.addColorStop(0,'rgba(0,102,204,0.5)'); grad.addColorStop(1,'rgba(0,102,204,0)'); new Chart(ctx,{ type:'line', data:{labels,datasets:[{ label:'Total Homeless', data,fill:true,backgroundColor:grad, borderColor:'rgba(0,102,204,1)',tension:0.4,pointRadius:3 }]}, options:{responsive:true,layout:{padding:16}} }); })(); /* ------------------------- CYCLABLE DEMOGRAPHIC BAR (year → year, colour #06C) ------------------------- */ (function(){ const ctx = document.getElementById('demoBarChart').getContext('2d'); const groups = [ {key:'male', label:'Male'}, {key:'female', label:'Female'}, {key:'transNB', label:'Trans & NB'}, {key:'lgbq', label:'LGBTQ'}, {key:'lgbtqYouth', label:'LGBTQ Youth'}, {key:'age25to49', label:'Ages 25‑49'}, {key:'racialized', label:'Racialized'}, {key:'indigenous', label:'Indigenous'}, {key:'veteran', label:'Veteran'}, {key:'immigrant', label:'Immigrant'} ]; let index = 0; const yearly = snapshotsByYear(hist); const labels = yearly.map((s, i) => `Year ${i + 1}`); const makeDataset = g => yearly.map(s=>s.pop[g.key]); const chart = new Chart(ctx,{ type:'bar', data:{ labels, datasets:[{ label:groups[0].label, data :makeDataset(groups[0]), backgroundColor:'#06C' }] }, options:{ responsive:true, layout:{padding:16}, scales:{y:{beginAtZero:true}} } }); function updateChart(dir){ index = (index+dir+groups.length)%groups.length; const g = groups[index]; chart.data.datasets[0].label = g.label; chart.data.datasets[0].data = makeDataset(g); chart.update(); } $('#prevDemo').on('click',()=>updateChart(-1)); $('#nextDemo').on('click',()=>updateChart(+1)); })(); }); <</script>> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT2');">Continue</button> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .drawer-content { padding: 10px; padding-left: 25%; padding-right: 25%; } .button-wrapper { display: inline-flex; align-items: center; width: 100%; max-width: 420px; margin: 0.9rem auto; position: relative; top: -0.25rem; } .button { flex: 1; background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); border-radius: 12px; box-shadow: rgba(0, 0, 0, 0.12) 0 1px 1px; color: #FFFFFF; cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 1rem; font-weight: 400; line-height: 1.25; padding: 0.65rem 1rem; text-align: center; transition: box-shadow 0.15s ease, transform 0.15s ease; user-select: none; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); box-shadow: rgba(255, 255, 255, 0.3) 0 0 2px inset, rgba(0, 0, 0, 0.4) 0 1px 2px; border-color: rgb(44, 123, 202); } .button:active { transform: scale(0.98); box-shadow: rgba(0, 0, 0, 0.15) 0 2px 4px inset, rgba(0, 0, 0, 0.4) 0 1px 1px; border-color: rgb(44, 123, 202); } .button:focus { outline: none; box-shadow: 0 0 0 3px rgba(58, 122, 217, 0.5); border-color: rgb(44, 123, 202); } .button:disabled { cursor: not-allowed; opacity: 0.6; box-shadow: none; border-color: #2a5a87; } .button:disabled:hover, .button:disabled:active { pointer-events: none; box-shadow: none; } .button:first-of-type { margin-top: 1rem; } .info-btn { background: none; border: none; padding: 0; margin-left: 0.5rem; cursor: pointer; } .info-btn img { width: 1.2rem; height: 1.2rem; opacity: 0.8; transition: opacity 0.2s ease; } .info-btn:hover img { opacity: 1; } </style> <!-- Intro Text & Buttons --> <div class="passage-content"> <div class="build-intro"> <p> Your city is preparing to launch its next major housing initiative—designed to tackle affordability, displacement, and income precarity head-on. You must choose between direct income support, market regulation, or tenant-focused protections. Which approach will shape the city’s future? </p> <div class="button-wrapper"> <button class="button" onclick="applyUBIPilot();"> Launch a Universal Basic Income Pilot </button> <button id="info-ubi" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> <div class="button-wrapper"> <button class="button" onclick="applyVacancyTax();"> Increase Vacancy & Speculation Taxes </button> <button id="info-vacancy" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> <div class="button-wrapper"> <button class="button" onclick="applyRightOfFirstRefusal();"> Establish Right of First Refusal </button> <button id="info-rofr" class="info-btn"> <img src="https://cdn.jsdelivr.net/npm/feather-icons@4.29.0/dist/icons/info.svg" alt="Info" /> </button> </div> </div> </div> <!-- Drawers --> <sl-drawer placement="bottom" label=" " class="drawer-ubi"> <div class="drawer-content"> <h3>Universal Basic Income Pilot</h3> <p> UBI schemes date back to the 1960s (e.g. Milton Friedman’s “Negative Income Tax”) and more recently Ontario’s 2017–18 pilot in Hamilton and Thunder Bay. They provide an unconditional floor of support—here, a city-funded $1,000/month for 1,000 households over 24 months—to stabilize incomes and reduce housing precarity. </p> <h4>Why It Matters</h4> <p> Evidence from pilots (e.g., Finland 2017–18) shows improvements in well-being and modest employment changes. For housing, a steady income can cut rental arrears and eviction risk instantly, buying time for longer-term supply fixes. </p> <h4>Variants</h4> <ul> <li><strong>Flat UBI</strong>: Same payment to all eligible.</li> <li><strong>Tiered UBI</strong>: Larger grants for highest-need groups (e.g., single parents).</li> <li><strong>Time-Limited UBI</strong>: Phases out over 2–3 years to encourage self-sufficiency.</li> </ul> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-vacancy"> <div class="drawer-content"> <h3>Speculation & Vacancy Tax</h3> <p> First pioneered in Vancouver (2017) and Montréal (2021), escalating vacancy taxes start at 3% of assessed value, rising by 1% each year. Coupled with a mandatory Empty Homes Registry, they discourage houses sitting unused and spur owners to rent or sell. </p> <h4>How It Works</h4> <p> Owners register every property; unoccupied homes incur a tax. Revenues fund affordable-housing grants. Transparency also reveals geographic clusters of empty units, guiding targeted outreach. </p> <h4>Possible Tweaks</h4> <ul> <li><strong>Tiered Tax Rates</strong>: Higher rates in hotter markets.</li> <li><strong>Occupancy Exemptions</strong>: Seniors or medically homebound.</li> <li><strong>Rebate Mechanisms</strong>: For owners who lease to nonprofits or below-market tenants.</li> </ul> </div> </sl-drawer> <sl-drawer placement="bottom" label=" " class="drawer-rofr"> <div class="drawer-content"> <h3>Right of First Refusal</h3> <p> Adopted in cities like New York (1988) and Montreal (2019), ROFR laws give tenants—or in this case, the municipality—the option to match a sale offer whenever a rental building is put on the market. This preserves affordability and prevents mass renoviction flips. </p> <h4>Key Features</h4> <p> When an owner accepts a third-party bid, the city or tenant coop has 60 days to step in. If exercised, sales close at the agreed price; if declined, the sale proceeds unrestricted. </p> <h4>Challenges & Variants</h4> <ul> <li><strong>Tenant ROFR</strong>: Empowers residents directly but requires strong tenant organizations.</li> <li><strong>City ROFR</strong>: Centralizes acquisitions under a municipal land trust or housing authority.</li> <li><strong>Limited Scope</strong>: Applies only to buildings over a certain size or in priority zones.</li> </ul> </div> </sl-drawer> <!-- Wiring & Policy Logic --> <<script>> // 1. Advance the turn window.PolicyManager.tick(); // 2. INFO buttons (delegated binding—no more null errors) $(document).on('click', '#info-ubi', () => document.querySelector('.drawer-ubi').show()); $(document).on('click', '#info-vacancy', () => document.querySelector('.drawer-vacancy').show()); $(document).on('click', '#info-rofr', () => document.querySelector('.drawer-rofr').show()); // 3. Close drawers (delegated too) $(document).on('click', 'sl-drawer sl-button[slot="footer"]', e => e.currentTarget.closest('sl-drawer').hide() ); // 4. Policy registration functions on window window.applyUBIPilot = function() { PolicyManager.register({ name: "UBI Pilot Program", costUpfront: 0, costPerTurn: 3, duration: 8, immediate(v) { if (v.publicOpinion < 90) v.publicOpinion += 8; v.politicalCapital -= 1; }, tick(v, age) { v.inflowBase = Math.max(0, v.inflowBase - 8); v.inflowShare.withChildren = Math.max(0.01, v.inflowShare.withChildren - 0.01); v.inflowShare.racialized = Math.max(0.01, v.inflowShare.racialized - 0.01); v.inflowShare.sheltered = Math.max(0.01, v.inflowShare.sheltered - 0.01); window.Util.renorm(v.inflowShare); window.Util.exitPeople(10, { sheltered: 0.5, transitional: 0.5 }); if (age === 8) { if (v.publicOpinion < 90) v.publicOpinion += 5; if (v.serviceCoordination < 90) v.serviceCoordination += 3; } } }); SugarCube.Engine.play("UBIPilot"); }; window.applyVacancyTax = function() { PolicyManager.register({ name: "Vacancy & Speculation Tax Increase", costUpfront: 1, costPerTurn: 0.1, duration: 999999, immediate(v) { if (v.publicOpinion > 10) v.publicOpinion -= 3; v.politicalCapital -= 1; }, tick(v, age) { const revenue = 0.625; v.budget += revenue; if (age >= 2) { window.Util.exitPeople(12, { sheltered: 0.6, transitional: 0.4 }); v.inflowBase = Math.max(0, v.inflowBase - 2); } if (age > 0 && age % 4 === 0 && v.publicOpinion < 60) { v.publicOpinion += 1; } } }); SugarCube.Engine.play("VacancyTax"); }; window.applyRightOfFirstRefusal = function() { PolicyManager.register({ name: "Right of First Refusal Program", costUpfront: 3, costPerTurn: 0.2, duration: 999999, immediate(v) { if (v.publicOpinion < 90) v.publicOpinion += 5; v.politicalCapital -= 1; }, tick(v, age) { if (age >= 1) { v.unitsPreserved = (v.unitsPreserved || 0) + 18; v.inflowBase = Math.max(0, v.inflowBase - 3); v.inflowShare.withChildren = Math.max(0.01, v.inflowShare.withChildren - 0.005); v.inflowShare.racialized = Math.max(0.01, v.inflowShare.racialized - 0.005); window.Util.renorm(v.inflowShare); if (age % 8 === 0) v.budget -= 1; } } }); SugarCube.Engine.play("RightOfFirstRefusal"); }; <</script>>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .button { background-color: #ffffff !important; color: #111827 !important; border: 1px solid #e5e7eb !important; padding: 0.5rem 0.75rem !important; border-radius: 0.5rem !important; font-size: 0.875rem !important; font-weight: 500 !important; line-height: 1.25 !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 0.3rem auto; /* centers it */ display: block; text-align: center !important; } </style> <!-- Intro Text --> <div class="passage-content"> <div class="build-intro"> <p> Now that key policies are in motion, it’s time to decide how to support them. From shelters to tenant services to housing outreach, your budget shapes what actually gets delivered — and how well it works. </p> <p> These services don’t always align perfectly. Now it’s your job to allocate funds to each one — and decide what role they’ll play in the years ahead. </p> <button class="button" onclick="SugarCube.Engine.play('ServiceFunding1')">Fund Services</button> </div> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <!-- CSS Styling --> <style> .build-intro { max-width: 700px; margin: 0 auto; padding: 40px 20px; text-align: center; font-size: 1.2em; margin-top: 5rem; line-height: 1.6; margin-bottom: 2rem; color: #1c1c1e; } .button { background-color: #ffffff !important; color: #111827 !important; border: 1px solid #e5e7eb !important; padding: 0.5rem 0.75rem !important; border-radius: 0.5rem !important; font-size: 0.875rem !important; font-weight: 500 !important; line-height: 1.25 !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 0.3rem auto; /* centers it */ display: block; text-align: center !important; } </style> <!-- Intro Text --> <div class="passage-content"> <div class="build-intro"> <p> Zoning rules determine what kind of buildings can be built — and where. They shape our neighborhoods, our housing supply, and who gets to live where. </p> <p> For decades, zoning laws have blocked density, excluded lower-income residents, and slowed the response to the housing crisis. But changing them is politically risky and not glamorous. Each path changes affordability, climate impact, and public trust. </p> <button class="button" onclick="SugarCube.Engine.play('Zoning')">Update Zoning</button> </div> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <div class="passageContent"> <h2>Quarter 4 Report</h2> <p><strong>Year:</strong> <<= $year >></p> <ul> <li><strong>Budget Remaining:</strong> $<<print $budget>>M</li> <li><strong>Public Trust:</strong> <<print $publicOpinion>></li> <li><strong>Service Coordination:</strong> <<print $serviceCoordination>></li> <li><strong>Staff Morale:</strong> <<print $staffMorale>></li> <li><strong>System Capacity:</strong> <<print $systemCapacity>></li> <li><strong>Political Capital:</strong> <<print $politicalCapital>></li> <li><strong>Total Homeless Population:</strong> <<print $homelessTotal>></li> </ul> <p><em>You’ve completed one full year of strategy implementation.</em></p> <p> <strong>→</strong> [[See End-of-Year Summary|QuarterTwo]] </p> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <div class="passageContent"> <h2>Quarter 3 Report</h2> <p><strong>Year:</strong> <<= $year >></p> <ul> <li><strong>Budget Remaining:</strong> $<<print $budget>>M</li> <li><strong>Public Trust:</strong> <<print $publicOpinion>></li> <li><strong>Service Coordination:</strong> <<print $serviceCoordination>></li> <li><strong>Staff Morale:</strong> <<print $staffMorale>></li> <li><strong>System Capacity:</strong> <<print $systemCapacity>></li> <li><strong>Political Capital:</strong> <<print $politicalCapital>></li> <li><strong>Total Homeless Population:</strong> <<print $homelessTotal>></li> </ul> <p> <strong>→</strong> [[Proceed to Q4|QuarterFour]] </p> </div>
<<script>>window.PolicyManager.tick();<</script>> <div class="passageContent"> <h2>Quarter 2 Report</h2> <p><strong>Year:</strong> <<= $year >></p> <ul> <li><strong>Budget Remaining:</strong> $<<print $budget>>M</li> <li><strong>Public Trust:</strong> <<print $publicOpinion>></li> <li><strong>Service Coordination:</strong> <<print $serviceCoordination>></li> <li><strong>Staff Morale:</strong> <<print $staffMorale>></li> <li><strong>System Capacity:</strong> <<print $systemCapacity>></li> <li><strong>Political Capital:</strong> <<print $politicalCapital>></li> <li><strong>Total Homeless Population:</strong> <<print $homelessTotal>></li> </ul> <p> <strong>→</strong> [[Proceed to Q3|QuarterThree]] </p> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">Units Preserved</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">75</span> <span class="impact-subtitle"> units / year</span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Unsheltered Inflow</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">5</span> <span class="impact-subtitle"> % / year</span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Political Cost</div> <div class="impact-content"> <span style="color: #06C; font-size: 5em; padding-right: 10px;">●</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em"> Now when rental properties are listed for sale, the city of Ottawa or a local tenant coop get 60 days to match offers. In its first year, its estimated 75 at-risk units will be secured under nonprofit or city ownership, reducing unsheltered inflow by around 5%. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT2');">Continue</button> </div>
<<script>> PolicyManager.register({ name: "Renoviction Awareness", costUpfront: 4, costPerTurn: 1, duration: 3, // heavy up-front push immediate(v) { }, tick(v, age) { // Nothing for now } }); <</script>> <div class="passageContent"> <p>Info </p> [[Continue|Dish out funding]] </div>
<<script>> PolicyManager.register({ name: "Anti-Renoviction Bylaw", costUpfront: 1, costPerTurn: 1, duration: 999999, immediate(v) { if (v.publicOpinion < 90) v.publicOpinion += 8; v.politicalCapital -= 1; v.unitsPreserved = (v.unitsPreserved || 0) + 150; }, tick(v, age) { if (age >= 1) { v.inflowBase = Math.max(0, v.inflowBase - 10); v.inflowShare.withChildren = Math.max(0.01, v.inflowShare.withChildren - 0.01); v.inflowShare.sheltered = Math.max(0.01, v.inflowShare.sheltered - 0.01); window.Util.renorm(v.inflowShare); if (age % 4 === 0 && v.publicOpinion < 70) v.publicOpinion +=1; } } }); <</script>> <div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">Eviction-driven Inflow</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">22</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> % / year</span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Affordable Units Preserved</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">150</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> units</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Enforcement Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">2</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> M / year</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em"> In January 2025, Council instructed staff to draft an anti‑renoviction by‑law after a five‑fold jump in N13 eviction notices. The by‑law will license major renovations, require relocation assistance, and fine landlords who misuse the process—aimed at cutting eviction‑driven homelessness while protecting roughly 150 affordable units a year. </p> <div class="break"> </div> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('BuildCanadaHomes');">Continue</button> <button class="continue-button" onclick="SugarCube.Engine.backward();">Back</button> </div>
<div class="puzzle-layout"> <div id="game-area"> <!-- The plugin will create its canvas inside this div --> </div> <div class="controls-panel"> <!-- Budget Meter --> <div class="balance-meter" id="budget-meter" data-unit="currency" style="top: 0px" > <div class="title">Budget Allocated</div> <div style=" border-top: 1px dashed #cccccc; margin-top: 8px; padding-top: 5px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; " > <div> <button id="inner-square-decrease" class="control-btn" title="Decrease Size" > - </button> <span class="dollar-sign">$</span> <span class="digits2" id="budget-val1">100</span>M <button id="inner-square-increase" class="control-btn" title="Increase Size" > + </button> </div> <button id="submit-button" class="submit-btn" disabled> Submit </button> </div> </div> <div id="selected-piece-controls-group"> <div class="title" id="selected-piece-id"> Click a service on the grid to select it. </div> <div style=" border-top: 1px dashed #cccccc; margin-top: 8px; padding-top: 5px; " > <div id="selected-piece-info" style="display: none"> <div class="control-group"> <button id="piece-transform-prev" class="control-btn" title="Previous Shape" > - </button> <span class="dollar-sign" id="piece-value-symbol" style="font-weight: 600; font-size: 32px" >¢</span > <button id="piece-transform-next" class="control-btn" title="Next Shape" > + </button> </div> </div> <div id="selected-piece-description" style="display: none; font-size: 0.9em; margin-top: 8px; color: #555;"> <!-- dynamically filled description --> </div> <p style="font-size: 0.8em; margin-top: 10px"> Press 'R' to rotate the selected piece. </p> </div> </div> </div> </div> <<script>> // jQuery Widget (no changes needed, include as is from iteration 1) (function (e) { "function" == typeof define && define.amd ? define(["jquery"], e) : e(jQuery); })(function (e) { var t = 0, i = Array.prototype.slice; (e.cleanData = (function (t) { return function (i) { var s, n, a; for (a = 0; null != (n = i[a]); a++) try { (s = e._data(n, "events")), s && s.remove && e(n).triggerHandler("remove"); } catch (o) {} t(i); }; })(e.cleanData)), (e.widget = function (t, i, s) { var n, a, o, r, h = {}, l = t.split(".")[0]; return ( (t = t.split(".")[1]), (n = l + "-" + t), s || ((s = i), (i = e.Widget)), (e.expr[":"][n.toLowerCase()] = function (t) { return !!e.data(t, n); }), (e[l] = e[l] || {}), (a = e[l][t]), (o = e[l][t] = function (e, t) { return this._createWidget ? (arguments.length && this._createWidget(e, t), void 0) : new o(e, t); }), e.extend(o, a, { version: s.version, _proto: e.extend({}, s), _childConstructors: [], }), (r = new i()), (r.options = e.widget.extend({}, r.options)), e.each(s, function (t, s) { return e.isFunction(s) ? ((h[t] = (function () { var e = function () { return i.prototype[t].apply(this, arguments); }, n = function (e) { return i.prototype[t].apply(this, e); }; return function () { var t, i = this._super, a = this._superApply; return ( (this._super = e), (this._superApply = n), (t = s.apply(this, arguments)), (this._super = i), (this._superApply = a), t ); }; })()), void 0) : ((h[t] = s), void 0); }), (o.prototype = e.widget.extend( r, { widgetEventPrefix: a ? r.widgetEventPrefix || t : t }, h, { constructor: o, namespace: l, widgetName: t, widgetFullName: n, } )), a ? (e.each(a._childConstructors, function (t, i) { var s = i.prototype; e.widget(s.namespace + "." + s.widgetName, o, i._proto); }), delete a._childConstructors) : i._childConstructors.push(o), e.widget.bridge(t, o), o ); }), (e.widget.extend = function (t) { for ( var s, n, a = i.call(arguments, 1), o = 0, r = a.length; r > o; o++ ) for (s in a[o]) (n = a[o][s]), a[o].hasOwnProperty(s) && void 0 !== n && (t[s] = e.isPlainObject(n) ? e.isPlainObject(t[s]) ? e.widget.extend({}, t[s], n) : e.widget.extend({}, n) : n); return t; }), (e.widget.bridge = function (t, s) { var n = s.prototype.widgetFullName || t; e.fn[t] = function (a) { var o = "string" == typeof a, r = i.call(arguments, 1), h = this; return ( (a = !o && r.length ? e.widget.extend.apply(null, [a].concat(r)) : a), o ? this.each(function () { var i, s = e.data(this, n); return "instance" === a ? ((h = s), !1) : s ? e.isFunction(s[a]) && "_" !== a.charAt(0) ? ((i = s[a].apply(s, r)), i !== s && void 0 !== i ? ((h = i && i.jquery ? h.pushStack(i.get()) : i), !1) : void 0) : e.error( "no such method '" + a + "' for " + t + " widget instance" ) : e.error( "cannot call methods on " + t + " prior to initialization; " + "attempted to call method '" + a + "'" ); }) : this.each(function () { var t = e.data(this, n); t ? (t.option(a || {}), t._init && t._init()) : e.data(this, n, new s(a, this)); }), h ); }; }), (e.Widget = function () {}), (e.Widget._childConstructors = []), (e.Widget.prototype = { widgetName: "widget", widgetEventPrefix: "", defaultElement: "<div>", options: { disabled: !1, create: null }, _createWidget: function (i, s) { (s = e(s || this.defaultElement || this)[0]), (this.element = e(s)), (this.uuid = t++), (this.eventNamespace = "." + this.widgetName + this.uuid), (this.bindings = e()), (this.hoverable = e()), (this.focusable = e()), s !== this && (e.data(s, this.widgetFullName, this), this._on(!0, this.element, { remove: function (e) { e.target === s && this.destroy(); }, }), (this.document = e( s.style ? s.ownerDocument : s.document || s )), (this.window = e( this.document[0].defaultView || this.document[0].parentWindow ))), (this.options = e.widget.extend( {}, this.options, this._getCreateOptions(), i )), this._create(), this._trigger("create", null, this._getCreateEventData()), this._init(); }, _getCreateOptions: e.noop, _getCreateEventData: e.noop, _create: e.noop, _init: e.noop, destroy: function () { this._destroy(), this.element .unbind(this.eventNamespace) .removeData(this.widgetFullName) .removeData(e.camelCase(this.widgetFullName)), this.widget() .unbind(this.eventNamespace) .removeAttr("aria-disabled") .removeClass( this.widgetFullName + "-disabled " + "ui-state-disabled" ), this.bindings.unbind(this.eventNamespace), this.hoverable.removeClass("ui-state-hover"), this.focusable.removeClass("ui-state-focus"); }, _destroy: e.noop, widget: function () { return this.element; }, option: function (t, i) { var s, n, a, o = t; if (0 === arguments.length) return e.widget.extend({}, this.options); if ("string" == typeof t) if (((o = {}), (s = t.split(".")), (t = s.shift()), s.length)) { for ( n = o[t] = e.widget.extend({}, this.options[t]), a = 0; s.length - 1 > a; a++ ) (n[s[a]] = n[s[a]] || {}), (n = n[s[a]]); if (((t = s.pop()), 1 === arguments.length)) return void 0 === n[t] ? null : n[t]; n[t] = i; } else { if (1 === arguments.length) return void 0 === this.options[t] ? null : this.options[t]; o[t] = i; } return this._setOptions(o), this; }, _setOptions: function (e) { var t; for (t in e) this._setOption(t, e[t]); return this; }, _setOption: function (e, t) { return ( (this.options[e] = t), "disabled" === e && (this.widget().toggleClass( this.widgetFullName + "-disabled", !!t ), t && (this.hoverable.removeClass("ui-state-hover"), this.focusable.removeClass("ui-state-focus"))), this ); }, enable: function () { return this._setOptions({ disabled: !1 }); }, disable: function () { return this._setOptions({ disabled: !0 }); }, _on: function (t, i, s) { var n, a = this; "boolean" != typeof t && ((s = i), (i = t), (t = !1)), s ? ((i = n = e(i)), (this.bindings = this.bindings.add(i))) : ((s = i), (i = this.element), (n = this.widget())), e.each(s, function (s, o) { function r() { return t || (a.options.disabled !== !0 && !e(this).hasClass("ui-state-disabled")) ? ("string" == typeof o ? a[o] : o).apply(a, arguments) : void 0; } "string" != typeof o && (r.guid = o.guid = o.guid || r.guid || e.guid++); var h = s.match(/^([\w:-]*)\s*(.*)$/), l = h[1] + a.eventNamespace, u = h[2]; u ? n.delegate(u, l, r) : i.bind(l, r); }); }, _off: function (t, i) { (i = (i || "").split(" ").join(this.eventNamespace + " ") + this.eventNamespace), t.unbind(i).undelegate(i), (this.bindings = e(this.bindings.not(t).get())), (this.focusable = e(this.focusable.not(t).get())), (this.hoverable = e(this.hoverable.not(t).get())); }, _delay: function (e, t) { function i() { return ("string" == typeof e ? s[e] : e).apply(s, arguments); } var s = this; return setTimeout(i, t || 0); }, _hoverable: function (t) { (this.hoverable = this.hoverable.add(t)), this._on(t, { mouseenter: function (t) { e(t.currentTarget).addClass("ui-state-hover"); }, mouseleave: function (t) { e(t.currentTarget).removeClass("ui-state-hover"); }, }); }, _focusable: function (t) { (this.focusable = this.focusable.add(t)), this._on(t, { focusin: function (t) { e(t.currentTarget).addClass("ui-state-focus"); }, focusout: function (t) { e(t.currentTarget).removeClass("ui-state-focus"); }, }); }, _trigger: function (t, i, s) { var n, a, o = this.options[t]; if ( ((s = s || {}), (i = e.Event(i)), (i.type = ( t === this.widgetEventPrefix ? t : this.widgetEventPrefix + t ).toLowerCase()), (i.target = this.element[0]), (a = i.originalEvent)) ) for (n in a) n in i || (i[n] = a[n]); return ( this.element.trigger(i, s), !( (e.isFunction(o) && o.apply(this.element[0], [i].concat(s)) === !1) || i.isDefaultPrevented() ) ); }, }), e.each({ show: "fadeIn", hide: "fadeOut" }, function (t, i) { e.Widget.prototype["_" + t] = function (s, n, a) { "string" == typeof n && (n = { effect: n }); var o, r = n ? n === !0 || "number" == typeof n ? i : n.effect || i : t; (n = n || {}), "number" == typeof n && (n = { duration: n }), (o = !e.isEmptyObject(n)), (n.complete = a), n.delay && s.delay(n.delay), o && e.effects && e.effects.effect[r] ? s[t](n) : r !== t && s[r] ? s[r](n.duration, n.easing, a) : s.queue(function (i) { e(this)[t](), a && a.call(s[0]), i(); }); }; }), e.widget; }); // --- Dynamic Block Puzzle Plugin --- (function ($) { "use strict"; // --- Default Themes (can be expanded) --- const DynamicBlockThemes = { default: { background: "#ffffff", // Outer grid background gridLineColor: "#cfcfcf", // Thin grid lines innerSquareBorderColor: "rgba(0, 0, 0, 0.7)", // Bright border for target pieceColors: { typeA: "#E53E3E", // Red → var(--uchu-red) typeB: "#3182CE", // Blue → var(--uchu-blue) typeC: "#37A169", // Green → var(--uchu-green) typeD: "#ECC94A", // Yellow → var(--uchu-yellow) typeE: "#9F7AEA", // Purple → var(--uchu-purple) typeF: "#38B2AC", // Teal → Close to green in Uchū typeG: "#ED8936", // Orange → var(--uchu-orange) typeH: "#D53F8C", }, pieceStrokeColor: "rgba(255, 255, 255, 10.0)", // Darker stroke for pieces pieceStrokeWidth: 1, selectedPieceStrokeColor: "rgba(255, 255, 255, 10.0)", // Bright white for selected selectedPieceStrokeWidth: 1, ghostFillColorValid: "rgba(0, 0, 0, 0.10)", // Semi-transparent light ghostFillColorInvalid: "rgba(231, 76, 60, 0.35)", // Semi-transparent red ghostStrokeColor: "rgba(0, 0, 0, 0.4)", }, }; // --- Shape Definitions for Piece Transformation --- // Helper to create simple rotated versions. For complex shapes, define all 4 rotations manually. // This simplified rotation works by swapping and negating coordinates, assuming rotation around (0,0) of the piece's local blocks. function _sR(blocks) { // simpleRotate90 const rotated = []; for (let i = 0; i < blocks.length; i += 2) { rotated.push(-blocks[i + 1], blocks[i]); } return rotated; } const PieceShapeRepository = { // --- Basic & Tetris-like --- dot: { name: "Dot 1x1", rotations: [ [0, 0], [0, 0], [0, 0], [0, 0], ], }, // Alias for 1x1 line2: { name: "Line I2", rotations: (() => { const b = [0, 0, 0, -1]; return [b, _sR(b), b, _sR(b)]; })(), }, line3: { name: "Line I3", rotations: (() => { const b = [0, 0, 0, -1, 0, -2]; return [b, _sR(b), b, _sR(b)]; })(), }, line4: { name: "Line I4", rotations: (() => { const b = [0, 0, 0, -1, 0, -2, 0, -3]; return [b, _sR(b), b, _sR(b)]; })(), }, line5: { name: "Line I5", rotations: (() => { const b = [0, 0, 0, -1, 0, -2, 0, -3, 0, -4]; return [b, _sR(b), b, _sR(b)]; })(), }, lShape3: { name: "L-Shape 3", rotations: (() => { const b = [0, 0, 0, -1, 1, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, lShape4: { name: "L-Shape L4", rotations: (() => { const b = [0, 0, 0, -1, 0, -2, 1, -2]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, jShape3: { name: "J-Shape 3", rotations: (() => { const b = [0, 0, 0, -1, -1, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, jShape4: { name: "J-Shape J4", rotations: (() => { const b = [0, 0, 0, -1, 0, -2, -1, -2]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, tShape4: { name: "T-Shape T4", rotations: (() => { const b = [-1, 0, 0, 0, 1, 0, 0, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, square2x2: { name: "Square O4", rotations: (() => { const b = [0, 0, 1, 0, 0, -1, 1, -1]; return [b, b, b, b]; })(), }, sShape4: { name: "S-Shape S4", rotations: (() => { const b = [0, 0, -1, 0, 0, -1, 1, -1]; const r1 = _sR(b); return [b, r1, b, r1]; })(), }, zShape4: { name: "Z-Shape Z4", rotations: (() => { const b = [0, 0, 1, 0, 0, -1, -1, -1]; const r1 = _sR(b); return [b, r1, b, r1]; })(), }, diagonal2: { name: "Diagonal 2", rotations: (() => { const b = [0, 0, 1, -1]; const r1 = _sR(b); return [b, r1, b, r1]; })(), }, corner3: { name: "Corner L3", rotations: (() => { const b = [0, 0, 1, 0, 0, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, // Small L stubbyT3: { name: "Stubby T3", rotations: (() => { const b = [-1, 0, 0, 0, 0, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, // --- More Complex Shapes --- plus5: { name: "Plus 5", rotations: (() => { const b = [0, 0, -1, 0, 1, 0, 0, -1, 0, 1]; return [b, b, b, b]; })(), }, // Symmetrical uShape5: { name: "U-Shape 5", rotations: (() => { const b = [-1, 0, 1, 0, -1, -1, 0, -1, 1, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, wideT5: { name: "Wide T5", rotations: (() => { const b = [-1, 0, 0, 0, 1, 0, 2, 0, 0, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, longL5: { name: "Long L5", rotations: (() => { const b = [0, 0, 0, -1, 0, -2, 0, -3, 1, -3]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, stairs4: { name: "Stairs 4", rotations: (() => { const b = [0, 0, 1, 0, 1, -1, 2, -1]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, // More like a skewed Z cShape6: { name: "C-Shape 6", rotations: (() => { const b = [-1, 1, 0, 1, 1, 1, -1, 0, -1, -1, 0, -1, 1, -1, 1, 0]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, // 3x3 with middle column removed hollowBox8: { name: "Hollow Box 3x3", rotations: (() => { const b = [-1, 1, 0, 1, 1, 1, -1, 0, 1, 0, -1, -1, 0, -1, 1, -1]; return [b, b, b, b]; })(), }, bigCross9: { name: "Big Cross 9", rotations: (() => { const b = [ 0, 2, -2, 0, 0, -2, -1, 0, 0, 1, -3, 0, 0, 3, 0, 0, 1, 0, 0, -3, 3, 0, 2, 0, 0, -1, ]; return [b, b, b, b]; })(), }, // 5x5 cross // --- Huge & Complex End Stages --- frame12_4x4: { name: "Frame 4x4", rotations: (() => { const b = [ -1, 1, 0, 1, 1, 1, 2, 1, -1, 0, 2, 0, -1, -1, 2, -1, -1, -2, 0, -2, 1, -2, 2, -2, ]; return [b, b, b, b]; })(), }, // Hollow 4x4 capitalH11: { name: "Capital H 11", rotations: (() => { const b = [ -1, 2, -1, 1, -1, 0, -1, -1, -1, -2, 0, 0, 1, 2, 1, 1, 1, 0, 1, -1, 1, -2, ]; const r1 = _sR(b); return [b, r1, b, r1]; })(), }, gridLock9: { name: "Pyramid 10", rotations: (() => { const b = [ -1.5, 0, -0.5, 0, 0.5, 0, 1.5, 0, // base of 4 blocks -1, -1, 0, -1, 1, -1, // 2nd level -0.5, -2, 0.5, -2, // 3rd level 0, -3, // top ].map((n) => Math.round(n)); // Round to integer grid coordinates return [b, b, b, b]; // Symmetrical, so all rotations identical })(), }, propeller9: { name: "Propeller 9", rotations: (() => { const b = [ 0, 0, 0, 1, 0, 2, 0, -1, 0, -2, 1, 1, 2, 2, -1, -1, -2, -2, ]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, chevronArrow7: { name: "Chevron Arrow 7", rotations: (() => { const b = [ -3, -3, 1, 1, -2, -2, 0, 0, 3, -3, -1, 1, 2, -2, 1, -1, -1, -1, ]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, starFort13: { name: "Star Fort 13", rotations: (() => { const b = [ 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 2, 1, 2, -1, -2, 1, -2, -1, 1, 2, -1, 2, 1, -2, -1, -2, ]; return [b, b, b, b]; })(), }, // Complex symmetrical doubleStairs8: { name: "Double Stairs 8", rotations: (() => { const b = [-1, 1, 0, 1, 0, 0, 1, 0, 1, -1, 2, -1, 2, -2, 3, -2]; const r1 = _sR(b); const r2 = _sR(r1); const r3 = _sR(r2); return [b, r1, r2, r3]; })(), }, branchingCross13: { name: "Branching Cross 13", rotations: (() => { const b = [ 0, 0, 0, 1, 1, 1, 0, -1, -1, -1, 1, 0, 1, -1, -1, 0, -1, 1, 2, 0, -2, 0, 0, 2, 0, -2, ]; return [b, b, b, b]; })(), }, Shape_1: { name: "Shape 1", rotations: (() => { const b = [ 1, 3, 0, 0, -1, -1, -1, 0, 0, 1, 1, 1, -1, 1, 0, 2, 1, 2, ]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_2: { name: "Shape 2", rotations: (() => { const b = [0, 2, 0, 1, 0, 0, -1, -1, 0, -1, 1, 2]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_3: { name: "Shape 3", rotations: (() => { const b = [ 1, -2, 2, -1, 1, 1, 0, 0, 0, -3, -1, -1, -3, -1, -2, -2, 1, -1, 3, -1, 0, -2, -1, 0, 0, 1, -1, -3, 2, -2, 1, 0, -2, -1, 1, -3, -1, 1, 0, -4, -1, -2, 0, -1, ]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_4: { name: "Shape 4", rotations: (() => { const b = [ 1, -1, 4, 0, 3, -1, -4, 0, -3, 1, -1, 3, 0, 4, -1, 0, 2, 2, 0, 1, 3, 1, -1, -3, 1, 1, -2, 2, 1, 3, 2, -2, 1, 0, -1, -1, 1, -3, -3, -1, -1, 1, -2, -2, 0, -4, 0, -1, ]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_5: { name: "Shape 5", rotations: (() => { const b = [1, -1, 0, 0, -1, -1, 1, 1, -1, 1]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_6: { name: "Shape 6", rotations: (() => { const b = [ -2, -3, -3, -1, -2, 0, 2, -2, 3, -1, 1, -1, 0, 0, -1, -2, -1, 1, -3, -2, -2, -1, 2, 0, 1, -2, 2, -3, 1, 1, 3, -2, -1, -3, 0, 2, -1, 0, 0, -1, -2, -2, 1, -3, 1, 0, 2, -1, -1, -1, 0, -2, 0, 1, ]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_7: { name: "Shape 7", rotations: (() => { const b = [ 2, -2, 1, 2, 2, 1, 1, -1, 3, -4, 0, 0, -1, 4, 0, 3, 2, 0, 1, -2, 2, -3, 1, 1, 0, 2, -1, 0, 0, -1, -1, 3, -2, 4, 1, 0, 3, -3, 4, -4, 3, 0, ]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_8: { name: "Shape 8", rotations: (() => { const b = [1, -1, 0, 0, 1, 0, -1, 1, 2, -1, -2, 2, -1, 2, 0, 1]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_9: { name: "Shape 9", rotations: (() => { const b = [0, 0, 1, 0, -1, 0, 1, -1, -1, -1]; return [b, _sR(b), b, _sR(b)]; })(), }, Shape_10: { name: "Shape 10", rotations: (() => { const b = [0, 0, 1, 0, -1, 1, -1, 0, 1, -1]; return [b, _sR(b), b, _sR(b)]; })(), }, }; const PieceTypeProgressions = { typeA: ["dot", "Shape_9", "Shape_5", "Shape_1", "bigCross9"], // Red typeB: ["dot", "sShape4", "jShape4", "capitalH11", "Shape_3"], // Blue typeC: ["dot", "tShape4", "uShape5", "frame12_4x4", "Shape_4"], // Green typeD: ["dot", "diagonal2", "stairs4", "gridLock9", "Shape_6"], // Yellow typeE: ["dot", "line4", "plus5", "propeller9", "Shape_7"], // Purple typeF: ["dot", "lShape3", "wideT5", "Shape_8", "chevronArrow7"], // Dark Gray typeG: ["dot", "jShape3", "sShape4", "hollowBox8", "starFort13"], // Purple (alt) typeH: [ "dot", "Shape_10", "longL5", "doubleStairs8", "branchingCross13", ], }; $.widget("custom.dynamicBlockPuzzle", { options: { outerGridWidth: 30, outerGridHeight: 25, blockSize: 20, // pixels initialInnerSquareSize: 8, minInnerSquareSize: 3, maxInnerSquareSize: 11, numPieces: 8, theme: "default", onPieceSelect: function (piece) {}, onPieceTransform: function (piece) {}, onInnerSquareResize: function (newSize) {}, }, _canvas: null, _ctx: null, _theme: {}, _allPieces: [], _selectedPiece: null, _draggingPiece: null, _isDragging: false, _dragStartX: 0, _dragStartY: 0, _pieceDragOffsetBlocksX: 0, _pieceDragOffsetBlocksY: 0, // Offset from piece's reference block to clicked block _innerSquare: { x: 0, y: 0, size: 0 }, _pieceIdCounter: 0, _create: function () { this.element.addClass("dynamic-puzzle-holder"); this.theme(this.options.theme); // Load theme colors this._canvas = $("<canvas></canvas>").appendTo(this.element); this._ctx = this._canvas[0].getContext("2d"); this._updateCanvasSize(); this._initInnerSquare(); this._generateInitialPieces(); // Creates Piece instances this._bindEvents(); this._updateSelectedPieceUI(); this._render(); }, _updateCanvasSize: function () { const w = this.options.outerGridWidth * this.options.blockSize; const h = this.options.outerGridHeight * this.options.blockSize; const dpr = window.devicePixelRatio || 1; // Set canvas size in physical pixels this._canvas[0].width = w * dpr; this._canvas[0].height = h * dpr; // Set canvas size in CSS pixels this._canvas.css({ width: w + "px", height: h + "px" }); // Scale the context to match device pixel ratio this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }, _initInnerSquare: function () { this._innerSquare.size = this.options.initialInnerSquareSize; this._innerSquare.x = Math.floor( (this.options.outerGridWidth - this._innerSquare.size) / 2 ); this._innerSquare.y = Math.floor( (this.options.outerGridHeight - this._innerSquare.size) / 2 ); $("#inner-square-size-display").text(this._innerSquare.size); this._updateBudgetValue(); }, // --- Piece Class Definition (Constructor within the plugin) --- Piece: function (pluginInstance, id, typeKey, x, y) { this.plugin = pluginInstance; // Reference to the plugin this.id = id; this.typeKey = typeKey; // e.g., "typeA" this.x = x; // grid units for piece's reference point this.y = y; this.progressionKeys = PieceTypeProgressions[typeKey]; if (!this.progressionKeys) { console.error("Undefined piece type progression:", typeKey); this.progressionKeys = PieceTypeProgressions.typeA; // Fallback } this.currentShapeKeyIndex = 0; // Index in this.progressionKeys this.orientation = 0; // 0, 1, 2, 3 this.color = this.plugin._theme.pieceColors[typeKey] || "#CCCCCC"; // Get color from theme }, _updateBudgetValue: function () { const size = this._innerSquare.size; const value = size * 5 + 15; // 3→30, 5→40, 7→50, 9→60, 11→70 $("#budget-val1").text(value); }, _setupPiecePrototype: function () { var self = this; // 'self' is the plugin instance for use in prototype methods this.Piece.prototype.getCurrentShapeDefinition = function () { const shapeKey = this.progressionKeys[this.currentShapeKeyIndex]; return PieceShapeRepository[shapeKey]; }; this.Piece.prototype.getBlocks = function (orientationOverride) { const shapeDef = this.getCurrentShapeDefinition(); if (!shapeDef) return []; // Should not happen if progressions are correct const effectiveOrientation = orientationOverride !== undefined ? orientationOverride : this.orientation; return shapeDef.rotations[ effectiveOrientation % shapeDef.rotations.length ]; }; this.Piece.prototype.getBounds = function (orientationOverride) { const blocks = this.getBlocks(orientationOverride); if (!blocks || blocks.length === 0) return { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0, }; let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; for (let i = 0; i < blocks.length; i += 2) { if (blocks[i] < minX) minX = blocks[i]; if (blocks[i] > maxX) maxX = blocks[i]; if (blocks[i + 1] < minY) minY = blocks[i + 1]; if (blocks[i + 1] > maxY) maxY = blocks[i + 1]; } return { left: minX, right: maxX, top: minY, bottom: maxY, width: maxX - minX, height: maxY - minY, }; }; this.Piece.prototype.rotate = function (direction) { const oldOrientation = this.orientation; const shapeDef = this.getCurrentShapeDefinition(); const numOrientations = shapeDef.rotations.length; this.orientation = (this.orientation + (direction === "left" ? -1 : 1) + numOrientations) % numOrientations; if ( self._isOutOfBounds(this) || self._checkPieceToPieceCollision(this, this.x, this.y, this) ) { this.orientation = oldOrientation; return false; } return true; }; this.Piece.prototype.transformShape = function (direction) { const oldShapeKeyIndex = this.currentShapeKeyIndex; const numShapesInProgression = this.progressionKeys.length; this.currentShapeKeyIndex = (this.currentShapeKeyIndex + (direction === "prev" ? -1 : 1) + numShapesInProgression) % numShapesInProgression; this.orientation = 0; // Reset orientation if ( self._isOutOfBounds(this) || self._checkPieceToPieceCollision(this, this.x, this.y, this) ) { this.currentShapeKeyIndex = oldShapeKeyIndex; // Revert this.orientation = 0; // Also revert orientation reset if transform fails return false; } self.options.onPieceTransform.call(self.element, this); return true; }; this.Piece.prototype.isBlockAt = function (checkGridX, checkGridY) { const blocks = this.getBlocks(); for (let i = 0; i < blocks.length; i += 2) { if ( this.x + blocks[i] === checkGridX && this.y + blocks[i + 1] === checkGridY ) { return true; } } return false; }; }, _shuffleArray: function (array) { const a = array.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }, _generateInitialPieces: function () { this._setupPiecePrototype(); // Ensure Piece methods are available this._allPieces = []; this._pieceIdCounter = 0; const pieceTypeKeys = this._shuffleArray( Object.keys(PieceTypeProgressions) ).slice(0, this.options.numPieces); for (let i = 0; i < pieceTypeKeys.length; i++) { const typeKey = pieceTypeKeys[i]; let pX, pY, newPiece; let attempts = 0; const maxAttempts = 100; do { const shapeKey = PieceTypeProgressions[typeKey][1]; // Stage 2 const tempPiece = new this.Piece(this, -1, typeKey, 0, 0); tempPiece.currentShapeKeyIndex = 1; const bounds = tempPiece.getBounds(); // Calculate safe placement bounds based on piece dimensions const minX = -bounds.left + 1; const maxX = this.options.outerGridWidth - bounds.right - 2; const minY = -bounds.top + 1; const maxY = this.options.outerGridHeight - bounds.bottom - 2; // If piece is too large to fit even once, skip if (minX > maxX || minY > maxY) { console.warn(`Piece too large to place: ${typeKey}`); break; } pX = this._randInt(minX, maxX); pY = this._randInt(minY, maxY); newPiece = new this.Piece( this, this._pieceIdCounter++, typeKey, pX, pY ); newPiece.currentShapeKeyIndex = 1; // Start at stage 2 attempts++; if (attempts > maxAttempts) { newPiece = null; break; } } while ( this._isOutOfBounds(newPiece) || this._checkPieceToPieceCollision( newPiece, newPiece.x, newPiece.y, null ) ); if (newPiece) { this._allPieces.push(newPiece); } else { console.warn( "Could not place a piece after " + maxAttempts + " attempts." ); } } }, _bindEvents: function () { const self = this; this._canvas.on("mousedown.dynPuzzle", function (e) { self._handleMouseDown(e); }); $(document).on("keydown.dynPuzzle", function (e) { if (e.key.toLowerCase() === "r") { e.preventDefault(); // Prevent default 'r' behavior like page reload const targetPiece = self._draggingPiece || self._selectedPiece; if (targetPiece && targetPiece.rotate("right")) { self._render(); } } }); $("#inner-square-increase").on("click.dynPuzzle", function () { self._resizeInnerSquare(1); }); $("#inner-square-decrease").on("click.dynPuzzle", function () { self._resizeInnerSquare(-1); }); $("#piece-transform-next").on("click.dynPuzzle", function () { self._transformSelectedPiece("next"); }); $("#piece-transform-prev").on("click.dynPuzzle", function () { self._transformSelectedPiece("prev"); }); }, _isPieceInsideInnerSquare: function (piece) { const blocks = piece.getBlocks(); for (let i = 0; i < blocks.length; i += 2) { const bx = piece.x + blocks[i]; const by = piece.y + blocks[i + 1]; if ( bx < this._innerSquare.x || bx >= this._innerSquare.x + this._innerSquare.size || by < this._innerSquare.y || by >= this._innerSquare.y + this._innerSquare.size ) { return false; } } return true; }, _checkAllPiecesInsideInnerSquare: function () { for (const piece of this._allPieces) { if (!this._isPieceInsideInnerSquare(piece)) return false; } return true; }, _handleMouseDown: function (e) { const rect = this._canvas[0].getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const clickGridX = Math.floor(mouseX / this.options.blockSize); const clickGridY = Math.floor(mouseY / this.options.blockSize); let clickedPiece = null; for (let i = this._allPieces.length - 1; i >= 0; i--) { // Iterate backwards for Z-order if (this._allPieces[i].isBlockAt(clickGridX, clickGridY)) { clickedPiece = this._allPieces[i]; break; } } if (clickedPiece) { this._selectedPiece = clickedPiece; this._draggingPiece = clickedPiece; this._isDragging = true; // Bring to front for rendering by moving to end of array this._allPieces.splice(this._allPieces.indexOf(clickedPiece), 1); this._allPieces.push(clickedPiece); // Offset of the specific block clicked within the piece's local coordinate system this._pieceDragOffsetBlocksX = clickGridX - clickedPiece.x; this._pieceDragOffsetBlocksY = clickGridY - clickedPiece.y; $(document).on( "mousemove.dynPuzzle", this._handleMouseMove.bind(this) ); $(document).on( "mouseup.dynPuzzle", this._handleMouseUp.bind(this) ); this.options.onPieceSelect.call( this.element, this._selectedPiece ); } else { this._selectedPiece = null; } this._updateSelectedPieceUI(); this._render(); }, _handleMouseMove: function (e) { if (!this._isDragging || !this._draggingPiece) return; const rect = this._canvas[0].getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; // Target for the piece's reference point (0,0) const targetPieceRefX = Math.floor(mouseX / this.options.blockSize) - this._pieceDragOffsetBlocksX; const targetPieceRefY = Math.floor(mouseY / this.options.blockSize) - this._pieceDragOffsetBlocksY; this._draggingPiece._ghostX = targetPieceRefX; this._draggingPiece._ghostY = targetPieceRefY; this._render(); }, _handleMouseUp: function (e) { $(document).off("mousemove.dynPuzzle mouseup.dynPuzzle"); if (!this._isDragging || !this._draggingPiece) return; const ghost = this._draggingPiece; // only do anything if we actually set _ghostX/Y on mousemove if (ghost._ghostX !== undefined && ghost._ghostY !== undefined) { const targetX = ghost._ghostX; const targetY = ghost._ghostY; if ( !this._isOutOfBounds(ghost, targetX, targetY) && !this._checkPieceToPieceCollision( ghost, targetX, targetY, ghost ) ) { ghost.x = targetX; ghost.y = targetY; } } // clean up delete ghost._ghostX; delete ghost._ghostY; this._isDragging = false; this._draggingPiece = null; this._render(); }, _resizeInnerSquare: function (delta) { const newSize = this._innerSquare.size + delta * 2; if ( newSize >= this.options.minInnerSquareSize && newSize <= this.options.maxInnerSquareSize ) { this._innerSquare.size = newSize; this._innerSquare.x = Math.floor( (this.options.outerGridWidth - this._innerSquare.size) / 2 ); this._innerSquare.y = Math.floor( (this.options.outerGridHeight - this._innerSquare.size) / 2 ); $("#inner-square-size-display").text(this._innerSquare.size); this.options.onInnerSquareResize.call(this.element, newSize); this._updateBudgetValue(); this._render(); } }, _transformSelectedPiece: function (direction) { if ( this._selectedPiece && this._selectedPiece.transformShape(direction) ) { this._updateSelectedPieceUI(); this._render(); } }, _updateSelectedPieceUI: function () { if (this._selectedPiece) { const idx = this._selectedPiece.currentShapeKeyIndex; let symbol = "¢"; let weight = 600; if (idx === 1) { symbol = "$"; weight = 400; } else if (idx === 2) { symbol = "$"; weight = 600; } else if (idx === 3) { symbol = "$"; weight = 900; } else if (idx >= 4) { symbol = "$$"; weight = 1000; } $("#piece-value-symbol").text(symbol).css("font-weight", weight); // map typeKey to a human-friendly name and description const typeNames = { typeA: "The Ottawa Mission", typeB: "Shepherds of Good Hope", typeC: "Cornerstone Housing for Women", typeD: "Youth Services Bureau", typeE: "Montfort Renaissance", typeF: "CCOC Housing" }; const typeDescriptions = { typeA: "Provides emergency shelter and support services in Ottawa. In 2024, the City of Ottawa provided $5,504,633 in funding.", typeB: "A harm‑reduction and housing‑first agency offering specialized programs for chronically homeless individuals; City funding $8,980,769 in 2024.", typeC: "A women‑only emergency shelter and long‑term housing provider with a trauma‑informed approach; expanded to 150 beds in 2024; City funding $340,765.", typeD: "Operates two youth‑dedicated emergency shelters and longer‑term housing, focusing on family reconnection and education/employment; City funding $5,098,345 in 2024.", typeE: "French‑language service focusing on addictions, mental health, housing, and seniors; City funding $1,417,102 in 2024.", typeF: "Affordable housing provider using a self‑sustaining mortgage model, gradually reducing homelessness over time." }; const key = this._selectedPiece.typeKey; $("#selected-piece-id").text(typeNames[key] || key); $("#selected-piece-description") .text(typeDescriptions[key] || "") .toggle(Boolean(typeDescriptions[key])); $("#selected-piece-shape-name").text( this._selectedPiece.getCurrentShapeDefinition().name ); $("#selected-piece-info").show(); $("#no-piece-selected-msg").hide(); } else { $("#selected-piece-info").hide(); $("#selected-piece-description").hide(); $("#no-piece-selected-msg").show(); } }, _isOutOfBounds: function (piece, newX, newY) { const x = newX !== undefined ? newX : piece.x; const y = newY !== undefined ? newY : piece.y; const blocks = piece.getBlocks(); if (!blocks || blocks.length === 0) return false; // No blocks, can't be out of bounds for (let i = 0; i < blocks.length; i += 2) { const bx = x + blocks[i]; const by = y + blocks[i + 1]; if ( bx < 0 || bx >= this.options.outerGridWidth || by < 0 || by >= this.options.outerGridHeight ) { return true; } } return false; }, _checkPieceToPieceCollision: function ( pieceToCheck, newX, newY, ignorePieceInstance ) { const blocksToCheck = pieceToCheck.getBlocks(); if (!blocksToCheck || blocksToCheck.length === 0) return false; for (let i = 0; i < blocksToCheck.length; i += 2) { const checkGridX = newX + blocksToCheck[i]; const checkGridY = newY + blocksToCheck[i + 1]; for (const otherPiece of this._allPieces) { if (otherPiece === ignorePieceInstance) continue; if (otherPiece.isBlockAt(checkGridX, checkGridY)) { return true; } } } return false; }, _render: function () { this._ctx.clearRect( 0, 0, this._canvas.width(), this._canvas.height() ); this._drawOuterGrid(); for (const piece of this._allPieces) { if (piece !== this._draggingPiece) { // Don't draw the actual piece if it's being dragged (ghost will be drawn) this._drawPiece(piece, piece === this._selectedPiece); } } if ( this._isDragging && this._draggingPiece && this._draggingPiece._ghostX !== undefined ) { // Check if ghost position is set this._drawPieceGhost( this._draggingPiece, this._draggingPiece._ghostX, this._draggingPiece._ghostY ); } this._drawInnerSquare(); const allInside = this._checkAllPiecesInsideInnerSquare(); $("#submit-button").prop("disabled", !allInside); }, _drawOuterGrid: function () { this._ctx.fillStyle = this._theme.background; this._ctx.fillRect( 0, 0, this._canvas.width(), this._canvas.height() ); this._ctx.strokeStyle = this._theme.gridLineColor; this._ctx.lineWidth = 0.5; // Thinner lines for (let x = 0; x <= this.options.outerGridWidth; x++) { this._ctx.beginPath(); this._ctx.moveTo(x * this.options.blockSize + 0.5, 0); // +0.5 for crisp lines this._ctx.lineTo( x * this.options.blockSize + 0.5, this.options.outerGridHeight * this.options.blockSize ); this._ctx.stroke(); } for (let y = 0; y <= this.options.outerGridHeight; y++) { this._ctx.beginPath(); this._ctx.moveTo(0, y * this.options.blockSize + 0.5); this._ctx.lineTo( this.options.outerGridWidth * this.options.blockSize, y * this.options.blockSize + 0.5 ); this._ctx.stroke(); } }, _drawInnerSquare: function () { this._ctx.strokeStyle = this._theme.innerSquareBorderColor; this._ctx.lineWidth = 2; this._ctx.strokeRect( this._innerSquare.x * this.options.blockSize, this._innerSquare.y * this.options.blockSize, this._innerSquare.size * this.options.blockSize, this._innerSquare.size * this.options.blockSize ); }, _drawPiece: function (piece, isSelected) { const blocks = piece.getBlocks(); if (!blocks || blocks.length === 0) return; this._ctx.fillStyle = piece.color; this._ctx.strokeStyle = isSelected ? this._theme.selectedPieceStrokeColor : this._theme.pieceStrokeColor; this._ctx.lineWidth = isSelected ? this._theme.selectedPieceStrokeWidth : this._theme.pieceStrokeWidth; for (let i = 0; i < blocks.length; i += 2) { // Adjust for crisp lines if lineWidth is odd const strokeOffset = this._ctx.lineWidth % 2 === 1 ? 0.5 : 0; const x = (piece.x + blocks[i]) * this.options.blockSize + strokeOffset; const y = (piece.y + blocks[i + 1]) * this.options.blockSize + strokeOffset; const s = this.options.blockSize; this._ctx.fillRect(x, y, s, s); this._ctx.strokeRect(x, y, s, s); } }, _drawPieceGhost: function (piece, ghostX, ghostY) { const blocks = piece.getBlocks(); if (!blocks || blocks.length === 0) return; const isValidMove = !this._isOutOfBounds(piece, ghostX, ghostY) && !this._checkPieceToPieceCollision(piece, ghostX, ghostY, piece); this._ctx.fillStyle = isValidMove ? this._theme.ghostFillColorValid : this._theme.ghostFillColorInvalid; this._ctx.strokeStyle = this._theme.ghostStrokeColor; this._ctx.lineWidth = 1; for (let i = 0; i < blocks.length; i += 2) { const strokeOffset = 0.5; // For 1px line width const x = (ghostX + blocks[i]) * this.options.blockSize + strokeOffset; const y = (ghostY + blocks[i + 1]) * this.options.blockSize + strokeOffset; const s = this.options.blockSize; this._ctx.fillRect(x, y, s, s); this._ctx.strokeRect(x, y, s, s); } }, theme: function (newThemeNameOrObject) { if (typeof newThemeNameOrObject === "undefined") { return this.options.theme; // Return current theme name or null if custom object } let themeObj; if (typeof newThemeNameOrObject === "string") { this.options.theme = newThemeNameOrObject; themeObj = DynamicBlockThemes[newThemeNameOrObject] || DynamicBlockThemes.default; } else { this.options.theme = null; // Custom theme object themeObj = newThemeNameOrObject; } this._theme = $.extend( true, {}, DynamicBlockThemes.default, themeObj ); // Deep merge with default if (this._allPieces.length > 0) { this._allPieces.forEach((p) => { p.color = this._theme.pieceColors[p.typeKey] || "#BDC3C7"; // Fallback color }); } if (this._ctx) this._render(); // Re-render if canvas exists }, _randInt: function (a, b) { return a + Math.floor(Math.random() * (1 + b - a)); }, _destroy: function () { this._canvas.off(".dynPuzzle"); $(document).off(".dynPuzzle"); $( "#inner-square-increase, #inner-square-decrease, #piece-transform-next, #piece-transform-prev" ).off(".dynPuzzle"); this.element.empty().removeClass("dynamic-puzzle-holder"); }, }); })(jQuery); // --- Initialize --- $(document).ready(function () { $("#game-area").dynamicBlockPuzzle({ outerGridWidth: 15, outerGridHeight: 15, blockSize: 34, initialInnerSquareSize: 7, numPieces: 8, theme: "candy", onPieceSelect: function (p) { console.log("Selected Piece ID:", p ? p.id : "None"); }, }); }); $(document).on("click", "#submit-button", function () { console.log("clicked"); const puzzle = $("#game-area").dynamicBlockPuzzle("instance"); // Register Ottawa Mission policy PolicyManager.register({ name: "The Ottawa Mission", costUpfront: 0, duration: puzzle._allPieces.find(p => p.typeKey === "typeA")?.currentShapeKeyIndex || 1, immediate(v) { // no immediate effect }, tick(v, age) { // Shift 5% of unsheltered into shelter const shifted = Math.round(v.pop.unsheltered * 0.05); v.pop.unsheltered -= shifted; v.pop.sheltered += shifted; // 1% of total homeless exit via wrap-around care const exits = Math.round(v.homelessTotal * 0.01); window.Util.exitPeople(exits); // Public opinion boost v.publicOpinion += 5; // Capacity down v.systemCapacity -= v.systemCapacity * 0.01; }, }); // Register Shepherds of Good Hope policy PolicyManager.register({ name: "Shepherds of Good Hope", costUpfront: 0, costPerTurn: 0, duration: puzzle._allPieces.find(p => p.typeKey === "typeB")?.currentShapeKeyIndex || 1, immediate(v) { // no immediate effect }, tick(v) { // Shift 3% of unsheltered into shelter const usToSh = Math.round(v.pop.unsheltered * 0.03); v.pop.unsheltered -= usToSh; v.pop.sheltered += usToSh; // Shift 3% of institutional into shelter const instToSh = Math.round(v.pop.institutional * 0.03); v.pop.institutional -= instToSh; v.pop.sheltered += instToSh; // 1% exit via wrap-around care const exits = Math.round(v.homelessTotal * 0.01); window.Util.exitPeople(exits); // Public opinion hit v.publicOpinion -= 5; // Capacity drain v.systemCapacity -= v.systemCapacity * 0.005; }, }); // Register Cornerstone Housing for Women policy PolicyManager.register({ name: "Cornerstone Housing for Women", costUpfront: 0, duration: puzzle._allPieces.find(p => p.typeKey === "typeC")?.currentShapeKeyIndex || 1, immediate(v) { // no immediate effect }, tick(v) { // 4% of unsheltered exit via wrap-around care const exits = Math.round(v.pop.unsheltered * 0.04); v.pop.unsheltered -= exits; window.Util.exitPeople(exits); // Public opinion boost v.publicOpinion += 3; }, }); // Register Youth Services Bureau policy PolicyManager.register({ name: "Youth Services Bureau", costUpfront: 0, duration: puzzle._allPieces.find(p => p.typeKey === "typeD")?.currentShapeKeyIndex || 1, immediate(v) { // no immediate effect }, tick(v) { // 8% of unsheltered youth exit via wrap-around care const exits = Math.round(v.pop.unsheltered * 0.08); v.pop.unsheltered -= exits; window.Util.exitPeople(exits); // Public opinion boost v.publicOpinion += 5; }, }); // Register Montfort Renaissance policy PolicyManager.register({ name: "Montfort Renaissance", costUpfront: 1, duration: puzzle._allPieces.find(p => p.typeKey === "typeE")?.currentShapeKeyIndex || 1, immediate(v) { // no immediate effect }, tick(v) { // 1% of total homeless exit via wrap-around care const exits = Math.round(v.homelessTotal * 0.01); window.Util.exitPeople(exits); // Public opinion boost v.publicOpinion += 5; }, }); // Register CCOC Housing policy PolicyManager.register({ name: "CCOC Housing", costUpfront: 0, duration: puzzle._allPieces.find(p => p.typeKey === "typeF")?.currentShapeKeyIndex || 1, immediate(v) { // no immediate effect }, tick(v) { // 0.2% of total homeless exit each tick via housing exits const exits = Math.round(v.homelessTotal * 0.002); window.Util.exitPeople(exits); // Public opinion boost v.publicOpinion += 1; }, }); const allocated = parseInt($("#budget-val1").text(), 10); PolicyManager.register({ name: "Budget Allocation", costUpfront: allocated, duration: 1, }); Engine.play("ChooseC4Project1"); }); <</script>> <style> /* styles.css - Light Mode */ body { font-family: sans-serif; display: flex; justify-content: center; align-items: flex-start; margin-top: 20px; } .puzzle-layout { margin-top: 20px; display: flex; flex-direction: column; align-items: center; gap: 20px; } .dynamic-puzzle-holder { } #game-area canvas { display: block; background-color: #ffffff; /* Light canvas background */ border: 1px solid #dcdcdc; /* Subtle border for contrast */ } .dollar-sign { font-family: "Spectral", serif; font-size: 30px; font-weight: 400; color: #0066cc; } .submit-btn { background-color: #ffffff; color: #111827; /* Tailwind slate-900 */ border: 1px solid #e5e7eb; /* Tailwind gray-200 */ padding: 0.5rem 0.75rem; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; line-height: 1.25; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; transition: all 0.2s ease-in-out; margin: 0.3rem; position: relative; top: -0.25rem; } .submit-btn:disabled { opacity: 0.35; background-color: #f3f4f6; /* Tailwind gray-100 */ color: #9ca3af; /* Tailwind gray-400 */ border-color: #d1d5db; /* Tailwind gray-300 */ cursor: not-allowed; box-shadow: none; } .title { font-family: "Inter", Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; color: #383838; text-transform: uppercase; } .digits2 { font-family: "Spectral", serif; font-size: 32px; font-weight: 600; color: #000000; } .controls-panel { width: 510px; padding: 20px; background-color: #ffffff; border-radius: 5px; border-radius: 15px; border-color: rgba(156, 162, 166, 0.32); border-width: 1px; border-style: solid; } .controls-panel h3 { margin-top: 0; margin-bottom: 12px; font-size: 1.1em; color: #2980b9; /* Accent color for headings */ border-bottom: 1px solid #ecf0f1; padding-bottom: 8px; } .controls-panel p { margin-top: 5px; font-family: "Inter", Helvetica, Arial, sans-serif; margin-bottom: 10px; font-size: 0.9em; line-height: 1.4; color: #2c3e50; } .control-group { margin-bottom: 18px; } .control-group label { display: block; margin-bottom: 5px; font-size: 0.9em; color: #555555; } .selected-piece-id { font-family: "Inter", Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; color: #383838; text-transform: uppercase; } .control-btn, #inner-square-size-display, #selected-piece-shape-name { background-color: #ffffff; color: #4d4d4d; border: 1px solid #e5e7eb; padding: 0.5rem 0.75rem; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; line-height: 1.25; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; transition: all 0.2s ease-in-out; margin: 0.3rem; position: relative; top: -0.25rem; } .control-btn:hover { background-color: #f9fafb; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); transform: scale(1.02); } .control-btn:active { background-color: #f3f4f6; transform: scale(0.96); } .control-btn:focus { outline: 2px solid #2563eb; outline-offset: 2px; } #inner-square-size-display, #selected-piece-shape-name { cursor: default; background-color: #e8e8e8; padding: 8px; display: inline-block; min-width: 30px; text-align: center; } #selected-piece-controls-group { margin-top: 15px; } #no-piece-selected-msg { font-style: italic; color: #999999; font-size: 0.85em; } .passage { width: 100% !important; } </style>
<style> button { background-color: #ffffff !important; color: #111827 !important; border: 1px solid #e5e7eb !important; padding: 0.5rem 0.75rem !important; border-radius: 0.5rem !important; font-size: 0.875rem !important; font-weight: 500 !important; line-height: 1.25 !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; cursor: pointer !important; transition: all 0.2s ease-in-out !important; margin: 0.3rem !important; position: relative !important; top: -0.25rem !important; text-align: center !important; margin: 0.3rem auto; /* centers it */ position: relative; } </style> <<script>> PolicyManager.register({ name: "Permit Shelters in All Urban Zones", costUpfront: 2, costPerTurn: 0, // Small ongoing admin/monitoring cost duration: 999999, immediate(v) { if (v.publicOpinion > 15) v.publicOpinion -= 5; if (v.systemCapacity < 180) v.systemCapacity += 15; v.budget += 8; v.politicalCapital -=1; }, tick(v, age) { if (age > 0 && age <= 16) { if (age % 4 === 0) { let toShelter = Math.min(v.pop.unsheltered, 50); v.pop.unsheltered -= toShelter; v.pop.sheltered += toShelter; } } if (age >= 1) { if (v.inflowShare.unsheltered > 0.05) { v.inflowShare.unsheltered = Math.max(0.01, v.inflowShare.unsheltered - 0.005); // Slower reduction than before v.inflowShare.sheltered = Math.min(0.9, v.inflowShare.sheltered + 0.005); window.Util.renorm(v.inflowShare); } } if (age > 0 && age % 8 === 0 && v.publicOpinion < 70) v.publicOpinion +=2; } }); <</script>> <div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">System Capacity</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">200</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> beds </span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Unsheltered Inflow</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">15</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> % / year</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Provincial Funding Access</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">8</span> <span class="impact-subtitle" style="font-size: 1.4em; margin-bottom: -3px;"> M</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em">In February 2025, Ottawa City Council passed a motion permitting shelters in all urban zones, removing zoning barriers that had delayed new sites for months. This expands system capacity, speeds up approvals, and unlocks access to new provincial and federal funds. Advocates welcome the change, though some warn it must be paired with long-term housing investments to avoid deepening reliance on temporary shelter. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('BuildCanadaHomes');">Continue</button> <<button "Back">> <<run Engine.backward()>> <</button>> </div>
<<set $turn += 1>> <<script>>window.PolicyManager.tick();<</script>> <canvas id="snowCanvas"></canvas> <style> canvas#snowCanvas { position: fixed; top: 0; left: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; background: #e5f5ff; } .passage-content { position: relative; z-index: 1; } #timeline { background: #e5f5ff !important; } .button { background-color: rgb(32, 117, 202); background-image: linear-gradient(rgb(41, 122, 202), rgb(30, 104, 178)); border: 1px solid rgb(38, 111, 185); border-radius: 12px; color: #fff; padding: 0.65rem 1rem; font-size: 1rem; display: block; margin: 0.9rem auto; cursor: pointer; max-width: 420px; } .button:hover { background-image: linear-gradient(rgb(51, 130, 210), rgb(39, 121, 202)); } label.checkbox { display: flex; align-items: center; cursor: pointer; margin: 0.75em 0; user-select: none; } label.checkbox input { -webkit-appearance: none; appearance: none; margin: 0; width: 1.2em; height: 1.2em; border: 2px solid #cbd5e1; border-radius: 0.25rem; position: relative; transition: background-color 0.2s, border-color 0.2s; display: inline-block; } label.checkbox input:checked { background-color: #3b82f6; border-color: #3b82f6; } label.checkbox input:checked::after { content: ""; position: absolute; top: 0.15em; left: 0.35em; width: 0.25em; height: 0.6em; border: solid white; border-width: 0 0.2em 0.2em 0; transform: rotate(45deg); } label.checkbox span { margin-left: 0.5em; font-size: 1em; color: #1f2937; } .impact-box { background-color: rgba(255, 255, 255, 0.6); } .impact-unit { font-weight: 700 !important; } .question-wrapper { background: rgba(255, 255, 255, 0.6); border-radius: 12px; padding: 1.5rem; margin-top: 1.5rem; } @keyframes tick { from { } to { } } .animate-number { display: inline-block; /* animation: tick 0.5s ease-out; */ } </style> <div class="build-intro" style=" color: #1c1c1e; background-color: rgba(255, 255, 255, 0.35); border-radius: 10px; " > <div class="policy-impact-screen" id="snowMetrics"> <div class="impact-container"> <div class="impact-box"> <div class="impact-title">Lives at Risk Averted</div> <div class="impact-content"> <span class="impact-value" id="metric-lives">0</span> <span class="impact-subtitle"> people</span> </div> </div> <div class="impact-box"> <div class="impact-title">System Strain</div> <div class="impact-content"> <span class="impact-unit">% </span> <span class="impact-value" id="metric-strain">0</span> </div> </div> <div class="impact-box"> <div class="impact-title">Vulnerable Reach</div> <div class="impact-content"> <span class="impact-unit">% </span> <span class="impact-value" id="metric-reach">0</span> </div> </div> </div> <!-- ✅ Choices Form --> <div class="question-wrapper"> <p> A severe snowstorm has struck the city. Which emergency measures will you put in place? </p> <form id="snowChoices"> <label class="checkbox" ><input type="checkbox" id="snow-opt1" /><span >Open community centres as emergency warming shelters</span ></label > <label class="checkbox" ><input type="checkbox" id="snow-opt2" /><span >Issue emergency motel stay vouchers</span ></label > <label class="checkbox" ><input type="checkbox" id="snow-opt3" /><span >Distribute thermal blankets and hot meals</span ></label > <label class="checkbox" ><input type="checkbox" id="snow-opt4" /><span >Deploy mobile warming buses on major routes</span ></label > <label class="checkbox" ><input type="checkbox" id="snow-opt5" /><span >Coordinate volunteer check-in phone calls to isolated individuals</span ></label > <label class="checkbox" ><input type="checkbox" id="snow-opt6" /><span >Enforce an overnight curfew in public spaces</span ></label > </form> </div> <div id="warning" style="display: none; color: #b91c1c; margin-top: 1em"> ⚠️ You must choose at least one response before continuing. </div> <button class="button" onclick="handleContinue()">Continue</button> </div> </div> <<script>> setTimeout(function(){ // ❄️ Snow Animation const canvas = document.getElementById('snowCanvas'), ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const numFlakes = 300, flakes = []; function Flake(x,y,r,d){ this.x=x; this.y=y; this.r=r; this.d=d; this.update=()=>{ this.y+=Math.cos(this.d)+1+this.r/2; this.x+=Math.sin(this.d)*2; if(this.y>canvas.height){ this.y=-10; this.x=Math.random()*canvas.width; } }; this.draw=()=>{ ctx.beginPath(); ctx.arc(this.x,this.y,this.r,0,Math.PI*2); ctx.fillStyle="#fff"; ctx.fill(); }; } for(let i=0;i<numFlakes;i++){ flakes.push(new Flake( Math.random()*canvas.width, Math.random()*canvas.height, Math.random()*3+1, Math.random()*Math.PI )); } function drawFlakes(){ ctx.clearRect(0,0,canvas.width,canvas.height); flakes.forEach(f=>{ f.update(); f.draw(); }); requestAnimationFrame(drawFlakes); } drawFlakes(); window.addEventListener("resize",()=>{ canvas.width=window.innerWidth; canvas.height=window.innerHeight; }); // 🎯 Continue button logic window.handleContinue = function(){ const any = document.querySelectorAll('#snowChoices input:checked').length; const w = document.getElementById('warning'); if(!any) w.style.display='block'; else SugarCube.Engine.play('Bill6'); }; // Helper function to animate number changes function animateNumber(el, start, end) { if (start === end) { el.textContent = end; return; } let current = start; const step = end > start ? 1 : -1; const duration = 500; const intervalTime = Math.max(Math.floor(duration / Math.abs(end - start)), 16); el.classList.add('animate-number'); const timer = setInterval(() => { current += step; el.textContent = current; if (current === end) { clearInterval(timer); el.classList.remove('animate-number'); } }, intervalTime); } // 📊 Impact Logic const snowImpacts = { "snow-opt1": { lives: 57, strain: 40, reach: 20 }, "snow-opt2": { lives: 49, strain: 45, reach: 10 }, "snow-opt3": { lives: 24, strain: 10, reach: 30 }, "snow-opt4": { lives: 21, strain: 25, reach: 40 }, "snow-opt5": { lives: 18, strain: 10, reach: 20 }, "snow-opt6": { lives: 6, strain: 20, reach: -30 } }; function updateSnowMetrics() { const livesEl = document.getElementById('metric-lives'); const prevLives = parseInt(livesEl.textContent, 10); const strainEl = document.getElementById('metric-strain'); const prevStrain = parseInt(strainEl.textContent, 10); const reachEl = document.getElementById('metric-reach'); const prevReach = parseInt(reachEl.textContent, 10); let totalLives = 0, totalStrain = 0, totalReach = 0; Object.entries(snowImpacts).forEach(([id, impact]) => { const cb = document.getElementById(id); if (cb && cb.checked) { totalLives += impact.lives; totalStrain += impact.strain; totalReach += impact.reach; } }); totalStrain = Math.min(totalStrain, 100); totalReach = Math.min(totalReach, 100); totalReach = Math.max(totalReach, 0); // document.getElementById('metric-lives').textContent = totalLives; // document.getElementById('metric-strain').textContent = totalStrain; // document.getElementById('metric-reach').textContent = totalReach; animateNumber(livesEl, prevLives, totalLives); animateNumber(strainEl, prevStrain, totalStrain); animateNumber(reachEl, prevReach, totalReach); const strainSubtitle = document.querySelector('#metric-strain').nextElementSibling; if (strainSubtitle) strainSubtitle.textContent = ' %'; document.getElementById('metric-reach').textContent = totalReach; const reachSubtitle = document.querySelector('#metric-reach').nextElementSibling; if (reachSubtitle) reachSubtitle.textContent = ' %'; } updateSnowMetrics(); // initial render if (typeof animateImpactValues === 'function') { animateImpactValues(); } document.querySelectorAll('#snowChoices input[type=checkbox]').forEach(cb => { cb.addEventListener('change', function(e) { const warning = document.getElementById('warning'); warning.style.display = 'none'; const checkedBoxes = document.querySelectorAll('#snowChoices input[type=checkbox]:checked'); if (checkedBoxes.length > 3) { e.target.checked = false; warning.textContent = '⚠️ You can only select up to 3 options.'; warning.style.display = 'block'; setTimeout(() => warning.style.display = 'none', 3000); } updateSnowMetrics(); if (typeof animateImpactValues === 'function') animateImpactValues(); }); }); }, 0); <</script>>
<style> body { background: #e5f5ff; } #timeline { background: #e5f5ff !important; } </style> history of snow storms in canadian cities. what impact do they have on homeless populations. what measures have various cities taken and their effectivness. why might people take their chances outside?
<<set $history = []>> <<set $turn = 1>> <<set $year= 1>> <<set $currentYear= 2024>> <<set $budget = 215>> <<set $homelessTotal = 2952>> <!-- Demographic breakdown of CURRENT homeless population --> <<set $pop = { // Housing status unsheltered: 472, // 16% of 2952 sheltered: 1269, // 43% transitional: 708, // 24% institutional: 59, // 2% chronic: 1446, // 49% withChildren: 295, // 10% fosterCareHistory: 561, // 19% // Identity demographics male: Math.round(2952 * 0.56), female: Math.round(2952 * 0.36), transNB: Math.round(2952 * 0.02), age25to49: Math.round(2952 * 0.58), youth: Math.round(2952 * 0.13), lgbq: Math.round(2952 * 0.11), lgbtqYouth: Math.round(2952 * 0.11 * 0.21), racialized: Math.round(2952 * 0.56), indigenous: 479, // exact number provided veteran: Math.round(2952 * 0.04), immigrant: Math.round(2952 * 0.42) }>> <!-- INFLOW system --> <<set $inflowBase = 0>> <!-- Number of new homeless per turn --> <!-- Percentage distribution of INCOMING new homelessness cases --> <<set $inflowShare = { // Housing status (default to unsheltered for new arrivals) unsheltered: 0.16, sheltered: 0.43, transitional: 0.24, institutional: 0.02, chronic: 0.49, withChildren: 0.10, fosterCareHistory: 0.19, // Identity demographics male: 0.56, female: 0.36, transNB: 0.02, lgbtqYouth: 0.21, age25to49: 0.58, lgbq: 0.11, racialized: 0.56, indigenous: 479 / 2952, // ~16.2% veteran: 0.04, immigrant: 0.42 }>> <!-- System + policy levers --> <<set $publicOpinion = 50>> <<set $serviceCoordination = 60>> <<set $systemCapacity = 100>> <<set $politicalCapital = 3>> <<script>> // Create an initial snapshot representing the state *before* year 1 begins. // This helps in comparing the first year's results against something. const v = State.variables; const initialPopSnapshot = {}; // Deep copy $pop for (const key in v.pop) { if (Object.prototype.hasOwnProperty.call(v.pop, key)) { initialPopSnapshot[key] = v.pop[key]; } } const initialSnapshot = { year: 0, // Conceptual "Year 0" or pre-game state turn: 0, // Conceptual "Turn 0" homelessTotal: v.homelessTotal, pop: initialPopSnapshot, budget: v.budget, publicOpinion: v.publicOpinion, serviceCoordination: v.serviceCoordination, systemCapacity: v.systemCapacity, politicalCapital: v.politicalCapital }; v.history.push(initialSnapshot); <</script>>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title" style="white-space: nowrap; display: inline;">Housing Supply</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">500</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Long-Term Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">2<span class="impact-subtitle"> M / year</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Upfront Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">10<span class="impact-subtitle"> M</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em"> A familiar playbook returns: low-cost, fast-track starter homes on the urban fringe. It won’t solve sprawl, but it will help hundreds of families buy their first home. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PreServiceFunding');">Continue</button> </div>
<!-- ===== PERSISTENT UI ===== --> <div id="spent-ui"> <div id="passages"> <div id="passage-start" data-passage="start" class="passage"></div> </div> <!-- Timeline --> <div id="timeline" class="timeline active"> <span class="day active" id="year_2024"> <span class="container"><span class="number">2024</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2025"> <span class="container"><span class="number">2025</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2026"> <span class="container"><span class="number">2026</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2027"> <span class="container"><span class="number">2027</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2028"> <span class="container"><span class="number">2028</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2029"> <span class="container"><span class="number">2029</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2030"> <span class="container"><span class="number">2030</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2031"> <span class="container"><span class="number">2031</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2032"> <span class="container"><span class="number">2032</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2033"> <span class="container"><span class="number">2033</span></span> </span> <span class="day spacer"></span><span class="day spacer"></span> <span class="day" id="year_2034"> <span class="container"><span class="number">2034</span></span> </span> </div> <!-- Timeline Marker --> <div id="date-marker" class="date-marker active"></div> <div id="timeline-separator"></div> <!-- Date Meter --> <div id="date-meter" class="date-meter"> <div class="title">Year</div> <div class="date"><<= $year || 1 >></div> <div class="days-total">/10</div> </div> <div class="balance-meter" id="balance-meter" data-unit="currency" style="top: 0px"> <div class="title">Budget</div> <div class="balance"> <span class="dollar-sign">$</span> <span class="digits" id="budget-val">100</span> <span class="million">M</span> </div> </div> <!-- Public Opinion --> <div class="balance-meter" id="balance-meter" data-unit="currency" style="top: 100px"> <div class="title">Public Opinion</div> <div class="balance"> <span class="emoji" id="public-opinion-emoji">•_•</span> </div> </div> <!-- Service Coordination (slider) --> <div class="balance-meter" id="balance-meter" data-unit="bar" style="top: 200px"> <div class="title">Service Coordination</div> <div class="balance"> <div class="bar-container"> <div class="bar-fill" id="coordination-bar"></div> </div> </div> </div> <!-- System Capacity --> <div class="balance-meter" id="balance-meter" data-unit="percent" style="top: 320px"> <div class="title">System Capacity</div> <div class="balance"> <span class="unit">%</span> <span class="digits" id="capacity-val">20</span> </div> </div> <!-- Political Capital (dots) --> <div class="balance-meter"id="balance-meter" data-unit="dots" style="top: 440px"> <div class="title">Political Capital</div> <div class="circles"> <span class="circle" data-index="1"></span> <span class="circle" data-index="2"></span> <span class="circle" data-index="3"></span> <span class="circle" data-index="4"></span> </div> </div> <!-- ===== END UI ===== -->
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Clients</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">600<span class="impact-subtitle"> / year</span></span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Annual Operating Cost</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">1.2<span class="impact-subtitle"> M / year</span></span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Service Coordination</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">20<span class="impact-subtitle"> %</span></span> </div> </div> </div> </div> <p style="padding: 0 6rem; font-size: 1.1em"> The Supportive Housing Delivery Office is now up and running—streamlining case management, coordinating builds, and partnering with health services. It enables faster, prioritized placements for those with the highest needs. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('SnowStorm');">Continue</button> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">Unsheltered Inflow</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">10</span> <span class="impact-subtitle"> % / year</span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">5</span> <span class="impact-subtitle"> pts</span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Political Cost</div> <div class="impact-content"> <span style="color: #06C; font-size: 5em; padding-right: 10px;">●</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em"> The Universal Basic Income pilot provides $1,000/month to 1,000 households unconditionally. Proponents highlight its immediate impact on rental arrears and eviction risk; critics caution about long-term budget pressures. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT2');">Continue</button> </div>
<div class="policy-impact-screen"> <div class="impact-container"> <div class="impact-box" data-index="0"> <div class="impact-title">Housing Supply</div> <div class="impact-content"> <span class="impact-icon">↑ </span> <span class="impact-value">50</span> <span class="impact-subtitle"> units / year</span> </div> </div> <div class="impact-box" data-index="1"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-icon">↓ </span> <span class="impact-value">2</span> <span class="impact-subtitle"> pts</span> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Annual Revenue</div> <div class="impact-content"> <span class="impact-unit">$ </span> <span class="impact-value">2.5</span> <span class="impact-subtitle"> M / year</span> </div> </div> </div> </div> <p style="padding:0 6rem;font-size:1.1em"> The city raises its vacancy and speculation tax to 5% and launches an Empty Homes Registry. In the first year, roughly 50 vacant units were returned to the rental market while generating about $2.5M in revenue. While it eases supply pressures, some homeowners argue it unfairly penalizes secondary residences. </p> <div class="centered-btn"> <button class="continue-button" onclick="SugarCube.Engine.play('PIT2');">Continue</button> </div>
<style> html, body { margin: 0; padding: 0; height: 100%; font-family: system-ui, sans-serif; } #continue-zoning { margin-top: 1rem; padding: 0.5rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; background: #ffffff; cursor: pointer; font-size: 0.875rem; font-weight: 500; text-align: center; } #container { position: relative; height: 90vh; margin: auto; top: 5vh; border-radius: 8px; overflow: hidden; } #map { width: 100%; height: 100%; } #menu { position: absolute; top: 0.5rem; left: 1rem; z-index: 2; background: #fff; padding: 1rem; border-radius: 6px; width: 220px; display: flex; flex-direction: column; } .mapboxgl-ctrl-bottom-right{ display: none; } /* New Continue button style */ #continue-zoning { margin-top: 0.75rem; padding: 0.5rem; background: #111827 !important; color: #fff !important; border: none; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; width: 100%; /* fill the menu width */ height: auto; /* auto height so it’s not too tall */ } #continue-zoning:hover { background: #1f2937; } </style> <div id="container"> <div id="map"></div> <div id="menu"> <h3>ZONING CHOICES</h3> <div class="layer-option"> <input type="checkbox" id="expand" /> <label for="expand">Expand Suburbia</label> </div> <div class="layer-option"> <input type="checkbox" id="transit" /> <label for="transit">Transit Oriented</label> </div> <div class="layer-option"> <input type="checkbox" id="reserve" /> <label for="reserve">Develop Reserved Zones</label> </div> <div class="layer-option"> <input type="checkbox" id="single" /> <label for="single">Remove Single Family Zoning</label> </div> <!-- Continue button --> <button id="continue-zoning">Continue</button> </div> </div> <script> mapboxgl.accessToken = "pk.eyJ1IjoicWRvZ2dlcjY5IiwiYSI6ImNseDd1MWZ4ZzBrdmsyanB2d3E4MHY4ejQifQ.aXV61OY1jofAOXK_28omhA"; const map = new mapboxgl.Map({ container: "map", style: "mapbox://styles/qdogger69/cmb87asdd012t01qy67cn6dn7", center: [-75.7003, 45.4141], zoom: 13.5, antialias: true, maxZoom: 17.5, minZoom: 10 }); const layerConfigs = { expand: { center: [-75.84776, 45.33199], zoom: 13.5 }, transit: { center: [-75.72607, 45.40851], zoom: 15.5 }, reserve: { center: [-75.6658, 45.40836], zoom: 15.5 }, single: { center: [-75.7003, 45.4141 ], zoom: 14.5 } }; map.on("load", () => { // hide layers initially Object.keys(layerConfigs).forEach(id => { if (map.getLayer(id)) map.setPaintProperty(id, "fill-opacity", 0); }); // fly-to & fade-in layers on toggle Object.entries(layerConfigs).forEach(([id, cfg]) => { document.getElementById(id).addEventListener("change", e => { if (e.target.checked) { map.flyTo({ center: cfg.center, zoom: cfg.zoom, essential: true }); let op = 0; const fade = setInterval(() => { op = Math.min(op + 0.05, 1); map.getLayer(id) && map.setPaintProperty(id, "fill-opacity", op); if (op === 1) clearInterval(fade); }, 50); } else { map.getLayer(id) && map.setPaintProperty(id, "fill-opacity", 0); } }); }); function expandSuburbia() { PolicyManager.register({ name: "Expand Suburbia (Zoning)", costUpfront: 5, costPerTurn: 0.2, duration: 999999, immediate(v) { if(v.publicOpinion > 15) v.publicOpinion -= 3; v.politicalCapital -=1; }, tick(v, age) { if (age >= 4) { window.Util.exitPeople(10, { withChildren: 0.5, sheltered: 0.3 }); v.inflowBase = Math.max(0, v.inflowBase - 2); if (age % 4 === 0 && v.systemCapacity > 50) v.systemCapacity = Math.max(30, v.systemCapacity - 2); if (age % 4 === 0 && v.serviceCoordination > 40) v.serviceCoordination = Math.max(30, v.serviceCoordination - 1); } } }); } function transitOriented() { PolicyManager.register({ name: "Transit-Oriented Zoning Reform", costUpfront: 4, costPerTurn: 0, duration: 999999, immediate(v) { if(v.publicOpinion < 90) v.publicOpinion += 8; if(v.serviceCoordination < 90) v.serviceCoordination += 5; }, tick(v, age) { if (age >= 8) { window.Util.exitPeople(20, { sheltered: 0.4, transitional: 0.3, racialized: 0.2, youth: 0.1 }); v.inflowBase = Math.max(0, v.inflowBase - 3); if (age % 4 === 0 && v.systemCapacity < 150) v.systemCapacity +=2; if (age % 4 === 0 && v.publicOpinion < 70) v.publicOpinion +=1; } } }); } function developReservedZones() { PolicyManager.register({ name: "Develop Reserved/Underused Zones", costUpfront: 12, costPerTurn: 0.1, duration: 999999, immediate(v) { if(v.publicOpinion > 10) v.publicOpinion -= 5; v.politicalCapital -=2; }, tick(v, age) { if (age >= 12) { window.Util.exitPeople(50, { chronic: 0.2, sheltered: 0.3, transitional: 0.3, withChildren: 0.2 }); if (age % 4 === 0 && v.serviceCoordination < 80) v.serviceCoordination += 1; if (age % 4 === 0 && v.systemCapacity < 180) v.systemCapacity +=3; } } }); } function removeSingleFamilyZoning() { PolicyManager.register({ name: "Abolish Single-Family Exclusive Zoning", costUpfront: 3, costPerTurn: 0, duration: 999999, immediate(v) { if(v.publicOpinion > 20) v.publicOpinion -= 15; v.politicalCapital -= 2; }, tick(v, age) { if (age >= 6) { window.Util.exitPeople(25, { sheltered: 0.4, racialized: 0.2, immigrant: 0.2, youth: 0.2 }); v.inflowBase = Math.max(0, v.inflowBase - 4); if (age % 4 === 0 && v.systemCapacity < 160) v.systemCapacity +=1; if (age > 20 && age % 4 === 0 && v.publicOpinion < 50) v.publicOpinion +=1; } } }); } // Continue click: register any checked policies, then advance document.getElementById("continue-zoning").addEventListener("click", () => { if (document.getElementById("expand").checked) expandSuburbia(); if (document.getElementById("transit").checked) transitOriented(); if (document.getElementById("reserve").checked) developReservedZones(); if (document.getElementById("single").checked) removeSingleFamilyZoning(); // Advance to your next passage (adjust the name if needed) SugarCube.Engine.play("Pilots"); }); }); </script>
<div class="policy-impact-screen" > <div class="impact-container"> <div class="impact-box" data-index="1"> <div class="impact-title">Economic Growth</div> <div class="impact-content"> <div id="impact-chart-1" class="impact-graph"></div> </div> </div> <div class="impact-box" data-index="2"> <div class="impact-title">Public Opinion</div> <div class="impact-content"> <span class="impact-number">+5%</span> </div> </div> <div class="impact-box" data-index="3"> <div class="impact-title">Service Coordination</div> <div class="impact-content"> <div class="impact-widget">★ ★ ★ ☆ ☆</div> </div> </div> </div> </div> <p>In January 2025, Ottawa City Council voted to move forward with developing a renovictions bylaw, following intense advocacy from tenant groups and mounting evidence of widespread misuse of eviction laws. The motion, introduced by Councillor Ariel Troster, came in response to a sharp increase—nearly 500% over three years—in N13 “renoviction” notices, where landlords evict tenants under the pretense of major renovations, only to re-rent the units at significantly higher rates. <div class="break"> </div> Despite a staff recommendation to delay action, the Planning and Housing Committee was swayed by powerful public delegations from renters across the city, as well as data showing a parallel rise in homelessness. Troster argued that municipal action was essential, especially in the absence of provincial vacancy control or enforcement. Her motion called on staff to explore a bylaw modeled after Hamilton’s, which requires landlords to obtain renovation licenses, provide engineering reports, and offer relocation assistance. <div class="break"> </div> The vote marked a rare instance where councillors pushed back against a staff recommendation, signalling the strength of local organizing. Groups like ACORN Ottawa played a key role in mobilizing tenants, gathering testimonies, and holding the city accountable. The issue will return to council in 2026 for a final vote on what the bylaw will look like. In the meantime, Troster and other councillors continue to press the province for stronger tenant protections, while warning that the current legal framework leaves far too many renters vulnerable to displacement. </p> <div class="break"> </div> <style> .policy-impact-screen { position: relative; margin: 10vh auto 2em; content */ border-radius: 14px; width: 80%; max-width: 800px; max-height: 90vh; overflow-y: auto; padding: 1.5em; font-family: "Inter", sans-serif; box-sizing: border-box; } /* Close button still sits inside relative parent */ .policy-impact-screen .impact-close { position: absolute; right: 0.75em; background: none; border: none; font-size: 1.25em; cursor: pointer; line-height: 1; } /* Flex row of impacts */ .policy-impact-screen .impact-container { display: flex; gap: 1.5em; justify-content: center; } /* On small screens: stack and bring even higher */ @media (max-width: 979px) { .policy-impact-screen { margin: 5vh auto 1.5em; /* only 5 vh top margin */ width: 90%; padding: 1em; } .policy-impact-screen .impact-container { flex-direction: column; gap: 1em; } } /* Individual impact box */ .impact-box { background: #fff; border: 1px solid #e2e2e8; border-radius: 8px; flex: 1; min-width: 170px; padding: 1em; display: flex; flex-direction: column; align-items: center; } /* Subtitle styling (uppercase Inter with a bottom dotted line) */ .impact-title { font-family: "Inter", sans-serif; font-weight: 400; font-size: 1em; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5em; text-align: center; border-bottom: 1px dotted rgba(0, 0, 0, 0.8); padding-bottom: 0.25em; } /* Content area */ .impact-content { flex: 1; display: flex; align-items: center; justify-content: center; width: 100%; } .impact-number { font-family: "Spectral", serif; font-weight: 600; font-size: 2em; color: #0066cc; } .impact-graph { width: 100%; height: 120px; } .impact-widget { font-size: 2em; line-height: 1; } </style>
<!-- ————————————————————————————————————————————— CHART AREA (replace your current .chart-section block) ————————————————————————————————————————————— --> <div class="chart-section"> <h3>Housing Status Change</h3> <canvas id="housingStackedBar" height="160"></canvas> </div> <div class="chart-section"> <h3>Homelessness Trend</h3> <canvas id="homelessLineChart" height="160"></canvas> </div> <div class="chart-section"> <h3>Identity Demographics</h3> </div> <div class="chart-section"> <div class="demo-nav"> <button id="prevDemo">◀</button> <canvas id="demoBarChart" height="160"></canvas> <button id="nextDemo">▶</button> </div> </div> <style> /* 👀 A little breathing‑room around each canvas */ .chart-section canvas{padding:12px!important;} /* make nav buttons line up with the bar chart nicely */ .demo-nav{ display:flex;align-items:center;gap:0.5em;justify-content:center; } .demo-nav button{ font-size:1.2em;border:none;border-radius:4px;padding:0.2em 0.5em; background-color: #06C; color: #fff; } </style> <<script>> // Global font settings for all charts Chart.defaults.font.family = "'Inter', sans-serif"; Chart.defaults.font.size = 16; Chart.defaults.font.style = 'normal'; Chart.defaults.font.weight = 'normal'; Chart.defaults.font.lineHeight = 1.5; /* ───────────────────────────────────────────── 1. build or update CHARTS after each passage ───────────────────────────────────────────── */ $(document).one(':passagedisplay', function(){ const V = State.variables; const hist = V.history || []; const before = hist[0]; const after = hist[1] ?? null; /* ------------------------- HOUSING‑STATUS STACKED BAR (absolute counts this time) ------------------------- */ /* ------------------------- HOUSING‑STATUS STACKED BAR (year → year) ------------------------- */ (function(){ const ctx = document.getElementById('housingStackedBar').getContext('2d'); const cats = ['unsheltered','sheltered','transitional','institutional']; const colours=['#FF6384','#36A2EB','#FFCE56','#4BC0C0']; // take the LAST snapshot for each year, only for years >= 0 const byYear = Array.from( new Map(hist.filter(s => s.year >= 0).map(s => [s.year, s])).values() ); const before = byYear[0]; const after = byYear[byYear.length-1]; const labels = byYear.map((s, i) => `Year ${i + 1}`); const datasets = cats.map((k,i)=>({ label : k.charAt(0).toUpperCase()+k.slice(1), data : [before.pop[k]], backgroundColor: colours[i] })); if(after !== before){ cats.forEach((k,i)=>datasets[i].data.push(after.pop[k])); } new Chart(ctx,{ type:'bar', data:{labels,datasets}, options:{ responsive:true, layout:{padding:16}, scales:{x:{stacked:true},y:{stacked:true,beginAtZero:true}} } }); })(); /* ------------------------- HOMELESS TOTAL LINE (turn → turn, unchanged) ------------------------- */ (function(){ const ctx = document.getElementById('homelessLineChart').getContext('2d'); const labels = hist.map(s=>`Turn ${s.turn}`); const data = hist.map(s=>s.homelessTotal); const grad=ctx.createLinearGradient(0,0,0,200); grad.addColorStop(0,'rgba(0,102,204,0.5)'); grad.addColorStop(1,'rgba(0,102,204,0)'); new Chart(ctx,{ type:'line', data:{labels,datasets:[{ label:'Total Homeless', data,fill:true,backgroundColor:grad, borderColor:'rgba(0,102,204,1)',tension:0.4,pointRadius:3 }]}, options:{responsive:true,layout:{padding:16}} }); })(); /* ------------------------- CYCLABLE DEMOGRAPHIC BAR (year → year, colour #06C) ------------------------- */ (function(){ const ctx = document.getElementById('demoBarChart').getContext('2d'); const groups = [ {key:'male', label:'Male'}, {key:'female', label:'Female'}, {key:'transNB', label:'Trans & NB'}, {key:'lgbq', label:'Lgbq'}, {key:'lgbtqYouth', label:'LgbtqYouth'}, {key:'age25to49', label:'Age25‑49'}, {key:'racialized', label:'Racialized'}, {key:'indigenous', label:'Indigenous'}, {key:'veteran', label:'Veteran'}, {key:'immigrant', label:'Immigrant'} ]; let index = 0; // unique years (take last snapshot from each, only for years >= 0) const yearly = Array.from( new Map(hist.filter(s => s.year >= 0).map(s => [s.year, s])).values() ); const labels = yearly.map((s, i) => `Year ${i + 1}`); const makeDataset = g => yearly.map(s=>s.pop[g.key]); const chart = new Chart(ctx,{ type:'bar', data:{ labels, datasets:[{ label:groups[0].label, data :makeDataset(groups[0]), backgroundColor:'#06C' }] }, options:{ responsive:true, layout:{padding:16}, scales:{y:{beginAtZero:true}} } }); function updateChart(dir){ index = (index+dir+groups.length)%groups.length; const g = groups[index]; chart.data.datasets[0].label = g.label; chart.data.datasets[0].data = makeDataset(g); chart.update(); } $('#prevDemo').on('click',()=>updateChart(-1)); $('#nextDemo').on('click',()=>updateChart(+1)); })(); }); <</script>>