パッケージのソース · Weather and Date Chart
CC BY 4.0 · @joeのクレジットを残してください · v1
持っていく。リミックスする。授業で使う。
下のコードは、パッケージの遊べる入口HTMLだけです。相対パスの素材をすべて含めて編集またはリミックスするには、完全なZIPをダウンロードしてください。
AI編集プロンプトの下書き
この教室ゲームを電子黒板向けに編集してください。大きなタッチ対象と学級全体での遊びを保ってください。トラッキング、ログイン、個人情報の収集、遠隔APIの呼び出しは禁止です。下に表示された入口HTMLだけでなく、ダウンロードした完全なZIPを使い、ブラウザーで動くフォルダー構成を保ってください。教育目標は保ってください。元の制作者: ClassBoard Gamesの@joe(CC BY 4.0)。
入口HTMLのみ · index.html · 3939.4 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#1d3557">
<title>Weather and Date Chart 1.0</title>
<style>
:root {
--navy: #1d3557;
--navy-2: #274c77;
--cream: #fffdf7;
--paper: #fff9e8;
--yellow: #ffc335;
--orange: #ff7a2d;
--coral: #ff595e;
--coral-deep: #d62828;
--sky: #4cc9f0;
--sky-pale: #e0f7ff;
--lime: #8ac926;
--mint: #2ec4b6;
--pink: #ff70a6;
--purple: #8338ec;
--ink-soft: #4a5d80;
--shadow: 0 14px 32px rgba(29, 53, 87, .16);
--font: "Trebuchet MS", "Nunito", "Century Gothic", system-ui, -apple-system, sans-serif;
--header-h: 68px;
--app-scale: 1;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background:
radial-gradient(circle at 18% 18%, rgba(255, 228, 94, .45), transparent 34%),
radial-gradient(circle at 84% 16%, rgba(76, 201, 240, .38), transparent 38%),
linear-gradient(160deg, #dff7ff 0%, #fffdf7 54%, #fff3c4 100%);
color: var(--navy);
font-family: var(--font);
-webkit-tap-highlight-color: transparent;
}
body::before {
content: "";
position: fixed;
z-index: 0;
inset: 0;
background-image:
radial-gradient(circle at 24px 24px, rgba(255, 195, 53, .22) 4px, transparent 5px),
radial-gradient(circle at 84px 68px, rgba(76, 201, 240, .22) 6px, transparent 7px);
background-size: 120px 120px;
pointer-events: none;
}
button, input, select { font: inherit; }
button { color: inherit; }
img { display: block; user-select: none; -webkit-user-drag: none; }
button:focus-visible, input:focus-visible {
outline: 4px solid #fff;
outline-offset: 2px;
box-shadow: 0 0 0 7px var(--navy);
}
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
[hidden] { display: none !important; }
/* Fixed 1280x720 Virtual Canvas scaled proportionally to fit ANY screen size */
#viewport {
position: fixed;
inset: 0;
overflow: hidden;
display: grid;
place-items: center;
z-index: 1;
}
#app {
position: absolute;
left: 50%;
top: 50%;
width: 1280px;
height: 720px;
transform: translate(-50%, -50%) scale(var(--app-scale, 1));
transform-origin: center center;
overflow: hidden;
isolation: isolate;
border: 5px solid rgba(255, 255, 255, .95);
border-radius: 28px;
background: var(--cream);
box-shadow: 0 24px 64px rgba(29, 53, 87, .24), 0 6px 0 rgba(29, 53, 87, .12);
}
:fullscreen #app {
border-radius: 18px;
}
.screen {
position: absolute;
inset: 0;
display: none;
overflow: hidden;
background: var(--cream);
}
.screen.active { display: block; }
/* Streamlined Teacher Strip (Repeat, Undo, Sound, Fullscreen only) */
.teacher-bar {
position: absolute;
z-index: 100;
left: 14px;
right: 14px;
top: 10px;
height: var(--header-h);
display: none;
align-items: center;
gap: 14px;
padding: 8px 14px;
border: 3px solid rgba(255, 255, 255, .92);
border-radius: 22px;
background: linear-gradient(90deg, rgba(29, 53, 87, .96), rgba(39, 76, 119, .96));
color: #fff;
box-shadow: 0 7px 0 rgba(29, 53, 87, .18), 0 12px 24px rgba(29, 53, 87, .18);
}
.teacher-bar.visible { display: flex; }
.brand-menu {
position: relative;
flex: 0 0 auto;
}
.brand-menu-button {
width: 144px;
height: 48px;
display: grid;
place-items: center;
padding: 2px 6px;
border: 0;
border-radius: 14px;
cursor: pointer;
background: rgba(255, 255, 255, .1);
transition: transform .15s, background .15s;
}
.brand-menu-button:hover,
.brand-menu-button[aria-expanded="true"] {
background: rgba(255, 255, 255, .22);
transform: translateY(-1px);
}
.bar-logo {
width: 100%;
height: 100%;
object-fit: contain;
}
.brand-popover {
position: absolute;
left: 0;
top: calc(100% + 10px);
z-index: 130;
min-width: 225px;
display: grid;
gap: 6px;
padding: 8px;
border: 3px solid #fff;
border-radius: 18px;
background: var(--navy);
box-shadow: var(--shadow);
}
.brand-popover[hidden] { display: none !important; }
.menu-nav-button {
width: 100%;
min-height: 42px;
display: flex;
align-items: center;
gap: 10px;
padding: 7px 12px;
border: 0;
border-radius: 12px;
cursor: pointer;
background: #fff;
color: var(--navy);
font-size: 15px;
font-weight: 900;
text-align: left;
transition: background .15s, transform .15s;
}
.menu-nav-button:hover { background: #fff3b0; transform: translateX(3px); }
.speaker-block {
width: 180px;
display: flex;
flex-direction: column;
gap: 2px;
}
.speaker-block strong { font-size: 13px; line-height: 1; color: #ffe45e; }
.speaker-name {
width: 100%;
height: 30px;
padding: 3px 9px;
border: 0;
border-radius: 8px;
color: var(--navy);
background: #fff;
font-size: 14px;
font-weight: 800;
}
.star-meter {
flex: 1 1 220px;
max-width: 300px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 8px;
font-weight: 900;
font-size: 15px;
}
.meter-track {
height: 16px;
overflow: hidden;
border: 2px solid rgba(255, 255, 255, .78);
border-radius: 999px;
background: rgba(255, 255, 255, .18);
}
.meter-fill {
width: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--yellow), #fff37a, #8ac926);
transition: width .5s cubic-bezier(.2, .8, .2, 1);
}
.teacher-tools {
margin-left: auto;
display: flex;
gap: 8px;
}
.tool-button {
height: 46px;
padding: 0 14px;
border: 0;
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
cursor: pointer;
background: #fff;
color: var(--navy);
font-size: 18px;
font-weight: 1000;
box-shadow: inset 0 -3px 0 rgba(29, 53, 87, .12);
transition: transform .15s, background .15s;
}
.tool-button:hover { background: #fff1a9; transform: translateY(-2px); }
.tool-button:active { transform: translateY(2px); }
.tool-button .tool-label { font-size: 12px; text-transform: uppercase; letter-spacing: .03em; }
.tool-button.active { background: var(--yellow); }
/* Shared Buttons & Labels */
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
padding: 4px 13px;
border-radius: 999px;
background: var(--navy);
color: #fff;
font-size: 13px;
font-weight: 1000;
letter-spacing: .08em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--yellow);
box-shadow: 0 0 0 3px rgba(255, 195, 53, .26);
}
.primary-button, .secondary-button, .accent-button {
min-height: 54px;
padding: 10px 22px;
border-radius: 18px;
cursor: pointer;
font-size: 21px;
font-weight: 1000;
transition: transform .15s, box-shadow .15s, filter .15s;
}
.primary-button {
border: 3px solid #fff;
color: var(--navy);
background: linear-gradient(180deg, #fff37a, var(--yellow) 68%, #ffa823);
box-shadow: 0 6px 0 #dd7527, 0 12px 24px rgba(68, 58, 28, .18);
}
.accent-button {
border: 3px solid #fff;
color: #fff;
background: linear-gradient(180deg, #4cc9f0, #1982c4 72%, #1d3557);
box-shadow: 0 6px 0 #144575, 0 12px 24px rgba(29, 53, 87, .18);
}
.secondary-button {
border: 3px solid rgba(29, 53, 87, .16);
color: var(--navy);
background: #fff;
box-shadow: 0 5px 0 rgba(29, 53, 87, .12);
}
.primary-button:hover, .secondary-button:hover, .accent-button:hover {
transform: translateY(-2px);
filter: saturate(1.08);
}
.primary-button:active, .secondary-button:active, .accent-button:active {
transform: translateY(3px);
box-shadow: 0 2px 0 rgba(29, 53, 87, .18);
}
/* TITLE SCREEN */
#title-screen {
background:
radial-gradient(circle at 10% 14%, rgba(76, 201, 240, .36), transparent 27%),
radial-gradient(circle at 92% 10%, rgba(255, 195, 53, .4), transparent 26%),
linear-gradient(145deg, #fffdf7 0%, #fff8da 52%, #e0f7ff 100%);
}
.title-shell {
height: 100%;
display: grid;
grid-template-columns: 560px 1fr;
align-items: center;
gap: 42px;
padding: 38px 52px;
}
.title-copy { position: relative; z-index: 2; }
.title-logo { width: 240px; height: auto; margin-bottom: 14px; }
.title-copy h1 {
margin: 0;
font-size: 76px;
line-height: .9;
letter-spacing: -.045em;
color: var(--navy);
}
.title-copy h1 span { display: block; color: var(--coral); }
.title-kicker {
margin: 16px 0 0;
font-size: 21px;
line-height: 1.3;
color: var(--ink-soft);
font-weight: 800;
}
.title-flow {
margin: 18px 0 22px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 7px;
color: var(--navy);
font-weight: 1000;
font-size: 16px;
}
.title-flow span {
padding: 6px 12px;
border-radius: 999px;
background: #fff;
border: 2px solid rgba(29, 53, 87, .12);
box-shadow: 0 3px 0 rgba(29, 53, 87, .08);
}
.title-flow i { color: var(--orange); font-style: normal; font-size: 1.3em; }
.title-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.campaign-line {
margin: 18px 0 0;
font-size: 16px;
font-weight: 1000;
color: var(--navy-2);
letter-spacing: .025em;
}
.title-art {
position: relative;
height: 570px;
border: 7px solid #fff;
border-radius: 44px;
overflow: hidden;
background: #e0f7ff;
box-shadow: 0 22px 58px rgba(29, 53, 87, .2), 0 8px 0 rgba(29, 53, 87, .1);
transform: rotate(1deg);
}
.title-art img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.title-art::after {
content: "☀️ OUR CLASS WEATHER STATION";
position: absolute;
right: 20px;
bottom: 18px;
padding: 9px 15px;
border: 3px solid #fff;
border-radius: 999px;
color: #fff;
background: rgba(29, 53, 87, .9);
font-size: 14px;
font-weight: 1000;
letter-spacing: .06em;
}
/* PLAY SCREEN & STAGES */
#play-screen { background: #dff7ff; }
.stage {
position: absolute;
inset: 0;
display: none;
padding: calc(var(--header-h) + 20px) 28px 18px;
}
.stage.active { display: flex; flex-direction: column; animation: stage-in .28s ease both; }
.stage-heading {
position: relative;
z-index: 4;
text-align: center;
flex: 0 0 auto;
}
.stage-heading h2 {
margin: 3px 0 0;
font-size: 40px;
line-height: .98;
letter-spacing: -.035em;
}
.stage-heading p {
margin: 3px auto 0;
font-size: 17px;
color: var(--ink-soft);
font-weight: 800;
}
/* STAGE 1: COMPACT DATE PANELS */
#date-stage {
background:
radial-gradient(circle at 88% 15%, rgba(255, 195, 53, .28), transparent 28%),
linear-gradient(145deg, #e0f7ff 0%, #fffdf7 55%, #fef9c3 100%);
}
.date-stage-shell {
width: 1200px;
margin: 10px auto 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
gap: 14px;
}
.date-quick-bar {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
flex: 0 0 auto;
}
.quick-pill {
height: 38px;
padding: 0 16px;
border: 3px solid #fff;
border-radius: 999px;
cursor: pointer;
background: var(--navy);
color: #fff;
font-size: 15px;
font-weight: 900;
box-shadow: 0 4px 0 rgba(29, 53, 87, .15);
transition: transform .15s, background .15s;
}
.quick-pill:hover { transform: translateY(-2px); background: var(--purple); }
.quick-pill.today-pill { background: linear-gradient(90deg, #ff595e, #ff924c); }
/* Compact, non-stretched 4-column date cards */
.date-cards-grid {
display: grid;
grid-template-columns: 1.15fr 1.25fr 1.15fr .85fr;
gap: 14px;
align-items: start;
flex: 0 0 auto;
}
.date-column-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 14px;
border: 4px solid #fff;
border-radius: 24px;
background: rgba(255, 255, 255, .94);
box-shadow: 0 8px 0 rgba(29, 53, 87, .09), var(--shadow);
}
.date-column-card.card-day { background: linear-gradient(180deg, #fff0f5, #ffffff); border-color: #ff8fab; }
.date-column-card.card-month { background: linear-gradient(180deg, #e8f8f0, #ffffff); border-color: #52b788; }
.date-column-card.card-date { background: linear-gradient(180deg, #fff8d6, #ffffff); border-color: #ffb703; }
.date-column-card.card-year { background: linear-gradient(180deg, #f3e8ff, #ffffff); border-color: #a855f7; }
.col-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 1000;
font-size: 13px;
text-transform: uppercase;
letter-spacing: .05em;
color: var(--ink-soft);
}
.spinner-box {
display: grid;
grid-template-columns: 38px 1fr 38px;
align-items: center;
gap: 4px;
padding: 6px;
border-radius: 16px;
background: #fff;
border: 3px solid rgba(29, 53, 87, .12);
box-shadow: inset 0 2px 5px rgba(29, 53, 87, .05);
}
.spin-arrow {
width: 36px;
height: 36px;
border: 0;
border-radius: 11px;
cursor: pointer;
background: var(--sky-pale);
color: var(--navy);
font-size: 16px;
font-weight: 1000;
transition: background .15s, transform .15s;
}
.spin-arrow:hover { background: var(--yellow); transform: scale(1.06); }
.spin-value {
text-align: center;
font-size: 24px;
font-weight: 1000;
line-height: 1.05;
color: var(--navy);
user-select: none;
cursor: pointer;
}
.ordinal-badge {
display: inline-block;
margin-left: 2px;
padding: 2px 6px;
border-radius: 8px;
background: var(--coral);
color: #fff;
font-size: .6em;
vertical-align: super;
}
.chip-grid {
display: flex;
flex-wrap: wrap;
gap: 5px;
justify-content: center;
max-height: 112px;
overflow-y: auto;
padding: 2px;
}
.chip-btn {
padding: 5px 8px;
border: 2px solid rgba(29, 53, 87, .14);
border-radius: 10px;
cursor: pointer;
background: #fff;
color: var(--navy);
font-size: 13px;
font-weight: 900;
transition: transform .12s, background .12s, border-color .12s;
}
.chip-btn:hover { transform: translateY(-1px); border-color: var(--navy); background: #fff9db; }
.chip-btn.selected {
background: var(--navy);
color: #fff;
border-color: var(--navy);
box-shadow: 0 2px 0 rgba(29, 53, 87, .22);
}
.date-preview-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 10px 18px;
border: 4px solid #fff;
border-radius: 22px;
background: rgba(255, 255, 255, .96);
box-shadow: 0 8px 0 rgba(29, 53, 87, .1), var(--shadow);
flex: 0 0 auto;
}
.date-preview-sentence {
display: flex;
align-items: center;
gap: 10px;
font-size: 25px;
font-weight: 1000;
color: var(--navy);
cursor: pointer;
}
/* STAGE 2: WEATHER CONCEPT MAP */
#weather-stage {
background:
radial-gradient(circle at 50% 48%, rgba(255, 253, 247, .9) 0 20%, transparent 21%),
linear-gradient(145deg, rgba(224, 247, 255, .92), rgba(255, 248, 216, .92) 62%, rgba(232, 248, 240, .92));
}
.concept-map {
position: relative;
z-index: 1;
width: 1180px;
flex: 1;
min-height: 0;
margin: 10px auto 46px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 16px;
}
.place-node {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
padding: 0;
overflow: hidden;
border: 5px solid #fff;
border-radius: 24px;
cursor: pointer;
background: #fff;
box-shadow: 0 9px 0 rgba(29, 53, 87, .12), var(--shadow);
display: flex;
flex-direction: column;
transition: transform .18s, box-shadow .18s, border-color .18s;
}
.place-node:hover { transform: translateY(-4px); border-color: var(--yellow); }
.place-node:active { transform: translateY(2px); }
.place-node img {
width: 100%;
flex: 1;
min-height: 0;
object-fit: cover;
object-position: center;
}
.place-node .node-label {
width: 100%;
height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 14px;
border-top: 3px solid rgba(255, 255, 255, .9);
color: #fff;
background: rgba(29, 53, 87, .92);
font-size: 20px;
font-weight: 1000;
}
.node-label b {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: 9px;
background: var(--number-color, var(--yellow));
color: var(--navy);
font-size: 15px;
}
.weather-bottom-strip {
position: absolute;
left: 50%;
bottom: 12px;
transform: translateX(-50%);
display: flex;
gap: 12px;
z-index: 4;
}
.other-place {
height: 40px;
padding: 0 18px;
border: 3px solid var(--navy);
border-radius: 15px;
cursor: pointer;
color: var(--navy);
background: #fff;
box-shadow: 0 5px 0 rgba(29, 53, 87, .14);
font-size: 15px;
font-weight: 1000;
}
.other-place:hover { background: #fff3b0; }
/* STAGE 3 & 4: TEMPERATURE & SPEAK */
#temp-stage, #speak-stage { background: #fffaf0; }
.scene-art {
position: absolute;
inset: calc(var(--header-h) + 12px) 0 0 0;
width: 100%;
height: calc(100% - var(--header-h) - 12px);
object-fit: cover;
object-position: center;
}
.scene-vignette {
position: absolute;
inset: calc(var(--header-h) + 12px) 0 0;
background: linear-gradient(90deg, rgba(255, 253, 247, .97) 0%, rgba(255, 253, 247, .94) 48%, rgba(255, 253, 247, .38) 72%, rgba(255, 253, 247, .12) 100%);
pointer-events: none;
}
.action-panel, .speech-panel {
position: relative;
z-index: 3;
width: 660px;
flex: 1;
margin-left: 36px;
display: flex;
flex-direction: column;
justify-content: center;
}
.action-panel h2, .speech-panel h2 {
margin: 6px 0 4px;
font-size: 48px;
line-height: .94;
letter-spacing: -.04em;
}
.action-panel > .eyebrow,
.speech-panel > .eyebrow,
.chart-sidebar > .eyebrow { align-self: flex-start; }
.action-panel > p, .speech-panel > p {
margin: 0 0 10px;
font-size: 18px;
color: var(--ink-soft);
font-weight: 800;
}
/* Interactive Thermometer Widget */
.therm-widget {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
margin-bottom: 12px;
border: 4px solid #fff;
border-radius: 999px;
background: rgba(255, 255, 255, .96);
box-shadow: 0 7px 0 rgba(29, 53, 87, .12), var(--shadow);
user-select: none;
touch-action: none;
}
.therm-step-btn {
width: 38px;
height: 38px;
border: 0;
border-radius: 50%;
cursor: pointer;
background: var(--sky-pale);
color: var(--navy);
font-size: 20px;
font-weight: 1000;
}
.therm-step-btn:hover { background: var(--yellow); }
.therm-bar-shell {
flex: 1;
display: flex;
align-items: center;
cursor: grab;
}
.therm-bar-shell:active { cursor: grabbing; }
.therm-bulb {
width: 44px;
height: 44px;
border-radius: 50%;
border: 4px solid #cbd5e1;
background: #fbbf24;
z-index: 2;
display: grid;
place-items: center;
font-size: 22px;
transition: background .3s;
}
.therm-tube {
flex: 1;
height: 22px;
margin-left: -10px;
border: 4px solid #cbd5e1;
border-left: 0;
border-radius: 0 999px 999px 0;
background: #f1f5f9;
overflow: hidden;
position: relative;
}
.therm-fill {
height: 100%;
width: 55%;
background: linear-gradient(90deg, #38bdf8, #fbbf24);
border-radius: 0 999px 999px 0;
transition: width .3s ease, background .3s ease;
}
.therm-readout {
min-width: 125px;
text-align: right;
font-size: 20px;
font-weight: 1000;
}
.action-choices {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 9px;
}
.action-choice {
width: 100%;
height: 60px;
display: grid;
grid-template-columns: 44px 1fr 30px;
align-items: center;
gap: 10px;
padding: 6px 12px 6px 8px;
border: 4px solid #fff;
border-radius: 18px;
cursor: pointer;
background: rgba(255, 255, 255, .95);
color: var(--navy);
box-shadow: 0 5px 0 rgba(29, 53, 87, .11), 0 8px 16px rgba(29, 53, 87, .08);
text-align: left;
font-size: 19px;
font-weight: 1000;
transition: transform .15s, border-color .15s;
}
.action-choice:hover, .action-choice.selected {
transform: translateY(-2px);
border-color: var(--yellow);
background: #fffef5;
}
.action-choice .action-icon {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--choice-color, var(--sky-pale));
font-size: 24px;
}
.action-choice .keycap {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 2px solid rgba(29, 53, 87, .15);
border-radius: 8px;
background: var(--key-color, #fff);
font-size: 13px;
}
.back-link {
align-self: flex-start;
margin-top: 8px;
padding: 6px 4px;
border: 0;
cursor: pointer;
color: var(--navy-2);
background: transparent;
font-size: 16px;
font-weight: 1000;
text-decoration: underline;
text-underline-offset: 4px;
}
/* STAGE 4: SPEAK STAGE */
.speech-panel { width: 700px; }
.speech-bubble {
position: relative;
margin-top: 8px;
padding: 16px 22px;
border: 5px solid var(--navy);
border-radius: 28px;
background: rgba(255, 255, 255, .97);
box-shadow: 0 10px 0 rgba(29, 53, 87, .13), var(--shadow);
}
.sentence-row {
display: grid;
grid-template-columns: 40px 1fr 46px;
align-items: center;
gap: 12px;
min-height: 64px;
padding: 6px 0;
border-bottom: 3px dashed rgba(29, 53, 87, .14);
font-size: 26px;
line-height: 1.15;
font-weight: 1000;
}
.sentence-row:last-child { border-bottom: 0; }
.sentence-number {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 11px;
color: #fff;
background: var(--coral);
font-size: 18px;
}
.speak-one {
width: 44px;
height: 44px;
border: 0;
border-radius: 13px;
cursor: pointer;
background: var(--sky-pale);
font-size: 20px;
transition: transform .15s, background .15s;
}
.speak-one:hover { background: var(--yellow); transform: scale(1.08); }
.mascot-advice-pill {
margin: 10px 0;
padding: 9px 15px;
border-radius: 16px;
background: #fff3b0;
border: 3px solid #ffb703;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 17px;
font-weight: 900;
color: var(--navy);
}
.speech-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 4px; }
.mascot-stage-card {
position: absolute;
right: 42px;
bottom: 36px;
z-index: 3;
width: 310px;
padding: 16px 18px;
border: 5px solid #fff;
border-radius: 28px;
background: rgba(255, 255, 255, .94);
box-shadow: var(--shadow);
text-align: center;
}
.mascot-avatar {
font-size: 64px;
line-height: 1.05;
margin-bottom: 6px;
}
.mascot-stage-card strong {
display: block;
font-size: 20px;
color: var(--navy);
}
.mascot-stage-card span {
display: block;
margin-top: 4px;
font-size: 15px;
color: var(--ink-soft);
font-weight: 800;
}
/* STAGE 5: CLASS CHART + SLOT BOARD */
#chart-stage {
background:
radial-gradient(circle at 5% 8%, rgba(255, 195, 53, .25), transparent 24%),
linear-gradient(145deg, #dff7ff, #fff9e1 70%);
}
.chart-layout {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1.45fr .65fr;
gap: 20px;
padding-top: 4px;
}
.map-visual {
position: relative;
min-width: 0;
overflow: hidden;
border: 5px solid #fff;
border-radius: 32px;
background: #e0f7ff;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
}
.board-mode-tabs {
position: absolute;
top: 12px;
right: 14px;
z-index: 10;
display: flex;
gap: 6px;
background: rgba(29, 53, 87, .9);
padding: 4px;
border-radius: 999px;
border: 3px solid #fff;
}
.board-tab {
padding: 6px 13px;
border: 0;
border-radius: 999px;
cursor: pointer;
background: transparent;
color: #fff;
font-size: 13px;
font-weight: 900;
}
.board-tab.active {
background: var(--yellow);
color: var(--navy);
}
.visual-pane {
position: relative;
flex: 1;
width: 100%;
height: 100%;
overflow: hidden;
}
.visual-pane > img { width: 100%; height: 100%; object-fit: cover; }
.map-title {
position: absolute;
left: 16px;
top: 12px;
z-index: 2;
max-width: 56%;
padding: 9px 15px;
border: 3px solid #fff;
border-radius: 18px;
color: #fff;
background: rgba(29, 53, 87, .92);
box-shadow: 0 5px 0 rgba(29, 53, 87, .16);
}
.map-title strong { display: block; font-size: 22px; }
.map-title span { display: block; margin-top: 2px; font-size: 13px; opacity: .9; font-weight: 800; }
.weather-tally-grid {
position: absolute;
left: 16px;
right: 16px;
bottom: 14px;
z-index: 4;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
}
.tally-card {
padding: 7px 5px;
border: 3px solid #fff;
border-radius: 16px;
background: rgba(255, 255, 255, .94);
text-align: center;
box-shadow: 0 5px 0 rgba(29, 53, 87, .14);
font-weight: 1000;
}
.tally-card .tally-icon { font-size: 24px; display: block; }
.tally-card .tally-name { font-size: 11px; color: var(--ink-soft); display: block; }
.tally-card .tally-num {
display: inline-block;
margin-top: 2px;
min-width: 28px;
padding: 2px 7px;
border-radius: 999px;
background: var(--yellow);
color: var(--navy);
font-size: 15px;
}
.total-stories {
position: absolute;
left: 50%;
top: 44%;
z-index: 3;
transform: translate(-50%, -50%);
min-width: 155px;
padding: 9px 15px;
border: 4px solid #fff;
border-radius: 22px;
color: #fff;
background: rgba(29, 53, 87, .92);
text-align: center;
box-shadow: var(--shadow);
font-weight: 1000;
}
.total-stories b { display: block; color: var(--yellow); font-size: 32px; line-height: 1; }
.map-star {
position: absolute;
z-index: 4;
color: var(--yellow);
font-size: 28px;
filter: drop-shadow(0 3px 0 #ff7a2d);
animation: star-arrive .55s cubic-bezier(.1, .9, .15, 1.3) both;
pointer-events: none;
}
.slot-board-pane {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 14px;
padding: 64px 20px 20px;
background:
radial-gradient(circle at 85% 18%, rgba(255, 223, 108, .45), transparent 28%),
linear-gradient(180deg, #cde6f7 0%, #e8f7ff 65%, #d8f3dc 100%);
}
.slot-sentence-card {
width: 100%;
padding: 18px 16px;
border: 5px solid #fff;
border-radius: 26px;
background: rgba(255, 255, 255, .92);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
text-align: center;
}
.slot-line {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 26px;
font-weight: 1000;
color: var(--navy);
}
.drum-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 999px;
border: 4px solid var(--drum-border, #38bdf8);
background: var(--drum-bg, #e0f2fe);
color: var(--drum-text, #0369a1);
box-shadow: 0 4px 0 rgba(29, 53, 87, .12);
cursor: pointer;
user-select: none;
}
.drum-pill button {
width: 26px;
height: 26px;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, .82);
cursor: pointer;
font-weight: 1000;
font-size: 13px;
}
.drum-pill button:hover { background: var(--yellow); }
.chart-sidebar {
min-width: 0;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
padding: 16px 20px;
border: 5px solid #fff;
border-radius: 32px;
background: rgba(255, 253, 247, .96);
box-shadow: var(--shadow);
}
.chart-sidebar h2 {
margin: 6px 0 4px;
font-size: 32px;
line-height: .96;
letter-spacing: -.035em;
}
.last-story {
margin: 8px 0 10px;
padding: 10px 12px;
border-left: 7px solid var(--coral);
border-radius: 14px;
background: #fff1e7;
font-size: 15px;
line-height: 1.3;
font-weight: 900;
}
.reaction-title { margin: 6px 0 6px; font-size: 14px; font-weight: 1000; }
.reactions { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.reaction-button {
min-height: 48px;
padding: 5px 4px;
border: 3px solid rgba(29, 53, 87, .1);
border-radius: 14px;
cursor: pointer;
background: #fff;
font-size: 14px;
font-weight: 1000;
transition: transform .15s, background .15s;
}
.reaction-button:hover { transform: translateY(-2px); background: #fff1a9; }
.reaction-button b { display: block; margin-top: 2px; color: var(--coral-deep); }
.chart-actions { margin-top: auto; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding-top: 8px; }
.chart-actions .primary-button, .chart-actions .secondary-button {
width: 100%;
min-height: 42px;
padding: 6px 10px;
font-size: 14px;
line-height: 1.05;
}
.class-progress-card {
grid-column: 1 / -1;
margin: 2px 0;
padding: 8px 11px;
border: 3px solid rgba(29, 53, 87, .1);
border-radius: 14px;
background: #eefaff;
color: var(--navy);
font-weight: 1000;
}
.class-progress-card strong { display: block; font-size: 15px; }
.class-progress-card span { display: block; margin-top: 2px; color: var(--ink-soft); font-size: 12px; }
.previous-results-button { grid-column: 1 / -1; }
/* Dialog, Toast, Effects */
dialog {
width: 520px;
padding: 0;
overflow: hidden;
border: 5px solid #fff;
border-radius: 28px;
color: var(--navy);
background: var(--cream);
box-shadow: 0 24px 70px rgba(0, 0, 0, .35);
}
dialog::backdrop {
background-color: rgba(29, 53, 87, .68);
backdrop-filter: blur(5px);
}
.dialog-shell { padding: 24px; }
.dialog-shell h2 { margin: 8px 0 6px; font-size: 30px; line-height: 1; }
.dialog-shell p { margin: 0 0 14px; color: var(--ink-soft); font-weight: 800; }
.field { display: grid; gap: 6px; margin: 10px 0; font-weight: 1000; }
.field input {
width: 100%;
height: 48px;
padding: 8px 12px;
border: 3px solid rgba(29, 53, 87, .18);
border-radius: 14px;
color: var(--navy);
background: #fff;
font-size: 18px;
font-weight: 800;
}
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.dialog-actions button { height: 44px; padding: 0 16px; border-radius: 13px; cursor: pointer; font-weight: 1000; }
.cancel-dialog { border: 2px solid rgba(29, 53, 87, .15); background: #fff; }
.save-dialog { border: 2px solid #fff; background: var(--yellow); box-shadow: 0 4px 0 #dd7527; }
.toast {
position: absolute;
z-index: 150;
left: 50%;
bottom: 18px;
transform: translate(-50%, 30px);
max-width: 620px;
padding: 10px 18px;
border: 3px solid #fff;
border-radius: 999px;
color: #fff;
background: rgba(29, 53, 87, .95);
box-shadow: var(--shadow);
font-size: 16px;
font-weight: 1000;
opacity: 0;
pointer-events: none;
transition: opacity .2s, transform .2s;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.effect-layer { position: absolute; z-index: 140; inset: 0; overflow: hidden; pointer-events: none; }
.burst-star {
position: absolute;
left: 50%;
top: 52%;
color: var(--yellow);
font-size: var(--size);
text-shadow: 0 3px 0 var(--orange);
animation: burst 1.2s cubic-bezier(.12, .7, .15, 1) both;
}
@keyframes stage-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes star-arrive { from { opacity: 0; transform: translate(-60px, 50px) scale(.1) rotate(-180deg); } to { opacity: 1; transform: none; } }
@keyframes burst { from { opacity: 1; transform: translate(-50%, -50%) scale(.3) rotate(0); } to { opacity: 0; transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y))) scale(1.2) rotate(var(--r)); } }
</style>
</head>
<body>
<div id="viewport">
<div id="app">
<!-- STREAMLINED TEACHER BAR: Repeat, Undo, Sound, Fullscreen only -->
<header class="teacher-bar" id="teacher-bar" aria-label="Teacher controls">
<div class="brand-menu">
<button class="brand-menu-button" id="brand-menu-button" type="button" aria-label="Open navigation menu" aria-expanded="false" aria-controls="brand-popover">
<img class="bar-logo" src="assets/sun-badge.svg" alt="Weather and Date Club">
</button>
<div class="brand-popover" id="brand-popover" hidden>
<button class="menu-nav-button" id="home-button" type="button"><span aria-hidden="true">🏠</span> Title screen</button>
<button class="menu-nav-button" id="menu-guided-button" type="button"><span aria-hidden="true">📅</span> Step-by-step turn</button>
<button class="menu-nav-button" id="menu-slot-button" type="button"><span aria-hidden="true">🎰</span> Quick Slot Board</button>
<button class="menu-nav-button" id="menu-chart-button" type="button"><span aria-hidden="true">📊</span> Class Weather Chart</button>
</div>
</div>
<div class="speaker-block">
<strong id="speaker-label">Reporter 1 of 30</strong>
<label class="sr-only" for="speaker-name">Student name, optional</label>
<input class="speaker-name" id="speaker-name" type="text" maxlength="18" placeholder="Helper name (optional)" autocomplete="off">
</div>
<div class="star-meter" aria-label="Class star progress">
<span aria-hidden="true">⭐</span>
<div class="meter-track"><div class="meter-fill" id="meter-fill"></div></div>
<span id="meter-label">0 / 30</span>
</div>
<nav class="teacher-tools" aria-label="Game tools">
<button class="tool-button" id="repeat-button" type="button" title="Repeat voice (R)" aria-label="Repeat voice"><span aria-hidden="true">🔊</span><span class="tool-label">Repeat</span></button>
<button class="tool-button" id="undo-button" type="button" title="Undo last report (U)" aria-label="Undo last report"><span aria-hidden="true">↶</span><span class="tool-label">Undo</span></button>
<button class="tool-button active" id="sound-button" type="button" title="Voice & sound on or off (M)" aria-pressed="true"><span id="sound-icon" aria-hidden="true">♪</span><span class="tool-label">Sound</span></button>
<button class="tool-button" id="fullscreen-button" type="button" title="Fullscreen (F)" aria-label="Toggle fullscreen"><span aria-hidden="true">⛶</span><span class="tool-label">Full</span></button>
</nav>
</header>
<main>
<!-- TITLE SCREEN -->
<section class="screen active" id="title-screen" aria-labelledby="game-title">
<div class="title-shell">
<div class="title-copy">
<img class="title-logo" src="assets/sun-badge.svg" alt="Sunny Sky Weather and Date Club">
<h1 id="game-title">Weather & Date <span>Chart 1.0</span></h1>
<p class="title-kicker">Check today’s calendar, look out the window at the sky, and share our daily weather report with Neural Jenny!</p>
<div class="title-flow" aria-label="Pick the date, choose weather, feel temperature, and speak">
<span>📅 1. Date</span><i aria-hidden="true">›</i><span>☀️ 2. Weather</span><i aria-hidden="true">›</i><span>🌡️ 3. Temp</span><i aria-hidden="true">›</i><span>⭐ 4. Speak!</span>
</div>
<div class="title-actions">
<button class="primary-button" id="start-button" type="button">Start Weather Chart</button>
<button class="accent-button" id="quick-board-button" type="button">🎰 Quick Slot Board</button>
<button class="secondary-button" id="new-class-button" type="button" hidden>Start a new class</button>
</div>
<p class="campaign-line">Every Day Is a Great Day to Learn & Talk Together!</p>
</div>
<div class="title-art" aria-hidden="true">
<img src="assets/weather-world.svg" alt="">
</div>
</div>
</section>
<!-- PLAY SCREEN -->
<section class="screen" id="play-screen" aria-label="Weather and Date Chart activity">
<!-- STAGE 1: COMPACT DATE PANELS -->
<section class="stage active" id="date-stage" aria-labelledby="date-title">
<div class="stage-heading">
<span class="eyebrow">Step 1 · Calendar Helper</span>
<h2 id="date-title">What is the date today?</h2>
<p>Tap any day, month, date, or year to hear Jenny say it!</p>
</div>
<div class="date-stage-shell">
<div class="date-quick-bar">
<button class="quick-pill today-pill" id="use-today-button" type="button">✨ Use Today’s Real Date</button>
<button class="quick-pill" id="random-date-button" type="button">🎲 Mystery Practice Date</button>
<button class="quick-pill" id="speak-date-preview-btn" type="button">🔊 Hear Full Sentence</button>
</div>
<div class="date-cards-grid">
<!-- Day of Week -->
<div class="date-column-card card-day">
<div class="col-header"><span>📅 Day of Week</span><span>1–7</span></div>
<div class="spinner-box">
<button class="spin-arrow" type="button" data-spin="day" data-dir="-1" aria-label="Previous day">◀</button>
<div class="spin-value" id="display-day" title="Click to hear day">Sunday</div>
<button class="spin-arrow" type="button" data-spin="day" data-dir="1" aria-label="Next day">▶</button>
</div>
<div class="chip-grid" id="chips-day"></div>
</div>
<!-- Month -->
<div class="date-column-card card-month">
<div class="col-header"><span>🌸 Month</span><span>Jan–Dec</span></div>
<div class="spinner-box">
<button class="spin-arrow" type="button" data-spin="month" data-dir="-1" aria-label="Previous month">◀</button>
<div class="spin-value" id="display-month" title="Click to hear month">September</div>
<button class="spin-arrow" type="button" data-spin="month" data-dir="1" aria-label="Next month">▶</button>
</div>
<div class="chip-grid" id="chips-month"></div>
</div>
<!-- Date + Ordinal -->
<div class="date-column-card card-date">
<div class="col-header"><span>🔢 Date & Ordinal</span><span>st/nd/rd/th</span></div>
<div class="spinner-box">
<button class="spin-arrow" type="button" data-spin="date" data-dir="-1" aria-label="Previous date">◀</button>
<div class="spin-value" id="display-date-wrap" title="Click to hear ordinal"><span id="display-date">27</span><span class="ordinal-badge" id="display-ordinal">th</span></div>
<button class="spin-arrow" type="button" data-spin="date" data-dir="1" aria-label="Next date">▶</button>
</div>
<div class="chip-grid" id="chips-date"></div>
</div>
<!-- Year -->
<div class="date-column-card card-year">
<div class="col-header"><span>🎉 Year</span><span>Calendar</span></div>
<div class="spinner-box">
<button class="spin-arrow" type="button" data-spin="year" data-dir="-1" aria-label="Previous year">◀</button>
<div class="spin-value" id="display-year" title="Click to hear year">2026</div>
<button class="spin-arrow" type="button" data-spin="year" data-dir="1" aria-label="Next year">▶</button>
</div>
<div class="chip-grid" id="chips-year"></div>
</div>
</div>
<div class="date-preview-bar">
<div class="date-preview-sentence" id="date-sentence-bar-click" title="Click to hear Jenny read the sentence">
<span aria-hidden="true">🔊</span>
<span id="live-date-sentence">Today is Sunday, September 27th, 2026.</span>
</div>
<button class="primary-button" id="to-weather-button" type="button">Next: How is the weather? ☀️ →</button>
</div>
</div>
</section>
<!-- STAGE 2: WEATHER SKY -->
<section class="stage" id="weather-stage" aria-labelledby="weather-title">
<div class="stage-heading">
<span class="eyebrow">Step 2 · Look at the Sky</span>
<h2 id="weather-title">How is the weather today?</h2>
<p>Choose the sky picture that matches outside.</p>
</div>
<div class="concept-map" id="concept-map">
<button class="place-node numbered-choice" type="button" data-weather="sunny" data-key="1">
<img src="assets/sunny.svg" alt="Sunny sky with smiling sun">
<span class="node-label"><span>☀️ Sunny</span><b style="--number-color:#ffc335">1</b></span>
</button>
<button class="place-node numbered-choice" type="button" data-weather="partly-cloudy" data-key="2">
<img src="assets/partly-cloudy.svg" alt="Partly cloudy sky with sun peeking out">
<span class="node-label"><span>⛅ Partly Cloudy</span><b style="--number-color:#71cbff">2</b></span>
</button>
<button class="place-node numbered-choice" type="button" data-weather="cloudy" data-key="3">
<img src="assets/cloudy.svg" alt="Cozy white and gray clouds">
<span class="node-label"><span>☁️ Cloudy</span><b style="--number-color:#cbd5e1">3</b></span>
</button>
<button class="place-node numbered-choice" type="button" data-weather="rainy" data-key="4">
<img src="assets/rainy.svg" alt="Raindrops falling with a red umbrella">
<span class="node-label"><span>🌧️ Rainy</span><b style="--number-color:#38bdf8">4</b></span>
</button>
<button class="place-node numbered-choice" type="button" data-weather="windy" data-key="5">
<img src="assets/windy.svg" alt="Windy sky with spinning pinwheel and leaves">
<span class="node-label"><span>🍃 Windy</span><b style="--number-color:#86efac">5</b></span>
</button>
<button class="place-node numbered-choice" type="button" data-weather="stormy" data-key="6">
<img src="assets/stormy.svg" alt="Stormy cloud with lightning bolt">
<span class="node-label"><span>⛈️ Stormy / Typhoon</span><b style="--number-color:#c084fc">6</b></span>
</button>
</div>
<div class="weather-bottom-strip">
<button class="other-place" id="back-to-date-button" type="button">← Back to Date</button>
<button class="other-place numbered-choice" type="button" data-weather="other" data-key="7">✏️ Custom Weather</button>
</div>
</section>
<!-- STAGE 3: TEMPERATURE & THERMOMETER -->
<section class="stage" id="temp-stage" aria-labelledby="temp-title">
<img class="scene-art" id="temp-art" src="assets/sunny.svg" alt="">
<div class="scene-vignette"></div>
<div class="action-panel">
<span class="eyebrow" id="temp-weather-label">☀️ Sunny Sky</span>
<h2 id="temp-title">How does it feel outside?</h2>
<p>Slide the thermometer or tap a temperature card (1–5)!</p>
<div class="therm-widget" aria-label="Interactive Thermometer">
<button class="therm-step-btn" id="stage-therm-down" type="button" aria-label="Cooler">−</button>
<div class="therm-bar-shell" id="stage-therm-drag" title="Drag left or right to change temperature!">
<div class="therm-bulb" id="stage-therm-bulb">😊</div>
<div class="therm-tube">
<div class="therm-fill" id="stage-therm-fill"></div>
</div>
</div>
<button class="therm-step-btn" id="stage-therm-up" type="button" aria-label="Warmer">+</button>
<div class="therm-readout" id="stage-therm-readout">😊 WARM</div>
</div>
<div class="action-choices" id="temp-choices"></div>
<button class="back-link" id="back-to-weather" type="button">← Choose different weather</button>
</div>
<aside class="mascot-stage-card" aria-live="polite">
<div class="mascot-avatar" id="temp-mascot-avatar">🧸🕶️</div>
<strong id="temp-mascot-title">Weather Bear’s Gear</strong>
<span id="temp-mascot-tip">Wear a T-shirt and play outside!</span>
</aside>
</section>
<!-- STAGE 4: SPEAK 2 SENTENCES -->
<section class="stage" id="speak-stage" aria-labelledby="speak-title">
<img class="scene-art" id="speak-art" src="assets/sunny.svg" alt="">
<div class="scene-vignette"></div>
<div class="speech-panel">
<span class="eyebrow">Step 4 · Weather Reporter</span>
<h2 id="speak-title">Say today’s weather report!</h2>
<div class="speech-bubble" aria-live="polite">
<div class="sentence-row">
<span class="sentence-number">1</span>
<span id="date-sentence-text">Today is Sunday, September 27th, 2026.</span>
<button class="speak-one" type="button" data-sentence="date" aria-label="Hear the date sentence">🔊</button>
</div>
<div class="sentence-row">
<span class="sentence-number">2</span>
<span id="weather-sentence-text">It is sunny and warm outside!</span>
<button class="speak-one" type="button" data-sentence="weather" aria-label="Hear the weather sentence">🔊</button>
</div>
</div>
<div class="mascot-advice-pill">
<span id="speak-mascot-pill">🧸 Weather Bear says: Put on your sunglasses and have fun!</span>
<button class="speak-one" type="button" data-sentence="tip" aria-label="Hear Weather Bear tip">🔊</button>
</div>
<div class="speech-actions">
<button class="primary-button" id="said-it-button" type="button">We said it! Add a star ⭐</button>
<button class="secondary-button" id="change-answer-button" type="button">Change temperature</button>
</div>
</div>
<aside class="mascot-stage-card">
<div class="mascot-avatar" id="speak-mascot-avatar">🧸☀️🕶️</div>
<strong id="speak-mascot-title">Ready to Report!</strong>
<span id="speak-mascot-sub">Tap 🔊 to hear Jenny, then say each sentence aloud.</span>
</aside>
</section>
<!-- STAGE 5: CLASS CHART + INTERACTIVE SLOT DRUM BOARD -->
<section class="stage" id="chart-stage" aria-labelledby="chart-title">
<div class="chart-layout">
<div class="map-visual" aria-label="Class weather chart and interactive board">
<div class="board-mode-tabs" role="tablist">
<button class="board-tab active" id="tab-map-view" type="button" role="tab" aria-selected="true">📊 Star Chart</button>
<button class="board-tab" id="tab-slot-view" type="button" role="tab" aria-selected="false">🎰 Quick Slot Board</button>
</div>
<!-- View A: Visual Weather Star Map -->
<div class="visual-pane" id="pane-map-view">
<img src="assets/weather-world.svg" alt="Sunny Sky Weather Club station">
<div class="map-title">
<strong>Our Weather & Date Chart</strong>
<span id="chart-banner-date">Today is Sunday, September 27th, 2026</span>
</div>
<div class="total-stories"><b id="total-stories">0</b><span>reports shared</span></div>
<div id="map-stars" aria-hidden="true"></div>
<div class="weather-tally-grid" id="weather-tally-grid"></div>
</div>
<!-- View B: All-in-One Slot Drum & Thermometer Board -->
<div class="visual-pane slot-board-pane" id="pane-slot-view" hidden>
<div class="map-title">
<strong>Interactive Date & Weather Board</strong>
<span>Click arrows or scroll on any pill to spin!</span>
</div>
<div class="slot-sentence-card">
<div class="slot-line">
<span>Today is</span>
<span class="drum-pill" style="--drum-bg:#ffe5ec;--drum-border:#ff8fab;--drum-text:#9d174d" data-wheel="day">
<button type="button" data-spin="day" data-dir="-1">◀</button>
<strong id="slot-val-day">Sunday</strong>
<button type="button" data-spin="day" data-dir="1">▶</button>
</span>
<span>,</span>
<span class="drum-pill" style="--drum-bg:#d8f3dc;--drum-border:#52b788;--drum-text:#1b4332" data-wheel="month">
<button type="button" data-spin="month" data-dir="-1">◀</button>
<strong id="slot-val-month">September</strong>
<button type="button" data-spin="month" data-dir="1">▶</button>
</span>
<span class="drum-pill" style="--drum-bg:#fff3b0;--drum-border:#ffb703;--drum-text:#9a3412" data-wheel="date">
<button type="button" data-spin="date" data-dir="-1">◀</button>
<strong id="slot-val-date">27th</strong>
<button type="button" data-spin="date" data-dir="1">▶</button>
</span>
<span>,</span>
<span class="drum-pill" style="--drum-bg:#f3e8ff;--drum-border:#a855f7;--drum-text:#581c87" data-wheel="year">
<button type="button" data-spin="year" data-dir="-1">◀</button>
<strong id="slot-val-year">2026</strong>
<button type="button" data-spin="year" data-dir="1">▶</button>
</span>
</div>
<div class="slot-line">
<span>and it is</span>
<span class="drum-pill" style="--drum-bg:#e0f2fe;--drum-border:#38bdf8;--drum-text:#0369a1" data-wheel="weather">
<button type="button" data-spin="weather" data-dir="-1">◀</button>
<strong id="slot-val-weather">☀️ Sunny</strong>
<button type="button" data-spin="weather" data-dir="1">▶</button>
</span>
<span>and</span>
<span class="drum-pill" style="--drum-bg:#fef08a;--drum-border:#f59e0b;--drum-text:#92400e" data-wheel="temp">
<button type="button" data-spin="temp" data-dir="-1">◀</button>
<strong id="slot-val-temp">😊 Warm</strong>
<button type="button" data-spin="temp" data-dir="1">▶</button>
</span>
<span>outside!</span>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:center;margin-top:4px;">
<button class="primary-button" id="slot-speak-all-btn" type="button" style="min-height:46px;font-size:17px;padding:8px 18px;">🔊 Hear Jenny Read Full Board</button>
<button class="accent-button" id="slot-add-star-btn" type="button" style="min-height:46px;font-size:17px;padding:8px 18px;">⭐ Add Report to Class Chart</button>
</div>
</div>
</div>
</div>
<aside class="chart-sidebar">
<span class="eyebrow">Class Progress</span>
<h2 id="chart-title">Great Weather Reporters!</h2>
<div class="last-story" id="last-story" aria-live="polite">Your first weather report will appear here.</div>
<p class="reaction-title">How does the class feel about today’s weather?</p>
<div class="reactions">
<button class="reaction-button" type="button" data-reaction="yay">☀️ Yay!<b id="reaction-yay">0</b></button>
<button class="reaction-button" type="button" data-reaction="cozy">🌈 Cozy!<b id="reaction-cozy">0</b></button>
<button class="reaction-button" type="button" data-reaction="fun">🎈 Fun!<b id="reaction-fun">0</b></button>
</div>
<div class="chart-actions">
<div class="class-progress-card" aria-live="polite">
<strong id="class-progress-text">0 of up to 30 students answered</strong>
<span id="next-student-text">Reporter 1 is next.</span>
</div>
<button class="primary-button" id="next-speaker-button" type="button">Next reporter → 1/30</button>
<button class="secondary-button" id="reset-class-chart-btn" type="button">🔄 New Class</button>
<button class="secondary-button previous-results-button" id="previous-story-button" type="button">← Previous screen · Change last report</button>
</div>
</aside>
</div>
<ol class="sr-only" id="story-log" aria-label="Completed weather reports"></ol>
</section>
</section>
</main>
<dialog id="custom-dialog" aria-labelledby="custom-dialog-title">
<form class="dialog-shell" id="custom-form" method="dialog">
<span class="eyebrow">Custom Weather Idea</span>
<h2 id="custom-dialog-title">Describe today’s sky!</h2>
<p id="custom-dialog-help">Type the weather word (for example: foggy, snowy, breezy, rainbow).</p>
<label class="field">How is the weather?
<input id="custom-weather-input" type="text" maxlength="32" placeholder="snowy and bright" autocomplete="off">
</label>
<div class="dialog-actions">
<button class="cancel-dialog" id="cancel-dialog" type="button">Cancel</button>
<button class="save-dialog" type="submit">Use this weather →</button>
</div>
</form>
</dialog>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<div class="effect-layer" id="effect-layer" aria-hidden="true"></div>
<div class="sr-only" id="live-status" aria-live="polite"></div>
</div>
</div>
<!-- Pre-rendered Microsoft Edge TTS (en-US-JennyNeural) Offline Voice Pack -->
<script src="assets/audio_jenny.js"></script>
<script>
const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const MONTHS = [
{ name: "January", short: "Jan", icon: "❄️" },
{ name: "February", short: "Feb", icon: "⛄" },
{ name: "March", short: "Mar", icon: "🌱" },
{ name: "April", short: "Apr", icon: "🌷" },
{ name: "May", short: "May", icon: "🌼" },
{ name: "June", short: "Jun", icon: "☀️" },
{ name: "July", short: "Jul", icon: "🏖️" },
{ name: "August", short: "Aug", icon: "🍉" },
{ name: "September", short: "Sep", icon: "🍎" },
{ name: "October", short: "Oct", icon: "🎃" },
{ name: "November", short: "Nov", icon: "🍂" },
{ name: "December", short: "Dec", icon: "🎄" }
];
const YEARS = [2025, 2026, 2027, 2028, 2029, 2030];
const WEATHERS = [
{
id: "sunny",
label: "Sunny",
word: "sunny",
icon: "☀️",
image: "assets/sunny.svg",
color: "#ffc335",
mascot: "🧸🕶️🧢",
gearTip: "Put on your sunglasses and sun hat!"
},
{
id: "partly-cloudy",
label: "Partly Cloudy",
word: "partly cloudy",
icon: "⛅",
image: "assets/partly-cloudy.svg",
color: "#71cbff",
mascot: "🧸⛅👟",
gearTip: "Great sky to fly a kite in the park!"
},
{
id: "cloudy",
label: "Cloudy",
word: "cloudy",
icon: "☁️",
image: "assets/cloudy.svg",
color: "#cbd5e1",
mascot: "🧸☁️👕",
gearTip: "Soft clouds in the sky, nice and gentle outside!"
},
{
id: "rainy",
label: "Rainy",
word: "rainy",
icon: "🌧️",
image: "assets/rainy.svg",
color: "#38bdf8",
mascot: "🧸☔🥾",
gearTip: "Take your umbrella and wear rain boots!"
},
{
id: "windy",
label: "Windy",
word: "windy",
icon: "🍃",
image: "assets/windy.svg",
color: "#86efac",
mascot: "🧸🍃🧥",
gearTip: "Hold onto your hat and watch the pinwheel spin!"
},
{
id: "stormy",
label: "Stormy",
word: "stormy",
icon: "⛈️",
image: "assets/stormy.svg",
color: "#c084fc",
mascot: "🧸🏠📚",
gearTip: "Stay cozy indoors and read a good book!"
}
];
const TEMPERATURES = [
{ id: "cold", label: "Cold", word: "cold", icon: "🥶", level: 16, fill: "#3b82f6", color: "#dbeafe", keyColor: "#93c5fd", tip: "Wear a warm winter coat, hat, and scarf!" },
{ id: "cool", label: "Cool", word: "cool", icon: "🧊", level: 36, fill: "#06b6d4", color: "#cffafe", keyColor: "#67e8f9", tip: "Wear a cozy sweater or light jacket!" },
{ id: "warm", label: "Warm", word: "warm", icon: "😊", level: 58, fill: "#facc15", color: "#fef9c3", keyColor: "#fde047", tip: "Wear a comfy T-shirt and play outside!" },
{ id: "hot", label: "Hot", word: "hot", icon: "🥵", level: 78, fill: "#f97316", color: "#ffedd5", keyColor: "#fdba74", tip: "Wear shorts, a sun hat, and drink lots of water!" },
{ id: "very-hot", label: "Very Hot", word: "very hot", icon: "🔥", level: 96, fill: "#ef4444", color: "#fee2e2", keyColor: "#fca5a5", tip: "Stay cool in the shade and drink cold water!" }
];
const CONFIG = {
goal: 30,
maxStudents: 30,
storageKey: "weather-and-date-chart-v1-0"
};
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const weatherById = id => WEATHERS.find(item => item.id === id) || WEATHERS[0];
const tempById = id => TEMPERATURES.find(item => item.id === id) || TEMPERATURES[2];
function getOrdinalSuffix(n) {
const v = n % 100;
if (v >= 11 && v <= 13) return "th";
switch (n % 10) {
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
}
function getTodaySelection() {
const now = new Date();
const jsDay = now.getDay();
const dayIndex = jsDay === 0 ? 6 : jsDay - 1;
const monthIndex = now.getMonth();
const dateNum = now.getDate();
const yearNum = YEARS.includes(now.getFullYear()) ? now.getFullYear() : 2026;
return {
day: DAYS[dayIndex],
month: MONTHS[monthIndex].name,
date: dateNum,
year: yearNum
};
}
const initialDate = getTodaySelection();
const S = {
screen: "title",
stage: "date",
boardTab: "map",
speaker: 1,
day: initialDate.day,
month: initialDate.month,
date: initialDate.date,
year: initialDate.year,
selectedWeather: WEATHERS[0],
selectedTemp: TEMPERATURES[2],
stories: [],
reactions: { yay: 0, cozy: 0, fun: 0 },
sound: true,
classComplete: false,
audioContext: null,
decodedBuffers: {},
activeSources: [],
activeHtmlAudio: null,
toastTimer: 0
};
/* PROPORTIONAL VIEWPORT CONTAINMENT SCALER */
function fitAppToScreen() {
const pad = document.fullscreenElement ? 0 : 18;
const availW = Math.max(320, window.innerWidth - pad);
const availH = Math.max(240, window.innerHeight - pad);
const scale = Math.min(availW / 1280, availH / 720);
document.documentElement.style.setProperty("--app-scale", scale.toFixed(4));
}
/* MICROSOFT NEURAL JENNY (en-US-JennyNeural) VOICE ENGINE */
function ensureAudio() {
if (!S.sound) return null;
if (!S.audioContext) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (AudioContextClass) S.audioContext = new AudioContextClass();
}
if (S.audioContext && S.audioContext.state === "suspended") {
S.audioContext.resume();
}
return S.audioContext;
}
function stopAllSpeech() {
S.activeSources.forEach(src => {
try { src.stop(); } catch (_) {}
});
S.activeSources = [];
if (S.activeHtmlAudio) {
try {
S.activeHtmlAudio.pause();
S.activeHtmlAudio.currentTime = 0;
} catch (_) {}
S.activeHtmlAudio = null;
}
if ("speechSynthesis" in window) window.speechSynthesis.cancel();
}
async function getClipBuffer(key) {
if (S.decodedBuffers[key]) return S.decodedBuffers[key];
const clip = window.JENNY_AUDIO?.[key];
if (!clip || !clip.src) return null;
const ctx = ensureAudio();
if (!ctx) return null;
const base64 = clip.src.split(",")[1];
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const audioBuffer = await ctx.decodeAudioData(bytes.buffer.slice(0));
S.decodedBuffers[key] = audioBuffer;
return audioBuffer;
}
async function playJennySequence(keys, gapSeconds = 0.05) {
if (!S.sound) return;
stopAllSpeech();
const validKeys = keys.filter(k => window.JENNY_AUDIO?.[k]);
if (!validKeys.length) return;
const ctx = ensureAudio();
if (ctx) {
try {
const buffers = await Promise.all(validKeys.map(k => getClipBuffer(k)));
let cursor = ctx.currentTime + 0.02;
buffers.forEach((buf, idx) => {
if (!buf) return;
const clipMeta = window.JENNY_AUDIO[validKeys[idx]];
const [startSpan, endSpan] = clipMeta.span || [0, buf.duration];
const startOffset = Math.max(0, Math.min(startSpan, buf.duration - 0.05));
const duration = Math.max(0.08, Math.min(endSpan - startOffset, buf.duration - startOffset));
const source = ctx.createBufferSource();
source.buffer = buf;
source.connect(ctx.destination);
source.start(cursor, startOffset, duration);
S.activeSources.push(source);
cursor += duration + gapSeconds;
});
return;
} catch (err) {
console.warn("WebAudio buffer playback fell back to HTML5 Audio:", err);
}
}
// Fallback sequential HTML5 Audio playback
for (const k of validKeys) {
const clip = window.JENNY_AUDIO[k];
if (!clip) continue;
await new Promise(resolve => {
const audio = new Audio(clip.src);
S.activeHtmlAudio = audio;
audio.onended = resolve;
audio.onerror = resolve;
audio.play().catch(resolve);
});
}
}
function getDateSequenceKeys() {
return [
`today_${S.day.toLowerCase()}`,
`month_${S.month.toLowerCase()}`,
`ord_${S.date}`,
`year_${S.year}`
];
}
function getWeatherSentenceKey() {
return `wt_${S.selectedWeather.id}_${S.selectedTemp.id}`;
}
function speakDateSentence() {
return playJennySequence(getDateSequenceKeys(), 0.05);
}
function speakWeatherSentence() {
const wtKey = getWeatherSentenceKey();
if (window.JENNY_AUDIO?.[wtKey]) {
return playJennySequence([wtKey]);
}
return playJennySequence([`weather_${S.selectedWeather.id}`, `temp_${S.selectedTemp.id}`], 0.08);
}
function speakFullReport() {
const wtKey = getWeatherSentenceKey();
const seq = [...getDateSequenceKeys()];
if (window.JENNY_AUDIO?.[wtKey]) seq.push(wtKey);
return playJennySequence(seq, 0.07);
}
function sanitizeText(value) {
return String(value || "").replace(/[<>]/g, "").replace(/\s+/g, " ").trim();
}
function buildDateSentence(day = S.day, month = S.month, date = S.date, year = S.year) {
return `Today is ${day}, ${month} ${date}${getOrdinalSuffix(date)}, ${year}.`;
}
function buildWeatherSentence(weather = S.selectedWeather, temp = S.selectedTemp) {
return `It is ${weather.word} and ${temp.word} outside!`;
}
function loadSavedState() {
try {
const saved = JSON.parse(localStorage.getItem(CONFIG.storageKey) || "null");
if (!saved) return;
if (Array.isArray(saved.stories)) S.stories = saved.stories.slice(0, CONFIG.maxStudents);
if (saved.reactions) S.reactions = { ...S.reactions, ...saved.reactions };
S.speaker = Math.max(1, Number(saved.speaker) || S.stories.length + 1);
S.sound = saved.sound !== false;
S.classComplete = saved.classComplete === true;
if (saved.day && DAYS.includes(saved.day)) S.day = saved.day;
if (saved.month && MONTHS.some(m => m.name === saved.month)) S.month = saved.month;
if (saved.date >= 1 && saved.date <= 31) S.date = Number(saved.date);
if (YEARS.includes(Number(saved.year))) S.year = Number(saved.year);
} catch (err) {
console.warn("Saved Weather Chart state could not be read.", err);
}
}
function saveState() {
try {
localStorage.setItem(CONFIG.storageKey, JSON.stringify({
stories: S.stories,
reactions: S.reactions,
speaker: S.speaker,
sound: S.sound,
classComplete: S.classComplete,
day: S.day,
month: S.month,
date: S.date,
year: S.year
}));
} catch (err) {
console.warn("Weather Chart state could not be saved.", err);
}
}
function showScreen(name) {
S.screen = name;
$$(".screen").forEach(screen => screen.classList.toggle("active", screen.id === `${name}-screen`));
$("#teacher-bar").classList.toggle("visible", name !== "title");
document.body.dataset.screen = name;
}
function toggleBrandMenu(force) {
const popover = $("#brand-popover");
const button = $("#brand-menu-button");
const shouldOpen = typeof force === "boolean" ? force : popover.hidden;
popover.hidden = !shouldOpen;
button.setAttribute("aria-expanded", String(shouldOpen));
}
function goHome() {
toggleBrandMenu(false);
showScreen("title");
updateHeader();
}
function setStage(stage) {
S.stage = stage;
$$(".stage").forEach(panel => panel.classList.toggle("active", panel.id === `${stage}-stage`));
if (stage === "date") {
renderDateStage();
playJennySequence(["prompt_date"]);
} else if (stage === "weather") {
playJennySequence(["prompt_weather"]);
} else if (stage === "temp") {
renderTempStage();
playJennySequence([`weather_${S.selectedWeather.id}`, "prompt_temp"], 0.12);
} else if (stage === "speak") {
renderSpeakStage();
speakFullReport();
} else if (stage === "chart") {
renderChart();
}
}
function beginGame(startFresh = false, initialMode = "guided") {
ensureAudio();
if (startFresh && S.stories.length && !confirm("Start a new class and clear the current weather chart?")) return;
if (startFresh) clearClass(false);
showScreen("play");
if (initialMode === "slot") {
switchBoardTab("slot");
setStage("chart");
speakFullReport();
return;
}
resetTurn();
}
function resetTurn() {
S.speaker = Math.min(S.stories.length + 1, CONFIG.maxStudents);
$("#speaker-name").value = "";
setStage("date");
updateHeader();
}
/* STAGE 1: COMPACT DATE STAGE */
function renderDateStage() {
const ord = getOrdinalSuffix(S.date);
$("#display-day").textContent = S.day;
$("#display-month").textContent = S.month;
$("#display-date").textContent = S.date;
$("#display-ordinal").textContent = ord;
$("#display-year").textContent = S.year;
$("#live-date-sentence").textContent = buildDateSentence();
$("#chips-day").innerHTML = DAYS.map((day, idx) => `
<button class="chip-btn numbered-choice ${day === S.day ? "selected" : ""}" type="button" data-pick-day="${day}" data-key="${idx + 1}">
${day.slice(0, 3)}
</button>
`).join("");
$("#chips-month").innerHTML = MONTHS.map(m => `
<button class="chip-btn ${m.name === S.month ? "selected" : ""}" type="button" data-pick-month="${m.name}">
${m.icon} ${m.short}
</button>
`).join("");
$("#chips-date").innerHTML = Array.from({ length: 31 }, (_, i) => i + 1).map(num => `
<button class="chip-btn ${num === S.date ? "selected" : ""}" type="button" data-pick-date="${num}">
${num}${getOrdinalSuffix(num)}
</button>
`).join("");
$("#chips-year").innerHTML = YEARS.map(yr => `
<button class="chip-btn ${yr === S.year ? "selected" : ""}" type="button" data-pick-year="${yr}">
${yr}
</button>
`).join("");
syncSlotBoardValues();
}
function spinField(field, dir) {
ensureAudio();
if (field === "day") {
const idx = (DAYS.indexOf(S.day) + dir + DAYS.length) % DAYS.length;
S.day = DAYS[idx];
playJennySequence([`day_${S.day.toLowerCase()}`]);
} else if (field === "month") {
const idx = (MONTHS.findIndex(m => m.name === S.month) + dir + MONTHS.length) % MONTHS.length;
S.month = MONTHS[idx].name;
playJennySequence([`month_${S.month.toLowerCase()}`]);
} else if (field === "date") {
S.date = ((S.date - 1 + dir + 31) % 31) + 1;
playJennySequence([`ord_${S.date}`]);
} else if (field === "year") {
const idx = (YEARS.indexOf(S.year) + dir + YEARS.length) % YEARS.length;
S.year = YEARS[idx];
playJennySequence([`year_${S.year}`]);
} else if (field === "weather") {
const idx = (WEATHERS.findIndex(w => w.id === S.selectedWeather.id) + dir + WEATHERS.length) % WEATHERS.length;
S.selectedWeather = WEATHERS[idx];
playJennySequence([`weather_${S.selectedWeather.id}`]);
} else if (field === "temp") {
const idx = (TEMPERATURES.findIndex(t => t.id === S.selectedTemp.id) + dir + TEMPERATURES.length) % TEMPERATURES.length;
S.selectedTemp = TEMPERATURES[idx];
playJennySequence([`temp_${S.selectedTemp.id}`]);
}
saveState();
renderDateStage();
}
function syncSlotBoardValues() {
$("#slot-val-day").textContent = S.day;
$("#slot-val-month").textContent = S.month;
$("#slot-val-date").textContent = `${S.date}${getOrdinalSuffix(S.date)}`;
$("#slot-val-year").textContent = S.year;
$("#slot-val-weather").textContent = `${S.selectedWeather.icon} ${S.selectedWeather.label}`;
$("#slot-val-temp").textContent = `${S.selectedTemp.icon} ${S.selectedTemp.label}`;
$("#chart-banner-date").textContent = buildDateSentence();
}
/* STAGE 2: CHOOSE WEATHER */
function chooseWeather(id) {
ensureAudio();
if (id === "other") {
openCustomDialog();
return;
}
S.selectedWeather = weatherById(id);
syncSlotBoardValues();
setStage("temp");
}
/* STAGE 3: TEMPERATURE & THERMOMETER */
function renderTempStage() {
const w = S.selectedWeather;
const t = S.selectedTemp;
$("#temp-art").src = w.image;
$("#temp-weather-label").textContent = `${w.icon} ${w.label}`;
updateThermometerUI();
$("#temp-choices").innerHTML = TEMPERATURES.map((temp, index) => `
<button class="action-choice numbered-choice ${temp.id === t.id ? "selected" : ""}" type="button" data-temp="${temp.id}" data-key="${index + 1}" style="--choice-color:${temp.color};--key-color:${temp.keyColor}">
<span class="action-icon" aria-hidden="true">${temp.icon}</span>
<span>${temp.label}</span>
<span class="keycap" aria-hidden="true">${index + 1}</span>
</button>
`).join("");
}
function updateThermometerUI() {
const t = S.selectedTemp;
const w = S.selectedWeather;
$("#stage-therm-bulb").textContent = t.icon;
$("#stage-therm-bulb").style.background = t.fill;
$("#stage-therm-fill").style.width = `${t.level}%`;
$("#stage-therm-fill").style.background = t.fill;
$("#stage-therm-readout").textContent = `${t.icon} ${t.label.toUpperCase()}`;
$("#stage-therm-readout").style.color = t.fill;
$("#temp-mascot-avatar").textContent = `${w.mascot}${t.icon}`;
$("#temp-mascot-title").textContent = `${w.label} & ${t.label}`;
$("#temp-mascot-tip").textContent = `${w.gearTip} ${t.tip}`;
syncSlotBoardValues();
}
function chooseTemp(id, advance = true) {
ensureAudio();
S.selectedTemp = tempById(id);
renderTempStage();
if (advance) {
setStage("speak");
} else {
playJennySequence([`temp_${S.selectedTemp.id}`]);
}
}
/* STAGE 4: SPEAK STAGE */
function renderSpeakStage() {
const w = S.selectedWeather;
const t = S.selectedTemp;
$("#speak-art").src = w.image;
const s1 = buildDateSentence();
const s2 = buildWeatherSentence();
$("#date-sentence-text").textContent = s1;
$("#weather-sentence-text").textContent = s2;
$("#speak-mascot-pill").textContent = `🧸 Weather Bear: ${w.gearTip} ${t.tip}`;
$("#speak-mascot-avatar").textContent = `${w.mascot}${t.icon}`;
$("#speak-mascot-title").textContent = `${w.label} · ${t.label}`;
}
/* RECORD REPORT & SHOW CHART */
function completeStory() {
const typedName = sanitizeText($("#speaker-name").value);
const story = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
speakerNumber: S.speaker,
speakerName: typedName || `Reporter ${S.speaker}`,
dateSentence: buildDateSentence(),
weatherSentence: buildWeatherSentence(),
weather: { ...S.selectedWeather },
temp: { ...S.selectedTemp }
};
S.stories.push(story);
S.speaker = Math.min(S.stories.length + 1, CONFIG.maxStudents);
if (S.stories.length >= CONFIG.maxStudents) S.classComplete = true;
$("#speaker-name").value = "";
saveState();
switchBoardTab("map");
setStage("chart");
renderChart(story.weather.id);
playJennySequence(["prompt_chart"]);
burst(14);
toast(`${story.speakerName} added a Weather Star! ⭐`);
}
function switchBoardTab(tab) {
S.boardTab = tab;
const isMap = tab === "map";
$("#tab-map-view").classList.toggle("active", isMap);
$("#tab-map-view").setAttribute("aria-selected", String(isMap));
$("#tab-slot-view").classList.toggle("active", !isMap);
$("#tab-slot-view").setAttribute("aria-selected", String(!isMap));
$("#pane-map-view").hidden = !isMap;
$("#pane-slot-view").hidden = isMap;
syncSlotBoardValues();
}
function renderChart(changedWeatherId = "") {
const counts = Object.fromEntries(WEATHERS.map(w => [w.id, 0]));
S.stories.forEach(story => {
const wid = counts[story.weather.id] !== undefined ? story.weather.id : "sunny";
counts[wid] += 1;
});
$("#weather-tally-grid").innerHTML = WEATHERS.map(w => `
<div class="tally-card ${changedWeatherId === w.id ? "changed" : ""}">
<span class="tally-icon" aria-hidden="true">${w.icon}</span>
<span class="tally-name">${w.label}</span>
<span class="tally-num">${counts[w.id]}</span>
</div>
`).join("");
$("#total-stories").textContent = S.stories.length;
const last = S.stories.at(-1);
$("#last-story").textContent = last
? `${last.speakerName}: “${last.dateSentence} ${last.weatherSentence}”`
: "Your first weather report will appear here.";
$("#story-log").innerHTML = S.stories.map(s => `<li>${s.speakerName}: ${s.dateSentence} ${s.weatherSentence}</li>`).join("");
const answered = Math.min(S.stories.length, CONFIG.maxStudents);
const nextNumber = Math.min(answered + 1, CONFIG.maxStudents);
$("#class-progress-text").textContent = `${answered} of up to ${CONFIG.maxStudents} reporters shared`;
$("#next-student-text").textContent = S.classComplete
? "The class weather chart is complete!"
: `Reporter ${nextNumber} is next.`;
$("#next-speaker-button").textContent = answered >= CONFIG.maxStudents
? "All 30 reporters shared"
: `Next reporter → ${nextNumber}/${CONFIG.maxStudents}`;
$("#next-speaker-button").disabled = answered >= CONFIG.maxStudents;
renderMapStars();
updateReactions();
updateHeader();
syncSlotBoardValues();
}
function renderMapStars() {
const positions = [
[20, 28], [32, 24], [68, 25], [78, 32],
[24, 52], [36, 48], [64, 50], [75, 55],
[45, 26], [55, 28], [48, 62], [58, 60]
];
$("#map-stars").innerHTML = S.stories.slice(-24).map((story, idx) => {
const pos = positions[idx % positions.length];
const offset = Math.floor(idx / positions.length) * 3;
return `<span class="map-star" style="left:${pos[0] + offset}%;top:${pos[1] + offset}%;animation-delay:${Math.min(idx * .03, .36)}s">${story.weather.icon || "★"}</span>`;
}).join("");
}
function nextSpeaker() {
saveState();
resetTurn();
}
function undoStory() {
if (!S.stories.length) {
toast("There is no weather report to undo yet.");
return;
}
const story = S.stories.pop();
S.classComplete = false;
S.speaker = story.speakerNumber;
S.selectedWeather = story.weather;
S.selectedTemp = story.temp;
$("#speaker-name").value = story.speakerName.startsWith("Reporter ") ? "" : story.speakerName;
saveState();
updateHeader();
showScreen("play");
setStage("speak");
toast("Last report removed—ready to change!");
}
function addReaction(kind) {
if (!Object.hasOwn(S.reactions, kind)) return;
S.reactions[kind] += 1;
saveState();
updateReactions();
const clipMap = { yay: "react_yay", cozy: "react_cozy", fun: "react_fun" };
playJennySequence([clipMap[kind]]);
}
function updateReactions() {
Object.entries(S.reactions).forEach(([key, value]) => {
const node = $(`#reaction-${key}`);
if (node) node.textContent = value;
});
}
function updateHeader() {
const currentStudent = Math.min(Math.max(S.stories.length + 1, 1), CONFIG.maxStudents);
$("#speaker-label").textContent = S.classComplete
? `Class complete · ${S.stories.length}`
: `Reporter ${currentStudent} of ${CONFIG.maxStudents}`;
const progress = Math.min(S.stories.length / CONFIG.goal, 1) * 100;
$("#meter-fill").style.width = `${progress}%`;
$("#meter-label").textContent = `${S.stories.length} / ${CONFIG.maxStudents}`;
$("#sound-button").classList.toggle("active", S.sound);
$("#sound-button").setAttribute("aria-pressed", String(S.sound));
$("#sound-icon").textContent = S.sound ? "♪" : "×";
$("#start-button").textContent = S.stories.length
? `Continue Chart · ${S.stories.length} ${S.stories.length === 1 ? "report" : "reports"}`
: "Start Weather Chart";
$("#new-class-button").hidden = !S.stories.length;
}
function openCustomDialog() {
$("#custom-weather-input").value = "";
$("#custom-dialog").showModal();
window.setTimeout(() => $("#custom-weather-input").focus(), 40);
}
function saveCustomWeather(event) {
event.preventDefault();
const word = sanitizeText($("#custom-weather-input").value).replace(/[.!?]+$/, "").toLowerCase();
if (!word) {
$("#custom-weather-input").focus();
toast("Please type a weather word!");
return;
}
S.selectedWeather = {
id: "sunny",
label: word[0].toUpperCase() + word.slice(1),
word,
icon: "🌈",
image: "assets/weather-world.svg",
color: "#ff7a2d",
mascot: "🧸🌈✨",
gearTip: `Enjoy the ${word} sky today!`
};
$("#custom-dialog").close();
setStage("temp");
}
function clearClass(confirmFirst = true) {
if (confirmFirst && S.stories.length && !confirm("Clear every report and start a new class?")) return false;
S.stories = [];
S.reactions = { yay: 0, cozy: 0, fun: 0 };
S.speaker = 1;
S.classComplete = false;
localStorage.removeItem(CONFIG.storageKey);
updateHeader();
renderChart();
return true;
}
function repeatPrompt() {
if (S.stage === "date") speakDateSentence();
else if (S.stage === "weather") playJennySequence(["prompt_weather"]);
else if (S.stage === "temp") speakWeatherSentence();
else if (S.stage === "speak" || S.stage === "chart") speakFullReport();
}
function toggleSound() {
S.sound = !S.sound;
if (!S.sound) stopAllSpeech();
saveState();
updateHeader();
if (S.sound) repeatPrompt();
toast(`Voice & Sound ${S.sound ? "on" : "off"}`);
}
async function toggleFullscreen() {
try {
if (!document.fullscreenElement) await document.documentElement.requestFullscreen();
else await document.exitFullscreen();
} catch (_) {
toast("Fullscreen is not available in this browser.");
}
fitAppToScreen();
}
function toast(message) {
const node = $("#toast");
node.textContent = message;
node.classList.add("show");
window.clearTimeout(S.toastTimer);
S.toastTimer = window.setTimeout(() => node.classList.remove("show"), 2300);
}
function burst(count = 14) {
const layer = $("#effect-layer");
layer.innerHTML = "";
const icons = ["★", "☀️", "🌈", "✦"];
for (let index = 0; index < count; index += 1) {
const star = document.createElement("span");
const angle = (Math.PI * 2 * index / count) + (Math.random() * .25);
const distance = 160 + Math.random() * 280;
star.className = "burst-star";
star.textContent = icons[index % icons.length];
star.style.setProperty("--x", `${Math.cos(angle) * distance}px`);
star.style.setProperty("--y", `${Math.sin(angle) * distance}px`);
star.style.setProperty("--r", `${Math.round(Math.random() * 540 - 270)}deg`);
star.style.setProperty("--size", `${22 + Math.round(Math.random() * 26)}px`);
star.style.animationDelay = `${Math.random() * .12}s`;
layer.appendChild(star);
}
window.setTimeout(() => { layer.innerHTML = ""; }, 1450);
}
function activateNumberChoice(number) {
if (S.screen !== "play") return;
const stage = $(`#${S.stage}-stage`);
const choice = stage?.querySelector(`[data-key="${number}"]`);
choice?.click();
}
function bindThermometerDrag() {
const bar = $("#stage-therm-drag");
let dragging = false;
function updateFromPointer(e) {
const rect = bar.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const idx = Math.min(TEMPERATURES.length - 1, Math.floor((x / rect.width) * TEMPERATURES.length));
if (TEMPERATURES[idx].id !== S.selectedTemp.id) {
S.selectedTemp = TEMPERATURES[idx];
renderTempStage();
playJennySequence([`temp_${S.selectedTemp.id}`]);
}
}
bar.addEventListener("pointerdown", e => {
dragging = true;
bar.setPointerCapture(e.pointerId);
updateFromPointer(e);
});
bar.addEventListener("pointermove", e => {
if (dragging) updateFromPointer(e);
});
const stop = e => {
if (!dragging) return;
dragging = false;
bar.releasePointerCapture(e.pointerId);
};
bar.addEventListener("pointerup", stop);
bar.addEventListener("pointercancel", stop);
}
function bindEvents() {
window.addEventListener("resize", fitAppToScreen);
document.addEventListener("fullscreenchange", fitAppToScreen);
$("#start-button").addEventListener("click", () => beginGame(false, "guided"));
$("#quick-board-button").addEventListener("click", () => beginGame(false, "slot"));
$("#new-class-button").addEventListener("click", () => beginGame(true, "guided"));
$("#use-today-button").addEventListener("click", () => {
const today = getTodaySelection();
Object.assign(S, today);
renderDateStage();
speakDateSentence();
toast(`Today: ${buildDateSentence()}`);
});
$("#random-date-button").addEventListener("click", () => {
S.day = DAYS[Math.floor(Math.random() * DAYS.length)];
S.month = MONTHS[Math.floor(Math.random() * MONTHS.length)].name;
S.date = Math.floor(Math.random() * 28) + 1;
renderDateStage();
speakDateSentence();
});
$("#speak-date-preview-btn").addEventListener("click", speakDateSentence);
$("#date-sentence-bar-click").addEventListener("click", speakDateSentence);
$("#display-day").addEventListener("click", () => playJennySequence([`day_${S.day.toLowerCase()}`]));
$("#display-month").addEventListener("click", () => playJennySequence([`month_${S.month.toLowerCase()}`]));
$("#display-date-wrap").addEventListener("click", () => playJennySequence([`ord_${S.date}`]));
$("#display-year").addEventListener("click", () => playJennySequence([`year_${S.year}`]));
$("#to-weather-button").addEventListener("click", () => setStage("weather"));
document.addEventListener("click", event => {
const spinBtn = event.target.closest("[data-spin]");
if (spinBtn) {
spinField(spinBtn.dataset.spin, Number(spinBtn.dataset.dir));
return;
}
const dayBtn = event.target.closest("[data-pick-day]");
if (dayBtn) {
S.day = dayBtn.dataset.pickDay;
renderDateStage();
playJennySequence([`day_${S.day.toLowerCase()}`]);
return;
}
const monthBtn = event.target.closest("[data-pick-month]");
if (monthBtn) {
S.month = monthBtn.dataset.pickMonth;
renderDateStage();
playJennySequence([`month_${S.month.toLowerCase()}`]);
return;
}
const dateBtn = event.target.closest("[data-pick-date]");
if (dateBtn) {
S.date = Number(dateBtn.dataset.pickDate);
renderDateStage();
playJennySequence([`ord_${S.date}`]);
return;
}
const yearBtn = event.target.closest("[data-pick-year]");
if (yearBtn) {
S.year = Number(yearBtn.dataset.pickYear);
renderDateStage();
playJennySequence([`year_${S.year}`]);
return;
}
if (!event.target.closest(".brand-menu")) toggleBrandMenu(false);
});
$$("[data-wheel]").forEach(pill => {
pill.addEventListener("wheel", e => {
e.preventDefault();
spinField(pill.dataset.wheel, e.deltaY > 0 ? 1 : -1);
}, { passive: false });
});
$$("[data-weather]").forEach(btn => {
btn.addEventListener("click", () => chooseWeather(btn.dataset.weather));
});
$("#back-to-date-button").addEventListener("click", () => setStage("date"));
$("#temp-choices").addEventListener("click", event => {
const btn = event.target.closest("[data-temp]");
if (btn) chooseTemp(btn.dataset.temp, true);
});
$("#stage-therm-down").addEventListener("click", () => {
const idx = Math.max(0, TEMPERATURES.findIndex(t => t.id === S.selectedTemp.id) - 1);
chooseTemp(TEMPERATURES[idx].id, false);
});
$("#stage-therm-up").addEventListener("click", () => {
const idx = Math.min(TEMPERATURES.length - 1, TEMPERATURES.findIndex(t => t.id === S.selectedTemp.id) + 1);
chooseTemp(TEMPERATURES[idx].id, false);
});
$("#back-to-weather").addEventListener("click", () => setStage("weather"));
bindThermometerDrag();
$$("[data-sentence]").forEach(btn => {
btn.addEventListener("click", () => {
const kind = btn.dataset.sentence;
if (kind === "date") speakDateSentence();
else if (kind === "weather") speakWeatherSentence();
else playJennySequence([`tip_${S.selectedWeather.id}`]);
});
});
$("#change-answer-button").addEventListener("click", () => setStage("temp"));
$("#said-it-button").addEventListener("click", completeStory);
$("#tab-map-view").addEventListener("click", () => switchBoardTab("map"));
$("#tab-slot-view").addEventListener("click", () => switchBoardTab("slot"));
$("#slot-speak-all-btn").addEventListener("click", speakFullReport);
$("#slot-add-star-btn").addEventListener("click", completeStory);
$("#next-speaker-button").addEventListener("click", nextSpeaker);
$("#reset-class-chart-btn").addEventListener("click", () => {
if (clearClass(true)) resetTurn();
});
$("#previous-story-button").addEventListener("click", undoStory);
$("#brand-menu-button").addEventListener("click", event => {
event.stopPropagation();
toggleBrandMenu();
});
$("#home-button").addEventListener("click", goHome);
$("#menu-guided-button").addEventListener("click", () => {
toggleBrandMenu(false);
showScreen("play");
setStage("date");
});
$("#menu-slot-button").addEventListener("click", () => {
toggleBrandMenu(false);
showScreen("play");
switchBoardTab("slot");
setStage("chart");
speakFullReport();
});
$("#menu-chart-button").addEventListener("click", () => {
toggleBrandMenu(false);
showScreen("play");
switchBoardTab("map");
setStage("chart");
});
$$("[data-reaction]").forEach(btn => btn.addEventListener("click", () => addReaction(btn.dataset.reaction)));
$("#repeat-button").addEventListener("click", repeatPrompt);
$("#undo-button").addEventListener("click", undoStory);
$("#sound-button").addEventListener("click", toggleSound);
$("#fullscreen-button").addEventListener("click", toggleFullscreen);
$("#custom-form").addEventListener("submit", saveCustomWeather);
$("#cancel-dialog").addEventListener("click", () => $("#custom-dialog").close());
$("#speaker-name").addEventListener("change", saveState);
window.addEventListener("keydown", event => {
if ($("#custom-dialog").open || ["INPUT", "TEXTAREA"].includes(event.target.tagName)) return;
if (/^[1-7]$/.test(event.key)) activateNumberChoice(event.key);
else if (event.key.toLowerCase() === "r") repeatPrompt();
else if (event.key.toLowerCase() === "u" || event.key === "Backspace") { event.preventDefault(); undoStory(); }
else if (event.key.toLowerCase() === "m") toggleSound();
else if (event.key.toLowerCase() === "f") toggleFullscreen();
else if (event.key === "Enter" && S.screen === "play") {
if (S.stage === "date") setStage("weather");
else if (S.stage === "temp") setStage("speak");
else if (S.stage === "speak") completeStory();
else if (S.stage === "chart") nextSpeaker();
}
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) stopAllSpeech();
});
}
function init() {
fitAppToScreen();
loadSavedState();
bindEvents();
renderDateStage();
renderChart();
updateHeader();
}
init();
</script>
</body>
</html>