\n' + GRID_HTML[_LEVEL_SPLIT_INDEX:]
# =========================================================
# CSS
# =========================================================
CSS = """
html,
body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
.gradio-container {
width: 100vw !important;
}
#game-shell {
width: 100% !important;
}
footer {
display: none !important;
}
.memory-container {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: clamp(12px, 1.5vw, 18px);
padding: clamp(10px, 1.6vw, 22px);
background:
radial-gradient(circle at top left, rgba(99,102,241,0.28), transparent 32%),
radial-gradient(circle at bottom right, rgba(14,165,233,0.18), transparent 30%),
linear-gradient(135deg, #0b1020 0%, #111827 52%, #020617 100%);
border-radius: 28px;
position: relative;
width: 100%;
max-width: 100vw;
min-height: 0;
height: auto;
max-height: none;
color: #f8fafc;
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.45);
box-sizing: border-box;
}
.memory-container.level-loading .board-frame {
visibility: hidden;
}
.game-brand {
width: min(100%, 1480px);
z-index: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 14px;
padding: 4px 4px 0;
box-sizing: border-box;
}
.game-brand-mark {
width: clamp(54px, 4.8vw, 72px);
height: clamp(54px, 4.8vw, 72px);
flex: 0 0 auto;
display: grid;
place-items: center;
border-radius: 18px;
background:
radial-gradient(circle at 35% 35%, rgba(255,255,255,0.18), transparent 42%),
radial-gradient(circle at 50% 50%, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.04) 70%);
box-shadow:
0 0 0 1px rgba(196, 181, 253, 0.18) inset,
0 0 18px rgba(168, 85, 247, 0.22);
}
.game-brand-logo {
width: 70%;
height: 70%;
object-fit: contain;
display: block;
}
.game-brand-copy {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.game-brand-title {
font-size: clamp(28px, 2.9vw, 42px);
font-weight: 900;
letter-spacing: -0.03em;
color: #f8fafc;
}
.game-brand-subtitle {
font-size: clamp(14px, 1.05vw, 17px);
line-height: 1.45;
color: #cbd5e1;
max-width: 72ch;
}
.memory-container::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
background-image:
linear-gradient(rgba(148,163,184,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(148,163,184,0.04) 1px, transparent 1px);
background-size: 32px 32px;
pointer-events: none;
opacity: 0.35;
}
.board-frame {
width: min(100%, 1480px);
max-width: 100%;
height: auto;
max-height: none;
position: relative;
z-index: 1;
--board-frame-padding: clamp(14px, 1.8vw, 26px);
border-radius: 28px;
border: 1px solid rgba(148, 163, 184, 0.18);
background: linear-gradient(180deg, rgba(15, 23, 42, 0.72), rgba(2, 6, 23, 0.84));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 28px 90px rgba(2, 6, 23, 0.42);
padding: var(--board-frame-padding);
display: flex;
flex-direction: column;
gap: clamp(16px, 2vw, 24px);
box-sizing: border-box;
overflow: visible;
}
#status-layout {
position: relative;
width: 100%;
min-height: 0;
display: flex;
flex-direction: column;
gap: clamp(14px, 1.6vw, 20px);
overflow: visible;
}
.preview-overlay {
position: absolute;
inset: 0;
background: rgba(2, 6, 23, 0.46);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
border-radius: 28px;
transition: opacity 0.45s ease;
pointer-events: none;
}
.memory-container.preview-wobbling .preview-overlay {
background: rgba(2, 6, 23, 0.18);
backdrop-filter: blur(1px);
}
.memory-container.preview-wobbling .preview-content {
transform: scale(0.96);
transition: transform 0.25s ease, opacity 0.25s ease;
opacity: 0.95;
}
.preview-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.transition-overlay {
position: absolute;
inset: 0;
background: rgba(2, 6, 23, 0.82);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 45;
border-radius: 28px;
transition: opacity 0.25s ease;
}
.transition-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.dialog-overlay {
position: absolute;
inset: 0;
background: rgba(2, 6, 23, 0.78);
backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: center;
z-index: 55;
border-radius: 28px;
transition: opacity 0.25s ease;
}
#leaderboard-overlay {
position: fixed;
inset: 0;
z-index: 85;
}
.dialog-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.start-overlay {
position: fixed;
inset: 0;
width: 100vw;
height: 100dvh;
min-height: 100vh;
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
background:
radial-gradient(circle at 50% 28%, rgba(139, 92, 246, 0.16), transparent 18%),
radial-gradient(circle at 50% 64%, rgba(14, 165, 233, 0.24), transparent 20%),
radial-gradient(circle at 50% 64%, rgba(168, 85, 247, 0.18), transparent 30%),
linear-gradient(180deg, #040714 0%, #06122a 55%, #020617 100%);
backdrop-filter: blur(8px);
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.start-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.start-stage {
position: relative;
width: 200vw;
height: 200dvh;
min-height: 200vh;
margin: 0 auto;
padding: clamp(18px, 3vw, 48px) clamp(18px, 5vw, 64px);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
transform: scale(0.5);
transform-origin: center center;
}
.start-hero {
position: relative;
z-index: 2;
width: min(100%, 820px);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: clamp(14px, 1.6vw, 22px);
}
.start-mark {
width: clamp(58px, 7vw, 86px);
height: clamp(58px, 7vw, 86px);
display: grid;
place-items: center;
border-radius: 50%;
background:
radial-gradient(circle at 35% 35%, rgba(255,255,255,0.2), transparent 40%),
radial-gradient(circle at 50% 50%, rgba(168, 85, 247, 0.24), rgba(168, 85, 247, 0.02) 70%);
box-shadow:
0 0 0 1px rgba(196, 181, 253, 0.22) inset,
0 0 18px rgba(168, 85, 247, 0.35),
0 0 40px rgba(168, 85, 247, 0.2);
}
.start-mark-icon {
font-size: clamp(28px, 3.4vw, 42px);
filter: drop-shadow(0 0 14px rgba(168, 85, 247, 0.65));
}
.start-mark-logo {
width: 70%;
height: 70%;
object-fit: contain;
display: block;
filter: drop-shadow(0 0 14px rgba(168, 85, 247, 0.45));
}
.start-title-wrap {
display: flex;
flex-direction: column;
gap: clamp(8px, 1vw, 12px);
align-items: center;
}
.start-title {
margin-top: 0;
font-size: clamp(48px, 10vw, 118px);
line-height: 0.92;
font-weight: 1000;
letter-spacing: -0.06em;
color: #f7f2ff;
text-shadow:
0 1px 0 #ffffff,
0 2px 0 #d7cfff,
0 5px 0 rgba(102, 0, 255, 0.85),
0 18px 34px rgba(110, 29, 255, 0.35),
0 0 28px rgba(168, 85, 247, 0.3);
-webkit-text-stroke: 1px rgba(255,255,255,0.35);
}
.start-tagline {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.3em 0.45em;
font-size: clamp(18px, 2.4vw, 34px);
font-weight: 900;
letter-spacing: -0.02em;
}
.tag-green { color: #86efac; text-shadow: 0 0 16px rgba(34,197,94,0.45); }
.tag-blue { color: #67e8f9; text-shadow: 0 0 16px rgba(6,182,212,0.45); }
.tag-purple { color: #d8b4fe; text-shadow: 0 0 16px rgba(168,85,247,0.45); }
.start-subtitle {
margin-top: 0;
color: #eef2ff;
font-size: clamp(17px, 2vw, 34px);
line-height: 1.45;
text-wrap: balance;
text-shadow: 0 4px 18px rgba(2, 6, 23, 0.65);
}
.start-preview {
width: min(100%, 400px);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(10px, 1.7vw, 16px);
padding: clamp(4px, 1vw, 10px);
margin-top: clamp(4px, 1vw, 8px);
}
.preview-card {
aspect-ratio: 1 / 1;
border-radius: clamp(20px, 2.6vw, 32px);
background:
radial-gradient(circle at 24% 18%, rgba(255,255,255,0.34), transparent 22%),
linear-gradient(145deg, #b3f9d1 0%, #a5f3a0 100%);
box-shadow:
0 10px 0 rgba(16, 185, 129, 0.5),
0 24px 34px rgba(0, 0, 0, 0.36);
display: grid;
place-items: center;
border: 1px solid rgba(168, 255, 211, 0.4);
}
.preview-emoji {
font-size: clamp(34px, 5.2vw, 68px);
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.18));
}
.start-help-button {
width: min(100%, 560px);
margin-top: clamp(8px, 1.2vw, 14px);
padding: 16px 20px;
border: 1px solid rgba(125, 211, 252, 0.28);
border-radius: 24px;
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.12), transparent 28%),
linear-gradient(135deg, rgba(8, 47, 73, 0.96), rgba(15, 23, 42, 0.96));
color: #e0f2fe;
font-size: clamp(17px, 2vw, 24px);
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
0 18px 40px rgba(2, 6, 23, 0.35);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 14px;
}
.start-help-button:hover {
transform: translateY(-1px);
border-color: rgba(125, 211, 252, 0.46);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.1),
0 20px 44px rgba(2, 6, 23, 0.42);
}
.start-utility-row {
width: min(100%, 760px);
display: flex;
align-items: stretch;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-top: clamp(8px, 1.2vw, 14px);
}
.start-utility-row .start-help-button,
.start-utility-row .music-toggle-button {
width: auto;
flex: 1 1 260px;
margin-top: 0;
}
.start-utility-row .start-help-button {
min-width: 240px;
}
.start-utility-row .music-toggle-button {
min-width: 220px;
}
.start-utility-row .leaderboard-button {
min-width: 220px;
}
.start-utility-row .high-score-pill {
min-width: 240px;
position: relative;
overflow: hidden;
padding: 15px 18px;
border-radius: 24px;
border-color: rgba(34, 211, 238, 0.42);
background:
radial-gradient(circle at 16% 18%, rgba(255,255,255,0.18), transparent 24%),
radial-gradient(circle at 84% 18%, rgba(34, 211, 238, 0.18), transparent 22%),
radial-gradient(circle at 50% 110%, rgba(16, 185, 129, 0.2), transparent 34%),
linear-gradient(135deg, rgba(5, 92, 68, 0.98), rgba(12, 18, 35, 0.99));
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.08),
0 0 0 1px rgba(34, 211, 238, 0.08) inset,
0 18px 44px rgba(2, 6, 23, 0.44),
0 0 28px rgba(34, 211, 238, 0.16);
}
.start-utility-row .high-score-pill::before {
content: "";
position: absolute;
inset: 1px;
border-radius: inherit;
background: linear-gradient(120deg, rgba(255,255,255,0.14), transparent 34%, rgba(255,255,255,0.05));
pointer-events: none;
}
.start-utility-row .high-score-icon {
width: 30px;
height: 30px;
font-size: 17px;
color: #fef3c7;
background:
radial-gradient(circle at 30% 30%, rgba(255,255,255,0.24), transparent 42%),
linear-gradient(135deg, rgba(250, 204, 21, 0.24), rgba(34, 197, 94, 0.14));
box-shadow:
inset 0 0 0 1px rgba(251, 191, 36, 0.24),
0 0 18px rgba(250, 204, 21, 0.12);
}
.start-help-icon {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 50%;
color: #22d3ee;
background: radial-gradient(circle at 35% 30%, rgba(255,255,255,0.18), transparent 42%), rgba(34, 211, 238, 0.12);
box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.22);
font-size: 20px;
line-height: 1;
}
.start-help-text {
line-height: 1;
}
.music-toggle-button,
.utility-button {
appearance: none;
border: 1px solid rgba(125, 211, 252, 0.28);
border-radius: 24px;
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.12), transparent 28%),
linear-gradient(135deg, rgba(8, 47, 73, 0.96), rgba(15, 23, 42, 0.96));
color: #e0f2fe;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
0 18px 40px rgba(2, 6, 23, 0.35);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
min-width: 0;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
}
.leaderboard-button {
appearance: none;
border: 1px solid rgba(245, 158, 11, 0.34);
border-radius: 24px;
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.1), transparent 28%),
linear-gradient(135deg, rgba(78, 32, 0, 0.96), rgba(15, 23, 42, 0.96));
color: #fde68a;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
0 18px 40px rgba(2, 6, 23, 0.35);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
min-width: 0;
padding: 16px 20px;
font-size: clamp(15px, 1.8vw, 22px);
width: auto;
flex: 1 1 260px;
}
.leaderboard-button:hover {
transform: translateY(-1px);
border-color: rgba(251, 191, 36, 0.5);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.1),
0 20px 44px rgba(2, 6, 23, 0.42);
}
.leaderboard-button .leaderboard-icon {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 50%;
color: #fbbf24;
background: rgba(251, 191, 36, 0.12);
box-shadow: inset 0 0 0 1px rgba(251, 191, 36, 0.22);
font-size: 20px;
line-height: 1;
flex: 0 0 auto;
}
.leaderboard-text {
line-height: 1;
}
.high-score-pill {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-width: 0;
padding: 14px 18px;
border-radius: 24px;
border: 1px solid rgba(52, 211, 153, 0.28);
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.08), transparent 28%),
linear-gradient(135deg, rgba(6, 95, 70, 0.94), rgba(15, 23, 42, 0.96));
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
0 18px 40px rgba(2, 6, 23, 0.35);
color: #d1fae5;
flex: 1 1 260px;
}
.high-score-icon {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(34, 197, 94, 0.14);
box-shadow: inset 0 0 0 1px rgba(110, 231, 183, 0.22);
font-size: 19px;
line-height: 1;
flex: 0 0 auto;
}
.high-score-copy {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1 1 auto;
}
.high-score-label {
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(209, 250, 229, 0.78);
line-height: 1;
}
.high-score-user {
font-size: 12px;
font-weight: 800;
line-height: 1;
color: #e2e8f0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 18ch;
}
.high-score-value {
font-size: clamp(18px, 1.8vw, 24px);
font-weight: 1000;
letter-spacing: -0.03em;
color: #86efac;
line-height: 1;
flex: 0 0 auto;
}
.music-toggle-button:hover,
.utility-button:hover {
transform: translateY(-1px);
border-color: rgba(125, 211, 252, 0.46);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.1),
0 20px 44px rgba(2, 6, 23, 0.42);
}
.music-toggle-button {
width: min(100%, 280px);
padding: 16px 20px;
font-size: clamp(15px, 1.8vw, 22px);
}
.utility-button {
padding: 12px 18px;
font-size: clamp(13px, 1.4vw, 18px);
border-color: rgba(167, 139, 250, 0.34);
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.1), transparent 28%),
linear-gradient(135deg, rgba(41, 26, 84, 0.96), rgba(15, 23, 42, 0.96));
}
.utility-button:hover {
border-color: rgba(196, 181, 253, 0.5);
}
.utility-button-icon,
.music-toggle-icon {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 18px;
line-height: 1;
flex: 0 0 auto;
}
.utility-button-icon {
color: #d8b4fe;
background: rgba(168, 85, 247, 0.14);
box-shadow: inset 0 0 0 1px rgba(196, 181, 253, 0.22);
}
.music-toggle-icon {
color: #67e8f9;
background: rgba(34, 211, 238, 0.12);
box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.22);
}
.music-toggle-off {
border-color: rgba(248, 113, 113, 0.36);
color: #ffe4e6;
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.1), transparent 28%),
linear-gradient(135deg, rgba(69, 10, 10, 0.96), rgba(15, 23, 42, 0.96));
}
.music-toggle-off .music-toggle-icon {
color: #fca5a5;
background: rgba(248, 113, 113, 0.12);
box-shadow: inset 0 0 0 1px rgba(248, 113, 113, 0.22);
}
.top-utility-bar {
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 2px;
}
.top-utility-bar .utility-button,
.top-utility-bar .music-toggle-button {
padding: 10px 14px;
font-size: 13px;
border-radius: 18px;
}
.top-utility-bar .utility-button {
min-width: 120px;
}
.top-utility-bar .music-toggle-button {
min-width: 180px;
}
.top-utility-bar .high-score-pill {
min-width: 230px;
flex: 0 1 250px;
padding: 10px 14px;
border-radius: 18px;
}
.top-utility-bar .high-score-icon {
width: 28px;
height: 28px;
font-size: 16px;
}
.top-utility-bar .high-score-label {
font-size: 10px;
}
.top-utility-bar .high-score-user {
font-size: 11px;
max-width: 14ch;
}
.top-utility-bar .high-score-value {
font-size: 20px;
}
.start-utility-row .high-score-copy {
gap: 3px;
}
.start-utility-row .high-score-label {
font-size: 10px;
letter-spacing: 0.2em;
color: rgba(209, 250, 229, 0.9);
}
.start-utility-row .high-score-user {
font-size: clamp(16px, 1.45vw, 20px);
font-weight: 1000;
color: #ffffff;
max-width: 18ch;
line-height: 1.05;
letter-spacing: -0.03em;
text-shadow: 0 0 10px rgba(255,255,255,0.08);
}
.start-utility-row .high-score-value {
font-size: clamp(22px, 2.2vw, 28px);
color: #86efac;
text-shadow: 0 0 12px rgba(52, 211, 153, 0.28);
}
.start-button {
width: min(100%, 680px);
margin-top: clamp(10px, 1.6vw, 18px);
padding: 18px 22px;
border: 1px solid rgba(255,255,255,0.25);
border-radius: 26px;
background: linear-gradient(135deg, #1463ff 0%, #2c53ff 34%, #b52cff 100%);
color: #fff;
font-size: clamp(19px, 2.4vw, 31px);
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
box-shadow:
0 8px 0 rgba(77, 12, 156, 0.55),
0 16px 36px rgba(119, 31, 255, 0.34),
0 0 34px rgba(126, 34, 206, 0.18);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 18px;
}
.start-button-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.92em;
transform: translateY(-1px);
}
.start-note {
margin-top: 10px;
color: #94a3b8;
font-size: 12px;
line-height: 1.4;
max-width: 64ch;
}
.start-footer-row {
width: min(100%, 900px);
margin-top: 8px;
margin-bottom: 4px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
align-items: stretch;
}
.start-footer-item {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-width: 0;
padding: 10px 14px;
border-radius: 18px;
border: 1px solid rgba(125, 211, 252, 0.18);
background: rgba(8, 15, 35, 0.55);
color: #dbeafe;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}
.start-footer-label {
color: #94a3b8;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.start-footer-value {
color: #f8fafc;
font-size: 12px;
font-weight: 700;
line-height: 1.35;
overflow-wrap: anywhere;
word-break: break-word;
}
.start-attribution {
width: min(100%, 900px);
margin-top: 6px;
color: #dbeafe;
font-size: clamp(12px, 1vw, 14px);
font-weight: 700;
line-height: 1.45;
text-align: center;
}
.start-attribution a {
color: #67e8f9;
text-decoration: none;
}
.start-attribution a:hover {
text-decoration: underline;
}
.start-orbit {
position: absolute;
inset: 6% 4% auto;
height: 64%;
border-radius: 50%;
background:
radial-gradient(circle at center, rgba(34, 211, 238, 0.22) 0%, rgba(34, 211, 238, 0.06) 20%, transparent 42%),
radial-gradient(circle at center, transparent 44%, rgba(99, 102, 241, 0.2) 45%, transparent 48%),
radial-gradient(circle at center, transparent 53%, rgba(168, 85, 247, 0.16) 54%, transparent 57%);
filter: blur(0.3px);
opacity: 0.7;
pointer-events: none;
}
.start-rings {
position: absolute;
inset: 45% 8% 7%;
border-radius: 50%;
background:
radial-gradient(circle at center, transparent 0 63%, rgba(34, 211, 238, 0.08) 64%, transparent 65%),
radial-gradient(circle at center, transparent 0 72%, rgba(168, 85, 247, 0.08) 73%, transparent 74%),
radial-gradient(circle at center, transparent 0 81%, rgba(59, 130, 246, 0.08) 82%, transparent 83%);
opacity: 0.7;
pointer-events: none;
}
.start-stars {
position: absolute;
inset: 0;
background-image:
radial-gradient(circle, rgba(255,255,255,0.55) 0 1px, transparent 1px),
radial-gradient(circle, rgba(168,85,247,0.6) 0 1px, transparent 1px),
radial-gradient(circle, rgba(34,197,94,0.35) 0 1px, transparent 1px);
background-size: 36px 36px, 54px 54px, 78px 78px;
background-position: 0 0, 18px 18px, 30px 12px;
opacity: 0.12;
pointer-events: none;
animation: starDrift 18s linear infinite;
}
@keyframes starDrift {
from { transform: translateY(0); }
to { transform: translateY(18px); }
}
.dialog-card {
width: min(100%, 420px);
margin: 0 18px;
padding: 22px 24px;
border-radius: 24px;
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(15, 23, 42, 0.96);
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.56);
}
.dialog-warning {
border-color: rgba(248, 180, 57, 0.36);
box-shadow: 0 24px 80px rgba(120, 53, 15, 0.42);
}
.dialog-gameover {
position: relative;
width: min(82vw, 720px);
max-width: none;
min-height: min(54dvh, 540px);
padding: clamp(22px, 2.2vw, 34px) clamp(20px, 2.4vw, 40px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
overflow: hidden;
text-align: center;
border-radius: 30px;
border: 2px solid rgba(255, 90, 145, 0.58);
background:
radial-gradient(circle at 50% 22%, rgba(255, 74, 112, 0.20), transparent 18%),
radial-gradient(circle at 20% 0%, rgba(96, 89, 255, 0.18), transparent 28%),
radial-gradient(circle at 80% 100%, rgba(95, 122, 255, 0.10), transparent 26%),
linear-gradient(180deg, rgba(12, 20, 52, 0.98), rgba(6, 12, 34, 0.98));
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05) inset,
0 0 0 1px rgba(255, 96, 167, 0.20),
0 0 26px rgba(255, 71, 114, 0.18),
0 0 42px rgba(120, 100, 255, 0.16),
0 24px 90px rgba(2, 6, 23, 0.68);
}
.dialog-gameover::before {
content: "";
position: absolute;
inset: auto 0 0;
height: 20px;
background: linear-gradient(90deg, transparent 0%, rgba(110, 104, 255, 0.48) 18%, rgba(255, 95, 160, 0.46) 52%, rgba(110, 104, 255, 0.48) 82%, transparent 100%);
filter: blur(16px);
opacity: 0.7;
pointer-events: none;
}
.dialog-gameover::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 50% 20%, rgba(255,255,255,0.05), transparent 14%),
radial-gradient(circle at 50% 32%, rgba(255, 78, 119, 0.10), transparent 24%);
opacity: 0.8;
}
.gameover-icon-wrap {
position: relative;
width: 96px;
height: 96px;
display: grid;
place-items: center;
margin-top: 2px;
z-index: 1;
}
.gameover-icon-halo {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid rgba(255, 63, 114, 0.92);
box-shadow:
0 0 24px rgba(255, 56, 109, 0.55),
0 0 46px rgba(255, 56, 109, 0.30),
inset 0 0 12px rgba(255, 56, 109, 0.18);
background:
radial-gradient(circle at 50% 50%, rgba(255, 80, 120, 0.08), transparent 58%);
}
.gameover-icon {
position: relative;
font-size: 46px;
line-height: 1;
filter: drop-shadow(0 0 14px rgba(255, 66, 120, 0.75));
}
.gameover-title {
position: relative;
z-index: 1;
color: #f8fafc;
font-size: clamp(42px, 4.8vw, 66px);
font-weight: 1000;
letter-spacing: -0.06em;
line-height: 0.95;
text-shadow:
0 1px 0 rgba(255,255,255,0.35),
0 8px 24px rgba(0, 0, 0, 0.35);
}
.gameover-divider {
position: relative;
z-index: 1;
width: min(100%, 280px);
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 10px;
margin-top: 2px;
}
.gameover-divider span {
height: 2px;
border-radius: 999px;
background: linear-gradient(90deg, transparent, rgba(255, 80, 120, 0.96), transparent);
box-shadow: 0 0 18px rgba(255, 80, 120, 0.28);
}
.gameover-diamond {
color: #ff577b;
font-size: 18px;
text-shadow: 0 0 16px rgba(255, 80, 120, 0.68);
}
.gameover-subtitle {
position: relative;
z-index: 1;
color: #d2d7e6;
font-size: clamp(16px, 1.7vw, 24px);
line-height: 1.35;
font-weight: 500;
max-width: 30ch;
}
.gameover-score-pill {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
margin-top: 8px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(10, 18, 42, 0.76);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.05),
0 0 0 1px rgba(255, 255, 255, 0.04),
0 18px 38px rgba(2, 6, 23, 0.34);
}
.gameover-score-icon {
font-size: 34px;
filter: drop-shadow(0 0 10px rgba(255, 191, 53, 0.34));
}
.gameover-score-label {
color: #c7cedd;
font-size: 22px;
font-weight: 700;
letter-spacing: 0.03em;
}
.gameover-score-sep {
width: 1px;
height: 30px;
background: rgba(148, 163, 184, 0.34);
}
.gameover-score-value {
min-width: 28px;
color: #ff5574;
font-size: 46px;
font-weight: 900;
letter-spacing: -0.06em;
line-height: 1;
text-shadow: 0 0 18px rgba(255, 85, 116, 0.25);
}
.dialog-howto {
position: relative;
width: min(94vw, 1520px);
max-width: none;
max-height: min(90dvh, 880px);
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
padding: clamp(20px, 1.8vw, 30px);
border-radius: 28px;
border: 1px solid rgba(164, 94, 255, 0.55);
background:
radial-gradient(circle at 18% 0%, rgba(87, 52, 255, 0.14), transparent 24%),
radial-gradient(circle at 82% 8%, rgba(255, 78, 173, 0.10), transparent 22%),
linear-gradient(180deg, rgba(6, 14, 35, 0.99), rgba(4, 10, 28, 0.99));
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.04) inset,
0 0 0 1px rgba(255, 111, 196, 0.18),
0 0 40px rgba(114, 74, 255, 0.18),
0 20px 80px rgba(2, 6, 23, 0.7);
}
.dialog-howto::-webkit-scrollbar {
width: 10px;
}
.dialog-howto::-webkit-scrollbar-track {
background: rgba(10, 18, 40, 0.55);
border-radius: 999px;
}
.dialog-howto::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(173, 114, 255, 0.86), rgba(79, 196, 255, 0.86));
border-radius: 999px;
border: 2px solid rgba(4, 10, 28, 0.95);
}
#how-to-play-overlay {
position: fixed;
inset: 0;
width: 100vw;
height: 100dvh;
z-index: 80;
padding: clamp(12px, 1.6vw, 22px);
box-sizing: border-box;
}
.howto-close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 58px;
height: 58px;
border-radius: 50%;
border: 1px solid rgba(255, 120, 200, 0.55);
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.08), transparent 42%), rgba(10, 18, 40, 0.96);
color: #fff;
font-size: 42px;
line-height: 1;
cursor: pointer;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03) inset, 0 0 22px rgba(255, 118, 199, 0.18);
}
.howto-logo-wrap {
display: flex;
justify-content: center;
align-items: center;
margin: 2px 0 4px;
}
.howto-logo {
width: clamp(26px, 2.6vw, 36px);
height: clamp(26px, 2.6vw, 36px);
object-fit: contain;
display: block;
filter: drop-shadow(0 0 12px rgba(173, 114, 255, 0.35));
}
.howto-kicker {
color: #ad72ff;
font-size: clamp(15px, 1.2vw, 20px);
font-weight: 900;
letter-spacing: 0.18em;
text-transform: uppercase;
text-align: center;
}
.howto-headline {
margin-top: 8px;
color: #f4f4f7;
font-size: clamp(18px, 1.95vw, 30px);
line-height: 1.08;
font-weight: 800;
letter-spacing: -0.04em;
text-align: center;
text-wrap: balance;
}
.howto-steps {
margin-top: clamp(14px, 1.6vw, 22px);
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
align-items: stretch;
gap: 12px;
min-height: 0;
}
.howto-step-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 20px;
border: 1px solid rgba(90, 141, 255, 0.24);
background:
radial-gradient(circle at 20% 0%, rgba(255,255,255,0.04), transparent 28%),
rgba(8, 15, 35, 0.72);
min-height: 104px;
}
.howto-step-card.purple {
border-color: rgba(173, 114, 255, 0.45);
box-shadow: 0 0 0 1px rgba(173, 114, 255, 0.08) inset;
}
.howto-step-card.blue {
border-color: rgba(56, 165, 255, 0.45);
}
.howto-step-card.green {
border-color: rgba(38, 210, 120, 0.45);
}
.howto-step-icon {
width: 58px;
height: 58px;
border-radius: 50%;
display: grid;
place-items: center;
flex: 0 0 auto;
font-size: 30px;
background: rgba(255,255,255,0.04);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05);
}
.howto-step-card.purple .howto-step-icon {
border: 1px solid rgba(173, 114, 255, 0.42);
box-shadow: 0 0 0 4px rgba(173, 114, 255, 0.08), inset 0 0 0 1px rgba(173, 114, 255, 0.18);
}
.howto-step-card.blue .howto-step-icon {
border: 1px solid rgba(56, 165, 255, 0.42);
box-shadow: 0 0 0 4px rgba(56, 165, 255, 0.08), inset 0 0 0 1px rgba(56, 165, 255, 0.18);
}
.howto-step-card.green .howto-step-icon {
border: 1px solid rgba(38, 210, 120, 0.42);
box-shadow: 0 0 0 4px rgba(38, 210, 120, 0.08), inset 0 0 0 1px rgba(38, 210, 120, 0.18);
}
.howto-step-copy {
min-width: 0;
}
.howto-step-title {
font-size: clamp(15px, 1.1vw, 18px);
font-weight: 800;
letter-spacing: -0.02em;
}
.howto-step-card.purple .howto-step-title { color: #bf8cff; }
.howto-step-card.blue .howto-step-title { color: #4fc4ff; }
.howto-step-card.green .howto-step-title { color: #39eb8b; }
.howto-step-text {
margin-top: 6px;
color: #d9e1f2;
font-size: clamp(11px, 0.75vw, 13px);
line-height: 1.26;
overflow-wrap: anywhere;
word-break: break-word;
}
.howto-step-arrow {
display: grid;
place-items: center;
color: #cfe0ff;
font-size: clamp(34px, 2.5vw, 50px);
font-weight: 300;
align-self: center;
text-shadow: 0 0 16px rgba(145, 189, 255, 0.6);
}
.howto-main {
margin-top: clamp(14px, 1.6vw, 22px);
display: grid;
grid-template-columns: 1.05fr 1fr;
gap: 12px;
min-height: 0;
flex: 1 1 auto;
overflow: hidden;
}
.howto-left,
.howto-right {
border-radius: 20px;
border: 1px solid rgba(104, 139, 255, 0.16);
background: rgba(7, 14, 34, 0.68);
padding: 11px;
min-width: 0;
min-height: 0;
}
.howto-left {
display: grid;
gap: 6px;
}
.howto-rule {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 9px;
border-radius: 14px;
border: 1px solid rgba(149, 166, 201, 0.12);
background: rgba(255,255,255,0.015);
}
.howto-rule-icon {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
flex: 0 0 auto;
font-size: 20px;
background: rgba(255,255,255,0.04);
}
.howto-rule-icon.blue { border: 1px solid rgba(56, 165, 255, 0.4); box-shadow: 0 0 0 4px rgba(56,165,255,0.08); }
.howto-rule-icon.purple { border: 1px solid rgba(173, 114, 255, 0.4); box-shadow: 0 0 0 4px rgba(173,114,255,0.08); }
.howto-rule-icon.red { border: 1px solid rgba(255, 90, 90, 0.4); box-shadow: 0 0 0 4px rgba(255,90,90,0.08); }
.howto-rule-icon.amber { border: 1px solid rgba(255, 200, 74, 0.4); box-shadow: 0 0 0 4px rgba(255,200,74,0.08); }
.howto-rule-icon.gold { border: 1px solid rgba(255, 194, 56, 0.4); box-shadow: 0 0 0 4px rgba(255,194,56,0.08); }
.howto-rule-icon.orange { border: 1px solid rgba(255, 142, 58, 0.4); box-shadow: 0 0 0 4px rgba(255,142,58,0.08); }
.howto-rule-copy {
min-width: 0;
}
.howto-rule-title {
color: #f4f7ff;
font-size: clamp(14px, 0.95vw, 16px);
font-weight: 800;
letter-spacing: -0.02em;
}
.howto-rule-text {
margin-top: 4px;
color: #c5d2ea;
font-size: clamp(10px, 0.75vw, 12px);
line-height: 1.22;
overflow-wrap: anywhere;
word-break: break-word;
}
.howto-chip {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.12);
font-size: 0.92em;
white-space: nowrap;
}
.howto-chip.danger { color: #ff7b7b; border-color: rgba(255, 94, 94, 0.3); background: rgba(255, 77, 77, 0.08); }
.howto-chip.warning { color: #f7c53d; border-color: rgba(247, 197, 61, 0.3); background: rgba(247, 197, 61, 0.08); }
.howto-chip.success { color: #42db7f; border-color: rgba(66, 219, 127, 0.3); background: rgba(66, 219, 127, 0.08); }
.howto-right {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.howto-panel-title {
color: #b573ff;
font-size: clamp(13px, 0.95vw, 16px);
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
margin: 2px 0 12px;
}
.howto-rewards-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
color: #dbeafe;
font-size: clamp(10px, 0.75vw, 12px);
line-height: 1.2;
overflow: hidden;
border-radius: 18px;
border: 1px solid rgba(149, 166, 201, 0.16);
}
.howto-rewards-table th,
.howto-rewards-table td {
padding: 9px 10px;
border-bottom: 1px solid rgba(149, 166, 201, 0.12);
border-right: 1px solid rgba(149, 166, 201, 0.12);
vertical-align: middle;
text-align: left;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
}
.howto-rewards-table th {
color: #f4f7ff;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 900;
}
.howto-rewards-table tbody tr:last-child td {
border-bottom: 0;
}
.howto-rewards-table th:last-child,
.howto-rewards-table td:last-child {
border-right: 0;
}
.howto-rewards-table td:last-child {
width: 40%;
}
.reward-good { color: #43db7f; }
.reward-bad { color: #ff6d6d; }
.reward-warn { color: #ffc94b; }
.howto-actions {
justify-content: flex-end;
gap: 0;
margin-top: clamp(12px, 1.2vw, 16px);
padding-top: 4px;
}
.howto-secondary-btn {
min-width: 0;
width: min(100%, 240px);
padding: 12px 18px;
border-radius: 16px;
font-size: clamp(13px, 0.95vw, 16px);
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1;
}
.howto-secondary-btn {
border: 1px solid rgba(85, 164, 255, 0.82);
background: rgba(15, 24, 47, 0.92);
color: #f4f7ff;
box-shadow: 0 0 0 1px rgba(85, 164, 255, 0.16) inset, 0 0 20px rgba(85, 164, 255, 0.14);
}
.dialog-leaderboard {
position: relative;
width: min(94vw, 920px);
max-width: none;
max-height: min(90dvh, 860px);
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
padding: clamp(20px, 2vw, 30px);
border-radius: 28px;
border: 1px solid rgba(245, 158, 11, 0.42);
background:
radial-gradient(circle at 18% 0%, rgba(245, 158, 11, 0.16), transparent 24%),
radial-gradient(circle at 82% 8%, rgba(16, 185, 129, 0.10), transparent 22%),
linear-gradient(180deg, rgba(6, 14, 35, 0.99), rgba(4, 10, 28, 0.99));
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.04) inset,
0 0 0 1px rgba(245, 158, 11, 0.16),
0 0 40px rgba(245, 158, 11, 0.12),
0 20px 80px rgba(2, 6, 23, 0.7);
}
.dialog-leaderboard::-webkit-scrollbar {
width: 10px;
}
.dialog-leaderboard::-webkit-scrollbar-track {
background: rgba(10, 18, 40, 0.55);
border-radius: 999px;
}
.dialog-leaderboard::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(245, 158, 11, 0.88), rgba(34, 197, 94, 0.82));
border-radius: 999px;
border: 2px solid rgba(4, 10, 28, 0.95);
}
.leaderboard-logo-wrap {
display: flex;
justify-content: center;
align-items: center;
margin: 2px 0 4px;
}
.leaderboard-logo {
width: clamp(26px, 2.6vw, 36px);
height: clamp(26px, 2.6vw, 36px);
object-fit: contain;
display: block;
filter: drop-shadow(0 0 12px rgba(245, 158, 11, 0.32));
}
.leaderboard-kicker {
color: #f59e0b;
font-size: clamp(15px, 1.2vw, 20px);
font-weight: 900;
letter-spacing: 0.18em;
text-transform: uppercase;
text-align: center;
}
.leaderboard-headline {
margin-top: 8px;
color: #f4f4f7;
font-size: clamp(18px, 1.95vw, 28px);
line-height: 1.08;
font-weight: 800;
letter-spacing: -0.04em;
text-align: center;
text-wrap: balance;
}
.leaderboard-my-score {
margin-top: 18px;
padding: 16px 18px;
border-radius: 20px;
border: 1px solid rgba(245, 158, 11, 0.18);
background: rgba(8, 15, 35, 0.58);
display: flex;
flex-direction: column;
gap: 10px;
}
.leaderboard-my-score-label {
color: #fcd34d;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.leaderboard-my-score-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.leaderboard-my-score-user {
color: #dbeafe;
font-size: 14px;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leaderboard-my-score-value {
color: #86efac;
font-size: clamp(22px, 2vw, 34px);
font-weight: 1000;
letter-spacing: -0.04em;
}
.leaderboard-list {
margin-top: 18px;
display: block;
}
.leaderboard-table-wrap {
width: 100%;
overflow: hidden;
border-radius: 18px;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(8, 15, 35, 0.58);
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.leaderboard-table thead th {
padding: 14px 16px;
text-align: left;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #fcd34d;
background: rgba(15, 23, 42, 0.76);
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
}
.leaderboard-table tbody td {
padding: 14px 16px;
border-top: 1px solid rgba(148, 163, 184, 0.08);
color: #e2e8f0;
font-size: 15px;
font-weight: 700;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leaderboard-table tbody tr:first-child td {
border-top: none;
}
.leaderboard-table tbody tr.current-user td {
background:
radial-gradient(circle at 18% 0%, rgba(34, 197, 94, 0.12), transparent 22%),
rgba(8, 15, 35, 0.72);
}
.leaderboard-table .leaderboard-rank-cell {
width: 96px;
color: #fbbf24;
font-weight: 1000;
}
.leaderboard-table .leaderboard-score-cell {
width: 120px;
color: #86efac;
font-weight: 1000;
text-align: right;
}
.leaderboard-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 18px;
border: 1px solid rgba(148, 163, 184, 0.14);
background: rgba(8, 15, 35, 0.58);
}
.leaderboard-row.current-user {
border-color: rgba(34, 197, 94, 0.36);
background:
radial-gradient(circle at 18% 0%, rgba(34, 197, 94, 0.12), transparent 22%),
rgba(8, 15, 35, 0.72);
}
.leaderboard-rank {
width: 56px;
height: 56px;
display: grid;
place-items: center;
border-radius: 16px;
background: rgba(245, 158, 11, 0.12);
color: #fbbf24;
font-size: 20px;
font-weight: 1000;
}
.leaderboard-name {
min-width: 0;
color: #f8fafc;
font-size: 15px;
font-weight: 800;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leaderboard-score {
color: #86efac;
font-size: 18px;
font-weight: 1000;
letter-spacing: -0.03em;
}
.leaderboard-empty {
padding: 18px 16px;
text-align: center;
color: #cbd5e1;
border: 1px dashed rgba(148, 163, 184, 0.22);
border-radius: 18px;
background: rgba(8, 15, 35, 0.45);
}
.dialog-challenge {
border-color: rgba(14, 165, 233, 0.34);
box-shadow: 0 24px 80px rgba(14, 165, 233, 0.32);
text-align: center;
}
.dialog-challenge.dialog-challenge-polished {
position: relative;
width: min(90vw, 430px);
padding: clamp(20px, 2.4vw, 28px) clamp(18px, 2.2vw, 24px) clamp(20px, 2.4vw, 24px);
border-radius: 28px;
border: 1px solid rgba(50, 215, 255, 0.84);
background:
radial-gradient(circle at 50% 0%, rgba(56, 189, 248, 0.18), transparent 22%),
radial-gradient(circle at 50% 100%, rgba(2, 132, 199, 0.12), transparent 34%),
linear-gradient(180deg, rgba(12, 18, 35, 0.94), rgba(8, 13, 28, 0.94));
box-shadow:
0 0 0 1px rgba(56, 189, 248, 0.06) inset,
0 0 0 1px rgba(255, 255, 255, 0.02),
0 20px 80px rgba(2, 6, 23, 0.64),
0 0 26px rgba(56, 189, 248, 0.12);
text-align: center;
overflow: hidden;
}
.dialog-challenge.dialog-challenge-polished::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(180deg, rgba(56, 189, 248, 0.52), rgba(59, 130, 246, 0.10), rgba(245, 158, 11, 0.22));
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.dialog-challenge.dialog-challenge-polished::after {
content: "";
position: absolute;
inset: 8px;
border-radius: 22px;
border: 1px solid rgba(255,255,255,0.03);
pointer-events: none;
}
.challenge-close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 34px;
height: 34px;
border: 1px solid rgba(255,255,255,0.10);
border-radius: 50%;
background: rgba(11, 17, 34, 0.62);
color: rgba(255,255,255,0.88);
font-size: 28px;
line-height: 1;
display: grid;
place-items: center;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
z-index: 2;
}
.challenge-close-btn:hover {
border-color: rgba(255,255,255,0.22);
background: rgba(16, 22, 40, 0.76);
}
.challenge-hero {
display: flex;
justify-content: center;
margin-top: 2px;
margin-bottom: 8px;
position: relative;
z-index: 1;
}
.challenge-hero-ring {
width: 72px;
height: 72px;
border-radius: 50%;
display: grid;
place-items: center;
background:
radial-gradient(circle at 50% 50%, rgba(255,255,255,0.10), transparent 54%),
linear-gradient(180deg, rgba(16, 185, 255, 0.26), rgba(59, 130, 246, 0.20));
border: 2px solid rgba(56, 189, 248, 0.96);
box-shadow:
0 0 0 5px rgba(56, 189, 248, 0.08),
0 0 28px rgba(56, 189, 248, 0.36);
}
.challenge-hero-icon {
width: 52px;
height: 52px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(24, 38, 71, 0.92);
font-size: 30px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.06),
0 0 0 1px rgba(255,255,255,0.04);
}
.challenge-pill {
width: fit-content;
margin: 0 auto 10px;
padding: 7px 12px;
border-radius: 999px;
border: 1px solid rgba(56, 189, 248, 0.34);
background: rgba(11, 27, 48, 0.76);
color: #1ed5ff;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.09em;
text-transform: uppercase;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
position: relative;
z-index: 1;
}
.challenge-title {
position: relative;
z-index: 1;
margin: 0;
font-size: clamp(24px, 5.6vw, 34px);
line-height: 1.04;
font-weight: 1000;
letter-spacing: -0.04em;
color: #f4f7fb;
text-shadow: 0 4px 18px rgba(2, 6, 23, 0.5);
}
.challenge-subtitle {
position: relative;
z-index: 1;
margin: 8px auto 0;
width: min(100%, 30ch);
color: #39aef8;
font-size: clamp(13px, 3.8vw, 16px);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.22;
}
.challenge-divider {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 12px 0 14px;
}
.challenge-divider span {
width: 34px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(56, 189, 248, 0.55), transparent);
}
.challenge-divider i {
width: 6px;
height: 6px;
border-radius: 50%;
background: #2fb5ff;
box-shadow: 0 0 12px rgba(47, 181, 255, 0.9);
}
.challenge-points {
position: relative;
z-index: 1;
display: grid;
gap: 10px;
width: 100%;
margin: 0 auto;
text-align: left;
}
.challenge-point {
display: flex;
align-items: center;
gap: 10px;
color: #e7eef8;
font-size: clamp(12px, 3.2vw, 14px);
line-height: 1.3;
}
.challenge-point-icon,
.challenge-note-icon {
width: 22px;
height: 22px;
border-radius: 50%;
display: grid;
place-items: center;
flex: 0 0 auto;
color: #38bdf8;
border: 1px solid rgba(56, 189, 248, 0.35);
background: rgba(11, 27, 48, 0.85);
font-weight: 900;
}
.challenge-rewards {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
margin: 14px auto 10px;
}
.challenge-reward {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
min-height: 38px;
border-radius: 999px;
font-size: clamp(11px, 3vw, 13px);
font-weight: 700;
letter-spacing: -0.01em;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
}
.challenge-reward.good {
color: #dfffe8;
border: 1px solid rgba(34, 197, 94, 0.34);
background: rgba(10, 42, 30, 0.74);
}
.challenge-reward.bad {
color: #ffd6d8;
border: 1px solid rgba(239, 68, 68, 0.34);
background: rgba(51, 15, 22, 0.74);
}
.challenge-reward-icon {
font-size: 16px;
line-height: 1;
}
.challenge-note {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 auto 16px;
color: #dce6f2;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1.3;
}
.challenge-actions {
justify-content: center;
margin-top: 0;
}
.challenge-start-btn {
appearance: none;
border: 1px solid rgba(63, 224, 255, 0.42);
background: linear-gradient(90deg, #16d8c8 0%, #2fb1ff 100%);
color: #f9fdff;
border-radius: 24px;
min-width: 100%;
padding: 14px 20px;
min-height: 52px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 14px;
font-size: clamp(16px, 4vw, 20px);
font-weight: 900;
letter-spacing: -0.02em;
cursor: pointer;
box-shadow:
0 0 0 1px rgba(255,255,255,0.04) inset,
0 0 18px rgba(16, 216, 200, 0.28),
0 16px 34px rgba(3, 105, 161, 0.32);
position: relative;
z-index: 1;
}
.challenge-start-btn:hover {
transform: translateY(-1px);
box-shadow:
0 0 0 1px rgba(255,255,255,0.04) inset,
0 0 26px rgba(16, 216, 200, 0.36),
0 20px 42px rgba(3, 105, 161, 0.38);
}
.challenge-start-icon {
font-size: 0.96em;
}
}
.dialog-title {
font-size: 22px;
font-weight: 900;
letter-spacing: -0.02em;
color: #f8fafc;
}
.dialog-challenge .dialog-title {
color: #ef4444;
font-size: 24px;
text-align: center;
}
.dialog-text {
margin-top: 10px;
color: #cbd5e1;
font-size: 15px;
line-height: 1.5;
}
.dialog-challenge .dialog-text {
text-align: center;
margin-top: 12px;
color: #dbeafe;
font-size: 16px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 18px;
flex-wrap: wrap;
}
.dialog-button {
padding: 12px 18px;
border-radius: 999px;
border: none;
font-size: 13px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
cursor: pointer;
}
.dialog-secondary {
background: rgba(51, 65, 85, 0.9);
color: #e2e8f0;
}
.dialog-primary {
background: linear-gradient(135deg, #fb7185 0%, #f97316 100%);
color: white;
box-shadow: 0 12px 30px rgba(249, 115, 22, 0.28);
}
.dialog-gameover .dialog-primary {
background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);
box-shadow: 0 12px 30px rgba(14, 165, 233, 0.28);
}
.dialog-challenge .dialog-primary {
background: linear-gradient(135deg, #22c55e 0%, #0ea5e9 100%);
box-shadow: 0 12px 30px rgba(34, 197, 94, 0.24);
}
.dialog-button:hover {
transform: translateY(-1px);
}
.transition-card {
padding: 24px 28px;
border-radius: 22px;
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(15, 23, 42, 0.88);
box-shadow: 0 20px 60px rgba(2, 6, 23, 0.5);
text-align: center;
min-width: min(320px, calc(100% - 48px));
}
.transition-title {
font-size: 26px;
font-weight: 900;
letter-spacing: -0.02em;
color: #f8fafc;
}
.transition-subtitle {
margin-top: 8px;
color: #cbd5e1;
font-size: 15px;
line-height: 1.45;
}
.preview-content {
text-align: center;
animation: previewFadeIn 0.35s ease;
}
@keyframes previewFadeIn {
from { opacity: 0; transform: scale(0.94); }
to { opacity: 1; transform: scale(1); }
}
.preview-countdown {
font-size: clamp(72px, 12vw, 132px);
font-weight: 900;
line-height: 1;
background: white;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: countPulse 1s ease infinite;
}
@keyframes countPulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.12); opacity: 0.86; }
100% { transform: scale(1); opacity: 1; }
}
.preview-subtitle {
margin-top: 10px;
text-transform: uppercase;
letter-spacing: 0.24em;
font-size: 13px;
font-weight: 700;
color: #cbd5e1;
}
.game-header {
width: 100%;
display: flex;
flex-direction: column;
gap: clamp(12px, 1.4vw, 16px);
z-index: 1;
}
.game-header-row {
width: 100%;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: clamp(12px, 1.4vw, 16px);
}
.game-stats {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: clamp(8px, 1vw, 14px);
align-items: stretch;
min-width: 0;
}
.stat-block {
display: grid;
grid-template-columns: 44px 1fr;
align-items: center;
gap: 6px 10px;
min-width: 0;
padding: clamp(12px, 1.2vw, 16px) clamp(12px, 1.3vw, 18px);
border-radius: clamp(14px, 1.5vw, 18px);
border: 1px solid rgba(148,163,184,0.18);
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(10px);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
box-sizing: border-box;
}
.stat-icon {
grid-row: span 2;
width: clamp(34px, 3vw, 44px);
height: clamp(34px, 3vw, 44px);
border-radius: clamp(11px, 1.2vw, 14px);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: clamp(16px, 1.6vw, 20px);
background: rgba(255,255,255,0.08);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}
.stat-label {
font-size: clamp(9px, 0.85vw, 11px);
font-weight: 800;
color: #7c8599;
letter-spacing: 0.18em;
text-transform: uppercase;
line-height: 1;
}
.stat-value {
font-size: clamp(16px, 1.5vw, 20px);
font-weight: 900;
color: #f8fafc;
margin-top: -1px;
line-height: 1.1;
grid-column: 2;
}
.stat-level { border-color: rgba(34,197,94,0.28); box-shadow: 0 0 0 1px rgba(34,197,94,0.08) inset; }
.stat-level .stat-icon { background: linear-gradient(135deg, rgba(74,222,128,0.18), rgba(34,197,94,0.16)); color: #86efac; }
.stat-moves { border-color: rgba(59,130,246,0.24); }
.stat-moves .stat-icon { background: linear-gradient(135deg, rgba(96,165,250,0.18), rgba(14,165,233,0.16)); color: #7dd3fc; }
.stat-matches { border-color: rgba(168,85,247,0.24); }
.stat-matches .stat-icon { background: linear-gradient(135deg, rgba(192,132,252,0.18), rgba(139,92,246,0.16)); color: #c4b5fd; }
.stat-score { border-color: rgba(245,158,11,0.24); }
.stat-score .stat-icon { background: linear-gradient(135deg, rgba(251,191,36,0.18), rgba(245,158,11,0.16)); color: #fbbf24; }
.stat-lives { border-color: rgba(248,113,113,0.24); }
.stat-lives .stat-icon { background: linear-gradient(135deg, rgba(248,113,113,0.18), rgba(239,68,68,0.16)); color: #fca5a5; }
.game-state-badge {
justify-self: end;
align-self: center;
padding: 8px 14px 9px;
border-radius: 999px;
border: 1px solid rgba(251, 191, 36, 0.35);
background: rgba(251, 191, 36, 0.12);
color: #fbbf24;
font-size: clamp(11px, 0.9vw, 12px);
font-weight: 900;
letter-spacing: 0.16em;
text-transform: uppercase;
white-space: nowrap;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1;
gap: 4px;
min-width: 92px;
}
.game-state-badge .badge-main {
font-size: 0.95em;
line-height: 1;
}
.game-state-badge .badge-sub {
font-size: 0.8em;
letter-spacing: 0.08em;
line-height: 1;
color: #fff7cc;
text-shadow: 0 0 8px rgba(253, 230, 138, 0.18);
}
.game-state-badge.playing {
border-color: rgba(52, 211, 153, 0.35);
background: rgba(52, 211, 153, 0.12);
color: #34d399;
}
.game-state-badge.complete {
border-color: rgba(250, 204, 21, 0.38);
background: rgba(250, 204, 21, 0.16);
color: #6CF527;
text-shadow: 0 0 10px rgba(253, 230, 138, 0.24);
}
.game-state-badge.gameover {
border-color: rgba(248, 113, 113, 0.35);
background: rgba(248, 113, 113, 0.14);
color: #f87171;
}
.level-meta {
width: 100%;
display: flex;
flex-direction: column;
gap: clamp(8px, 1vw, 10px);
z-index: 1;
}
.level-title {
font-size: clamp(26px, 3vw, 38px);
font-weight: 900;
letter-spacing: -0.03em;
color: #fff;
}
.level-personality {
color: #f8fafc;
opacity: 0.9;
font-size: clamp(13px, 1.1vw, 15px);
font-weight: 700;
letter-spacing: 0.02em;
}
.level-chips {
display: flex;
flex-wrap: nowrap;
gap: 10px;
min-width: 0;
}
.theme-chip {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 7px 12px;
font-size: clamp(10px, 0.85vw, 12px);
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
min-width: 0;
}
.theme-chip {
background: rgba(14, 165, 233, 0.15);
color: #7dd3fc;
border: 1px solid rgba(14, 165, 233, 0.26);
}
.level-focus {
color: #cbd5e1;
font-size: clamp(13px, 1.1vw, 15px);
line-height: 1.5;
max-width: 72ch;
}
.memory-grid {
--grid-gap: clamp(8px, 1vw, 16px);
display: grid;
grid-template-columns: repeat(var(--grid-cols, 2), minmax(0, var(--card-fit-size, var(--card-size, 150px))));
gap: var(--grid-gap);
width: fit-content;
max-width: 100%;
z-index: 1;
margin-top: 0;
justify-content: center;
align-content: start;
}
.game-layout {
width: 100%;
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: clamp(14px, 1.8vw, 24px);
align-items: start;
overflow: visible;
}
.level-panel {
display: flex;
justify-content: center;
}
.challenge-panel {
display: none;
justify-content: center;
align-items: stretch;
width: 100%;
}
.memory-container.challenge-mode .challenge-panel {
display: flex;
}
.challenge-card {
width: min(100%, 920px);
padding: clamp(18px, 2vw, 28px);
border-radius: 24px;
border: 1px solid rgba(96, 165, 250, 0.26);
background:
radial-gradient(circle at 20% 10%, rgba(14, 165, 233, 0.14), transparent 34%),
linear-gradient(180deg, rgba(15, 23, 42, 0.88), rgba(2, 6, 23, 0.92));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 24px 70px rgba(2, 6, 23, 0.48);
display: flex;
flex-direction: column;
gap: 14px;
}
.challenge-label {
font-size: 12px;
font-weight: 900;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #7dd3fc;
}
.challenge-theme {
color: #f8fafc;
font-size: clamp(14px, 1.25vw, 18px);
font-weight: 800;
letter-spacing: 0.02em;
}
.challenge-question {
color: #f8fafc;
font-size: clamp(22px, 2.3vw, 34px);
line-height: 1.25;
font-weight: 900;
letter-spacing: -0.03em;
}
.challenge-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.challenge-option {
appearance: none;
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(15, 23, 42, 0.78);
color: #f8fafc;
border-radius: 18px;
padding: 16px 18px;
min-height: 70px;
font-size: clamp(16px, 1.4vw, 20px);
font-weight: 800;
letter-spacing: 0.01em;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.challenge-option:hover:not(:disabled) {
transform: translateY(-1px);
border-color: rgba(125, 211, 252, 0.45);
}
.challenge-option:disabled {
cursor: default;
opacity: 0.72;
}
.challenge-option.correct-choice {
border-color: rgba(34, 197, 94, 0.75);
background: linear-gradient(135deg, rgba(22, 163, 74, 0.24), rgba(34, 197, 94, 0.34));
box-shadow: 0 0 0 1px rgba(34, 197, 94, 0.16) inset, 0 0 26px rgba(34, 197, 94, 0.28);
}
.challenge-option.wrong-choice {
border-color: rgba(248, 113, 113, 0.75);
background: linear-gradient(135deg, rgba(185, 28, 28, 0.22), rgba(248, 113, 113, 0.34));
box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.16) inset, 0 0 26px rgba(248, 113, 113, 0.22);
}
.challenge-option.reveal-correct {
border-color: rgba(250, 204, 21, 0.7);
background: linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(245, 158, 11, 0.28));
box-shadow: 0 0 0 1px rgba(250, 204, 21, 0.14) inset, 0 0 22px rgba(250, 204, 21, 0.24);
}
.challenge-focus {
color: #cbd5e1;
font-size: 14px;
line-height: 1.45;
}
.game-state-badge.challenge {
border-color: rgba(14, 165, 233, 0.35);
background: rgba(14, 165, 233, 0.12);
color: #7dd3fc;
}
.cards-panel {
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 0;
position: relative;
z-index: 1;
width: 100%;
}
.memory-card {
width: var(--card-fit-size, var(--card-size, 150px));
aspect-ratio: 1 / 1;
height: auto;
perspective: 1200px;
cursor: pointer;
user-select: none;
transform-origin: center center;
}
.memory-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1);
transform-style: preserve-3d;
}
.memory-card.flipped .memory-inner {
transform: rotateY(180deg);
}
.memory-front,
.memory-back {
position: absolute;
inset: 0;
border-radius: clamp(18px, 1.8vw, 24px);
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: clamp(32px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.34), 64px);
box-shadow: 0 18px 30px rgba(2, 6, 23, 0.32);
}
.memory-front {
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.35), transparent 24%),
linear-gradient(135deg, #334155 0%, #6366f1 55%, #8b5cf6 100%);
color: white;
border: 1px solid rgba(255,255,255,0.1);
overflow: hidden;
}
.memory-container.challenge-mode .memory-front {
background:
radial-gradient(circle at 20% 20%, rgba(255, 237, 213, 0.38), transparent 24%),
radial-gradient(circle at 80% 18%, rgba(255, 107, 53, 0.26), transparent 26%),
linear-gradient(135deg, #7f1d1d 0%, #dc2626 52%, #fb923c 100%);
border-color: rgba(251, 146, 60, 0.24);
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.08),
0 0 0 1px rgba(251, 146, 60, 0.08),
0 18px 30px rgba(69, 10, 10, 0.36);
}
.memory-front-logo {
width: clamp(34px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.38), 76px);
height: auto;
object-fit: contain;
display: block;
filter: drop-shadow(0 0 12px rgba(255, 255, 255, 0.22));
}
.memory-back {
background:
radial-gradient(circle at 25% 20%, rgba(255,255,255,0.38), transparent 22%),
linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
transform: rotateY(180deg);
color: #0f172a;
}
.memory-card.text-card .memory-back {
color: #020617;
font-size: clamp(12px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.19), 28px);
font-weight: 800;
line-height: 1.12;
letter-spacing: -0.02em;
text-align: center;
padding: clamp(8px, 1vw, 14px);
box-sizing: border-box;
overflow-wrap: anywhere;
word-break: break-word;
hyphens: auto;
}
.memory-card:hover:not(.preview-locked):not(.matched) .memory-front {
transform: scale(1.025);
}
.memory-card.matched {
cursor: default;
pointer-events: none;
}
.memory-card.matched .memory-back {
background:
radial-gradient(circle at 25% 20%, rgba(255,255,255,0.42), transparent 22%),
linear-gradient(135deg, #bbf7d0 0%, #86efac 100%);
animation: matchGlow 0.8s ease;
}
@keyframes matchGlow {
0% { box-shadow: 0 0 0 0 rgba(52,211,153,0.4); }
60% { box-shadow: 0 0 0 14px rgba(52,211,153,0); }
100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); }
}
.memory-card.wrong .memory-back {
background:
radial-gradient(circle at 25% 20%, rgba(255,255,255,0.24), transparent 24%),
linear-gradient(135deg, #fca5a5 0%, #fb7185 100%);
animation: shake 0.45s ease;
}
@keyframes shake {
0%, 100% { transform: rotateY(180deg) translateX(0); }
20% { transform: rotateY(180deg) translateX(-7px); }
40% { transform: rotateY(180deg) translateX(7px); }
60% { transform: rotateY(180deg) translateX(-4px); }
80% { transform: rotateY(180deg) translateX(4px); }
}
.memory-card.preview-locked {
cursor: default;
pointer-events: none;
}
.memory-card.hint-peek .memory-back {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
box-shadow: 0 0 0 8px rgba(250, 204, 21, 0.2);
}
.game-status {
width: 100%;
margin-top: 0;
padding: clamp(14px, 1.4vw, 18px) clamp(16px, 1.8vw, 22px);
background: rgba(15, 23, 42, 0.55);
border-radius: clamp(16px, 1.5vw, 18px);
border: 1px solid rgba(148, 163, 184, 0.14);
color: white;
backdrop-filter: blur(8px);
z-index: 1;
box-sizing: border-box;
}
.game-status.show {
border-color: rgba(250, 204, 21, 0.34);
background: rgba(113, 63, 18, 0.45);
}
.game-status.error {
border-color: rgba(248, 113, 113, 0.34);
background: rgba(127, 29, 29, 0.45);
}
.game-status.win {
border-color: rgba(52, 211, 153, 0.34);
background: rgba(6, 78, 59, 0.45);
}
.status-text {
font-size: clamp(15px, 1.3vw, 18px);
font-weight: 800;
}
.status-subtext {
margin-top: 6px;
color: #cbd5e1;
font-size: clamp(12px, 1vw, 14px);
line-height: 1.45;
}
.performance-meter-shell {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
padding: 18px 18px 20px;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.10);
background: rgba(7, 11, 24, 0.36);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.03),
0 20px 40px rgba(2, 6, 23, 0.22);
box-sizing: border-box;
}
.performance-meter-title {
text-align: center;
font-size: 14px;
font-weight: 900;
letter-spacing: 0.20em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.58);
line-height: 1;
}
.meter-card {
width: 100%;
padding: 18px 24px;
border-radius: 999px;
background: rgba(8, 12, 23, 0.78);
border: 1px solid rgba(255,255,255,0.06);
box-shadow:
0 18px 45px rgba(0, 0, 0, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 20px;
box-sizing: border-box;
min-width: 0;
}
.side-label {
display: flex;
align-items: center;
gap: 10px;
font-size: clamp(15px, 1.65vw, 22px);
font-weight: 900;
white-space: nowrap;
flex: 0 0 auto;
min-width: max-content;
}
.easy {
color: #4ced7f;
}
.challenge {
color: #ff684f;
}
.icon-box {
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: 16px;
font-size: 24px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.16),
0 0 0 1px rgba(255,255,255,0.05);
}
.easy .icon-box {
background: rgba(34,197,94,0.14);
}
.challenge .icon-box {
background: rgba(239,68,68,0.14);
}
.meter {
--meter-value: 0%;
--meter-side-padding: 18px;
--meter-knob-size: 42px;
--knob-color: #17a34a;
--knob-glow: rgba(23, 163, 74, 0.65);
position: relative;
width: 100%;
min-width: 0;
height: 28px;
padding-inline: var(--meter-side-padding);
box-sizing: border-box;
border-radius: 999px;
background: linear-gradient(
90deg,
#17a34a 0%,
#97c51e 28%,
#ffd522 50%,
#ff981f 72%,
#ff2e25 100%
);
overflow: visible;
box-shadow:
inset 0 3px 8px rgba(0, 0, 0, 0.26),
0 0 0 1px rgba(255,255,255,0.08),
0 0 24px rgba(255, 147, 31, 0.18);
}
.meter::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
180deg,
rgba(255,255,255,0.38),
rgba(255,255,255,0.02)
);
pointer-events: none;
}
.ticks {
position: absolute;
inset: 0 var(--meter-side-padding);
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
}
.ticks span {
width: 3px;
height: 12px;
border-radius: 99px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 1px 3px rgba(0,0,0,0.18);
}
.meter-fill-spark {
position: absolute;
top: 0;
left: 0;
width: var(--meter-value);
height: 100%;
border-radius: inherit;
background: linear-gradient(
90deg,
rgba(255,255,255,0.04),
rgba(255,255,255,0.35),
rgba(255,255,255,0.06)
);
transition: width 720ms cubic-bezier(.2, .9, .2, 1.15);
pointer-events: none;
}
.knob {
position: absolute;
top: 50%;
left: clamp(
var(--meter-side-padding),
calc(var(--meter-side-padding) + var(--meter-value)),
calc(100% - var(--meter-side-padding))
);
width: var(--meter-knob-size);
height: var(--meter-knob-size);
transform: translate(-50%, -50%);
border-radius: 50%;
z-index: 5;
display: grid;
place-items: center;
transition:
left 720ms cubic-bezier(.2, .9, .2, 1.15),
transform 180ms ease;
filter:
drop-shadow(0 8px 14px rgba(0, 0, 0, 0.24))
drop-shadow(0 0 18px var(--knob-glow));
}
.knob::before {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
padding: 3px;
background:
conic-gradient(
from 120deg,
rgba(255,255,255,0.95),
var(--knob-color),
rgba(255,255,255,0.95),
var(--knob-color),
rgba(255,255,255,0.95)
);
box-shadow:
0 0 10px var(--knob-glow),
0 0 20px var(--knob-glow),
inset 0 0 8px rgba(255,255,255,0.9);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: neonSpin 1.4s linear infinite;
}
@keyframes neonSpin {
from {
rotate: 0deg;
}
to {
rotate: 360deg;
}
}
.knob-core {
position: relative;
width: calc(var(--meter-knob-size) * 0.76);
height: calc(var(--meter-knob-size) * 0.76);
border-radius: 50%;
background: linear-gradient(180deg, #ffffff, #f3f4f6);
box-shadow:
inset 0 3px 7px rgba(255, 255, 255, 1),
inset 0 -5px 10px rgba(0, 0, 0, 0.08);
display: grid;
place-items: center;
}
.knob-core::after {
content: "";
width: calc(var(--meter-knob-size) * 0.07);
height: calc(var(--meter-knob-size) * 0.30);
border-radius: 99px;
background: var(--knob-color);
box-shadow:
0 0 10px var(--knob-glow),
0 0 18px var(--knob-glow);
transition:
background 720ms ease,
box-shadow 720ms ease;
}
.knob.bump {
animation: knobBump 420ms ease;
}
@keyframes knobBump {
0% {
transform: translate(-50%, -50%) scale(1);
}
35% {
transform: translate(-50%, -50%) scale(1.14);
}
65% {
transform: translate(-50%, -50%) scale(0.96);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
.meter.flash {
animation: meterFlash 520ms ease;
}
@keyframes meterFlash {
0% {
filter: brightness(1);
}
40% {
filter: brightness(1.25) saturate(1.28);
}
100% {
filter: brightness(1);
}
}
.controls-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
justify-content: center;
gap: clamp(10px, 1.2vw, 14px);
z-index: 1;
margin-top: 0;
width: min(100%, 760px);
margin-inline: auto;
box-sizing: border-box;
}
.memory-container:not(.game-playing) .controls-row { display: none !important; }
.memory-container.game-playing .controls-row { display: grid !important; }
.reset-button,
.hint-button,
.next-level-button {
width: 100%;
min-width: 0;
padding: clamp(10px, 1vw, 12px) clamp(14px, 1.4vw, 20px);
font-size: clamp(11px, 0.9vw, 13px);
font-weight: 900;
color: white;
border: none;
border-radius: 999px;
cursor: pointer;
transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
letter-spacing: 0.08em;
text-transform: uppercase;
}
#controls-row .reset-button,
#controls-row .reset-button:disabled {
background: linear-gradient(135deg, #ef4444 0%, #f97316 100%) !important;
box-shadow: 0 12px 30px rgba(239,68,68,0.32) !important;
color: #ffffff !important;
}
#controls-row .hint-button,
#controls-row .hint-button:disabled {
background: linear-gradient(135deg, #fde68a 0%, #facc15 100%) !important;
box-shadow: 0 10px 30px rgba(250, 204, 21, 0.25) !important;
color: #1e293b !important;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
line-height: 1;
}
.hint-prefix,
.hint-count {
display: inline-block;
}
.hint-count {
font-weight: 1000;
letter-spacing: 0.04em;
}
.hint-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 14px 36px rgba(250, 204, 21, 0.35);
}
#controls-row .next-level-button,
#controls-row .next-level-button:disabled {
background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%) !important;
box-shadow: 0 12px 30px rgba(14,165,233,0.28) !important;
color: #f5f8ff !important;
}
.reset-button:hover:not(:disabled),
.next-level-button:hover:not(:disabled) {
transform: translateY(-2px);
}
.reset-button:disabled,
.hint-button:disabled,
.next-level-button:disabled {
opacity: 0.9;
cursor: not-allowed;
filter: saturate(0.95) brightness(0.96);
}
.hud-glow-peek,
.hud-glow-lives,
.hud-glow-lives-gain {
animation: hudPulseGlow 0.8s ease-out;
}
.hud-glow-peek {
text-shadow:
0 0 0 rgba(134, 239, 172, 0),
0 0 10px rgba(34, 197, 94, 0.65),
0 0 18px rgba(134, 239, 172, 0.75),
0 0 28px rgba(34, 197, 94, 0.45);
box-shadow:
0 0 0 1px rgba(34, 197, 94, 0.15) inset,
0 0 18px rgba(34, 197, 94, 0.24),
0 0 30px rgba(134, 239, 172, 0.14);
}
.hud-glow-lives {
text-shadow:
0 0 0 rgba(248, 113, 113, 0),
0 0 10px rgba(248, 113, 113, 0.68),
0 0 18px rgba(252, 165, 165, 0.75),
0 0 28px rgba(248, 113, 113, 0.46);
box-shadow:
0 0 0 1px rgba(248, 113, 113, 0.15) inset,
0 0 18px rgba(248, 113, 113, 0.24),
0 0 30px rgba(252, 165, 165, 0.14);
}
.hud-glow-lives-gain {
text-shadow:
0 0 0 rgba(74, 222, 128, 0),
0 0 10px rgba(34, 197, 94, 0.68),
0 0 18px rgba(134, 239, 172, 0.78),
0 0 28px rgba(74, 222, 128, 0.46);
box-shadow:
0 0 0 1px rgba(34, 197, 94, 0.15) inset,
0 0 18px rgba(34, 197, 94, 0.24),
0 0 30px rgba(134, 239, 172, 0.14);
}
@keyframes hudPulseGlow {
0% {
filter: brightness(1);
transform: translateY(0) scale(1);
}
35% {
filter: brightness(1.18);
transform: translateY(-1px) scale(1.015);
}
100% {
filter: brightness(1);
transform: translateY(0) scale(1);
}
}
@media (max-width: 900px) {
.board-frame {
padding: 14px;
gap: 14px;
--board-frame-padding: 14px;
}
.performance-meter-shell {
padding: 12px 12px 14px;
gap: 8px;
}
.performance-meter-title {
font-size: 11px;
}
.meter-card {
padding: 15px 18px;
gap: 14px;
}
.game-stats {
gap: 8px;
}
.dialog-gameover {
width: min(88vw, 660px);
min-height: min(50dvh, 500px);
padding: clamp(20px, 2.2vw, 30px) clamp(18px, 2.4vw, 34px);
border-radius: 28px;
gap: 12px;
}
.gameover-icon-wrap {
width: 88px;
height: 88px;
}
.gameover-icon {
font-size: 42px;
}
.gameover-title {
font-size: clamp(38px, 5vw, 60px);
}
.gameover-subtitle {
font-size: clamp(15px, 1.7vw, 22px);
}
.gameover-score-pill {
padding: 13px 16px;
gap: 12px;
}
.gameover-score-icon {
font-size: 30px;
}
.gameover-score-label {
font-size: 18px;
}
.gameover-score-value {
font-size: 40px;
}
.dialog-challenge.dialog-challenge-polished {
width: min(94vw, 560px);
padding: 24px 18px 22px;
border-radius: 28px;
}
.dialog-howto {
width: min(98vw, 1200px);
max-height: min(94dvh, 900px);
padding: 18px;
overflow-y: auto;
overscroll-behavior: contain;
}
.howto-close-btn {
width: 50px;
height: 50px;
font-size: 34px;
top: 12px;
right: 12px;
}
.howto-logo {
width: 30px;
height: 30px;
}
.howto-headline {
font-size: clamp(17px, 3.4vw, 24px);
}
.howto-steps {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
gap: 8px;
}
.howto-step-arrow {
display: grid;
font-size: clamp(20px, 3vw, 28px);
}
.howto-main {
grid-template-columns: 1fr;
gap: 10px;
}
.howto-step-card {
min-height: 96px;
padding: 9px 10px;
gap: 8px;
flex-direction: column;
justify-content: flex-start;
align-items: center;
text-align: center;
}
.howto-step-icon {
width: 48px;
height: 48px;
font-size: 24px;
}
.howto-step-title {
font-size: 12px;
}
.howto-step-text {
font-size: 9.5px;
line-height: 1.16;
text-align: center;
max-width: 12ch;
}
.howto-step-copy {
display: flex;
flex-direction: column;
align-items: center;
}
.howto-left,
.howto-right {
padding: 9px;
border-radius: 16px;
}
.howto-rule {
padding: 7px 8px;
gap: 7px;
border-radius: 12px;
}
.howto-rule-icon {
width: 32px;
height: 32px;
font-size: 16px;
}
.howto-rule-title {
font-size: 10px;
}
.howto-rule-text {
font-size: 9px;
line-height: 1.16;
}
.howto-panel-title {
font-size: 11px;
margin: 1px 0 8px;
}
.howto-rewards-table {
font-size: 9px;
line-height: 1.16;
table-layout: fixed;
width: 100%;
}
.howto-rewards-table th,
.howto-rewards-table td {
padding: 6px 6px;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
}
.howto-rewards-table th {
font-size: 7px;
letter-spacing: 0.08em;
}
.howto-rewards-table td:last-child {
width: 38%;
}
.howto-secondary-btn {
width: min(100%, 220px);
padding: 12px 16px;
font-size: 13px;
border-radius: 14px;
}
.dialog-leaderboard {
width: min(98vw, 1200px);
max-height: min(94dvh, 900px);
padding: 18px;
overflow-y: auto;
overscroll-behavior: contain;
}
.leaderboard-row {
grid-template-columns: 58px minmax(0, 1fr) auto;
padding: 10px 12px;
gap: 10px;
}
.leaderboard-rank {
width: 46px;
height: 46px;
border-radius: 14px;
font-size: 16px;
}
.leaderboard-name {
font-size: 13px;
}
.leaderboard-score {
font-size: 16px;
}
.challenge-close-btn {
top: 14px;
right: 14px;
width: 38px;
height: 38px;
font-size: 24px;
}
.challenge-hero-ring {
width: 82px;
height: 82px;
}
.challenge-hero-icon {
width: 60px;
height: 60px;
font-size: 34px;
}
.challenge-pill {
margin-bottom: 12px;
padding: 8px 14px;
font-size: 12px;
}
.challenge-title {
font-size: clamp(28px, 5.5vw, 40px);
}
.challenge-subtitle {
font-size: clamp(15px, 3.4vw, 20px);
max-width: 34ch;
}
.challenge-divider {
margin: 14px 0 16px;
}
.challenge-points {
gap: 10px;
width: min(100%, 360px);
}
.challenge-point {
gap: 10px;
font-size: 14px;
}
.challenge-point-icon,
.challenge-note-icon {
width: 26px;
height: 26px;
}
.challenge-rewards {
gap: 10px;
margin: 18px auto 14px;
}
.challenge-reward {
min-height: 48px;
padding: 10px 14px;
font-size: 14px;
}
.challenge-note {
margin-bottom: 20px;
font-size: 13px;
gap: 10px;
}
.challenge-start-btn {
min-width: min(100%, 440px);
min-height: 64px;
padding: 16px 22px;
font-size: clamp(18px, 4.4vw, 24px);
border-radius: 20px;
}
}
@media (max-width: 820px) {
.memory-container {
padding: 8px;
padding-bottom: calc(20px + env(safe-area-inset-bottom));
gap: 8px;
border-radius: 16px;
}
.memory-container:not(.game-playing) .board-frame {
display: none !important;
}
.game-brand {
width: 100%;
justify-content: center;
gap: 10px;
padding-top: 2px;
}
.game-brand-mark {
width: 48px;
height: 48px;
border-radius: 14px;
}
.game-brand-title {
font-size: clamp(22px, 6vw, 30px);
}
.game-brand-subtitle {
font-size: 12px;
line-height: 1.35;
max-width: 44ch;
}
.board-frame {
padding: 12px;
padding-bottom: 18px;
border-radius: 18px;
gap: 12px;
--board-frame-padding: 12px;
}
.performance-meter-shell {
padding: 10px 10px 12px;
gap: 8px;
}
.performance-meter-title {
font-size: 9px;
letter-spacing: 0.16em;
}
#status-layout,
.game-layout,
.level-panel,
.cards-panel {
flex: 0 0 auto;
}
.game-header-row {
grid-template-columns: 1fr;
gap: 6px;
}
.game-stats {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 4px;
}
.meter-card {
padding: 11px 12px;
gap: 10px;
border-radius: 28px;
}
.meter {
--meter-side-padding: 10px;
--meter-knob-size: 32px;
height: 22px;
}
.ticks span {
height: 9px;
}
.side-label {
font-size: 8px;
gap: 5px;
}
.icon-box {
width: 16px;
height: 16px;
border-radius: 6px;
font-size: 9px;
}
.game-state-badge {
display: inline-flex;
grid-column: 1;
justify-self: center;
align-self: start;
margin-top: 2px;
padding: 6px 10px;
min-width: 0;
width: fit-content;
max-width: 100%;
flex-direction: row;
gap: 6px;
white-space: nowrap;
letter-spacing: 0.12em;
font-size: 10px;
}
.stat-block {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
justify-items: center;
align-items: center;
text-align: center;
gap: 1px;
padding: 7px 4px 6px;
}
.stat-icon {
width: 22px;
height: 22px;
font-size: 11px;
grid-row: 1;
grid-column: 1;
justify-self: center;
}
.stat-label {
grid-column: 1;
grid-row: 2;
align-self: center;
letter-spacing: 0.08em;
font-size: 6px;
line-height: 1;
}
.stat-value {
grid-column: 1;
grid-row: 3;
align-self: center;
margin-top: 0;
font-size: 10px;
line-height: 1;
}
.memory-grid {
--grid-gap: 10px;
width: 100%;
}
.game-layout {
gap: 12px;
}
.start-overlay {
position: relative;
inset: auto;
width: 100%;
height: auto;
min-height: auto;
display: flex;
align-items: flex-start;
justify-content: center;
overflow: visible;
}
.level-panel {
justify-content: stretch;
}
.level-chips {
flex-wrap: wrap;
}
.start-card {
width: min(100%, calc(100vw - 24px));
padding: 20px 18px 18px;
border-radius: 22px;
}
.start-key-row {
margin-top: 12px;
}
.top-utility-bar {
gap: 8px;
}
.top-utility-bar .utility-button,
.top-utility-bar .music-toggle-button {
padding: 9px 12px;
font-size: 12px;
border-radius: 16px;
}
.top-utility-bar .utility-button {
min-width: 92px;
}
.top-utility-bar .music-toggle-button {
min-width: 150px;
}
.top-utility-bar .high-score-pill {
min-width: 160px;
flex: 1 1 160px;
}
.start-utility-row {
width: min(100%, 680px);
gap: 8px;
}
.start-utility-row .start-help-button,
.start-utility-row .music-toggle-button {
min-width: 0;
flex: 1 1 0;
padding: 14px 16px;
font-size: 14px;
}
.start-utility-row .leaderboard-button,
.start-utility-row .high-score-pill {
min-width: 0;
flex: 1 1 0;
padding: 14px 16px;
font-size: 14px;
}
.start-utility-row .start-help-icon,
.start-utility-row .music-toggle-icon {
width: 28px;
height: 28px;
font-size: 16px;
}
.start-utility-row .leaderboard-icon,
.start-utility-row .high-score-icon {
width: 28px;
height: 28px;
font-size: 16px;
}
.start-utility-row .high-score-copy {
gap: 2px;
}
.start-utility-row .high-score-label {
font-size: 8px;
letter-spacing: 0.16em;
}
.start-utility-row .high-score-user {
font-size: 15px;
font-weight: 1000;
max-width: 12ch;
}
.start-utility-row .high-score-value {
font-size: 22px;
}
.cards-panel {
width: 100%;
}
.controls-row {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-top: 2px;
width: min(100%, 560px);
margin-inline: auto;
}
.next-level-button {
grid-column: auto;
justify-self: stretch;
width: 100%;
}
.reset-button,
.hint-button,
.next-level-button {
padding: 10px 10px;
font-size: 10px;
letter-spacing: 0.06em;
}
.hint-button {
gap: 6px;
}
.level-title {
font-size: clamp(22px, 6vw, 30px);
}
.level-personality {
font-size: 13px;
}
.level-focus {
font-size: 14px;
}
.status-text {
font-size: 15px;
}
.status-subtext {
font-size: 12px;
}
.memory-front,
.memory-back {
border-radius: 18px;
}
.start-stage {
width: 100%;
height: auto;
min-height: 0;
padding: 18px 18px 24px;
transform: none;
align-items: flex-start;
}
.start-hero {
width: min(100%, 680px);
gap: 14px;
}
.start-title {
font-size: clamp(40px, 11vw, 86px);
}
.start-tagline {
font-size: clamp(16px, 3vw, 24px);
}
.start-subtitle {
max-width: 28ch;
font-size: clamp(16px, 2.8vw, 24px);
}
.start-preview {
width: min(100%, 520px);
gap: 10px;
}
.start-help-button {
width: min(100%, 680px);
font-size: clamp(15px, 2.8vw, 20px);
padding: 14px 16px;
}
.start-button {
width: min(100%, 560px);
font-size: clamp(17px, 3.2vw, 24px);
padding: 16px 18px;
}
}
@media (max-width: 420px) {
.memory-container {
padding-bottom: calc(24px + env(safe-area-inset-bottom));
}
.performance-meter-shell {
padding: 8px 8px 10px;
gap: 6px;
}
.performance-meter-title {
font-size: 8px;
letter-spacing: 0.14em;
}
.meter-card {
padding: 9px 10px;
gap: 8px;
border-radius: 24px;
}
.meter {
--meter-side-padding: 8px;
--meter-knob-size: 28px;
height: 20px;
}
.ticks span {
height: 8px;
}
.knob-core::after {
height: calc(var(--meter-knob-size) * 0.30);
}
.side-label {
font-size: 8px;
gap: 3px;
}
.icon-box {
width: 16px;
height: 16px;
border-radius: 5px;
font-size: 9px;
}
.game-brand {
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
}
.game-brand-mark {
width: 42px;
height: 42px;
}
.game-brand-title {
font-size: 21px;
}
.game-brand-subtitle {
font-size: 11px;
line-height: 1.3;
max-width: 36ch;
}
.dialog-gameover {
width: min(92vw, 460px);
min-height: auto;
padding: 16px 14px 14px;
border-radius: 22px;
gap: 10px;
}
.gameover-icon-wrap {
width: 72px;
height: 72px;
}
.gameover-icon {
font-size: 34px;
}
.gameover-title {
font-size: clamp(30px, 10vw, 44px);
}
.gameover-divider {
width: min(100%, 180px);
gap: 6px;
}
.gameover-diamond {
font-size: 16px;
}
.gameover-subtitle {
font-size: 14px;
max-width: 24ch;
}
.gameover-score-pill {
padding: 11px 13px;
gap: 10px;
}
.gameover-score-icon {
font-size: 24px;
}
.gameover-score-label {
font-size: 14px;
}
.gameover-score-sep {
height: 22px;
}
.gameover-score-value {
font-size: 30px;
}
.start-utility-row {
width: min(100%, 560px);
gap: 8px;
}
.start-utility-row .start-help-button,
.start-utility-row .music-toggle-button {
padding: 13px 14px;
font-size: 13px;
border-radius: 20px;
}
.start-utility-row .start-help-icon,
.start-utility-row .music-toggle-icon {
width: 26px;
height: 26px;
font-size: 15px;
}
.top-utility-bar {
gap: 6px;
}
.top-utility-bar .utility-button,
.top-utility-bar .music-toggle-button {
padding: 8px 10px;
font-size: 11px;
border-radius: 16px;
}
.top-utility-bar .utility-button {
min-width: 84px;
}
.top-utility-bar .music-toggle-button {
min-width: 138px;
}
.top-utility-bar .high-score-pill {
min-width: 150px;
flex: 1 1 150px;
padding: 8px 10px;
}
.top-utility-bar .high-score-user {
max-width: 10ch;
}
.dialog-challenge.dialog-challenge-polished {
width: min(calc(100vw - 20px), 460px);
padding: 20px 14px 18px;
border-radius: 24px;
}
.challenge-close-btn {
top: 12px;
right: 12px;
width: 34px;
height: 34px;
font-size: 21px;
}
.challenge-hero {
margin-top: 0;
margin-bottom: 8px;
}
.challenge-hero-ring {
width: 74px;
height: 74px;
}
.challenge-hero-icon {
width: 54px;
height: 54px;
font-size: 30px;
}
.challenge-pill {
margin-bottom: 10px;
padding: 7px 12px;
font-size: 11px;
letter-spacing: 0.08em;
}
.challenge-title {
font-size: clamp(24px, 8vw, 34px);
}
.challenge-subtitle {
margin-top: 8px;
font-size: clamp(13px, 4.2vw, 16px);
max-width: 30ch;
}
.challenge-divider {
margin: 12px 0 14px;
gap: 8px;
}
.challenge-divider span {
width: 36px;
}
.challenge-points {
gap: 8px;
width: min(100%, 100%);
}
.challenge-point {
gap: 8px;
font-size: 13px;
}
.challenge-point-icon,
.challenge-note-icon {
width: 22px;
height: 22px;
font-size: 13px;
}
.challenge-rewards {
gap: 8px;
margin: 16px auto 12px;
}
.challenge-reward {
min-height: 42px;
padding: 9px 12px;
font-size: 12px;
}
.challenge-reward-icon {
font-size: 18px;
}
.challenge-note {
margin-bottom: 16px;
gap: 8px;
font-size: 12px;
}
.challenge-start-btn {
min-width: 100%;
min-height: 58px;
padding: 14px 18px;
font-size: 18px;
border-radius: 18px;
}
.dialog-card {
width: min(100%, calc(100vw - 24px));
padding: 18px 18px 16px;
border-radius: 20px;
}
#how-to-play-overlay {
padding: 8px;
}
.dialog-howto {
width: min(100%, calc(100vw - 16px));
max-height: calc(100dvh - 16px);
padding: 14px 12px 12px;
overflow-y: auto;
overscroll-behavior: contain;
}
.howto-logo-wrap {
margin: 0 0 2px;
}
.howto-logo {
width: 24px;
height: 24px;
}
.dialog-title {
font-size: 19px;
}
.dialog-text {
font-size: 13px;
}
.howto-kicker {
font-size: 12px;
letter-spacing: 0.14em;
}
.howto-headline {
font-size: clamp(15px, 4vw, 18px);
line-height: 1.08;
}
.howto-step-card {
padding: 8px 10px;
gap: 7px;
min-height: 0;
flex-direction: column;
justify-content: flex-start;
align-items: center;
text-align: center;
}
.howto-step-icon {
width: 36px;
height: 36px;
font-size: 18px;
}
.howto-step-title {
font-size: 11px;
}
.howto-step-text,
.howto-rule-text,
.howto-rewards-table {
font-size: 8.5px;
line-height: 1.15;
}
.howto-step-text {
text-align: center;
max-width: 13ch;
}
.howto-step-copy {
display: flex;
flex-direction: column;
align-items: center;
}
.howto-rewards-table {
width: 100%;
}
.howto-rule {
padding: 6px 7px;
gap: 6px;
}
.howto-rule-icon {
width: 28px;
height: 28px;
font-size: 15px;
}
.howto-rule-title {
font-size: 9px;
}
.howto-panel-title {
font-size: 10px;
margin: 1px 0 7px;
}
.howto-left,
.howto-right {
padding: 7px;
border-radius: 14px;
}
.howto-rewards-table th,
.howto-rewards-table td {
padding: 5px 4px;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
}
.howto-actions {
justify-content: flex-end;
gap: 0;
margin-top: 6px;
}
.howto-secondary-btn {
width: min(100%, 136px);
padding: 7px 9px;
font-size: 9px;
}
.dialog-leaderboard {
width: min(100%, calc(100vw - 16px));
max-height: calc(100dvh - 16px);
padding: 14px 12px 12px;
overflow-y: auto;
overscroll-behavior: contain;
}
.leaderboard-logo-wrap {
margin: 0 0 2px;
}
.leaderboard-logo {
width: 24px;
height: 24px;
}
.leaderboard-headline {
font-size: clamp(15px, 4vw, 18px);
}
.leaderboard-my-score {
margin-top: 12px;
padding: 12px 14px;
gap: 8px;
}
.leaderboard-my-score-label {
font-size: 10px;
letter-spacing: 0.14em;
}
.leaderboard-my-score-user {
font-size: 11px;
}
.leaderboard-my-score-value {
font-size: 22px;
}
.leaderboard-list {
margin-top: 12px;
display: block;
}
.leaderboard-table thead th {
padding: 10px 10px;
font-size: 10px;
}
.leaderboard-table tbody td {
padding: 10px 10px;
font-size: 12px;
}
.leaderboard-name {
font-size: 11px;
}
.leaderboard-score {
font-size: 14px;
}
.start-stage {
width: 100%;
height: auto;
min-height: 0;
padding: 16px 12px 18px;
transform: none;
align-items: flex-start;
}
.start-hero {
width: min(100%, 560px);
gap: 12px;
}
.start-mark {
width: 54px;
height: 54px;
}
.start-mark-icon {
font-size: 28px;
}
.start-title {
font-size: clamp(34px, 15vw, 60px);
letter-spacing: -0.05em;
}
.start-tagline {
font-size: 15px;
gap: 0.25em 0.35em;
}
.start-subtitle {
max-width: 28ch;
font-size: 15px;
}
.start-preview {
width: min(100%, 320px);
gap: 8px;
}
.preview-card {
border-radius: 20px;
box-shadow:
0 8px 0 rgba(16, 185, 129, 0.48),
0 16px 24px rgba(0, 0, 0, 0.3);
}
.preview-emoji {
font-size: 30px;
}
.start-help-button {
width: 100%;
padding: 13px 14px;
font-size: 15px;
border-radius: 20px;
gap: 10px;
}
.start-help-icon {
width: 28px;
height: 28px;
font-size: 16px;
}
.start-utility-row {
width: 100%;
gap: 8px;
}
.start-utility-row .start-help-button,
.start-utility-row .music-toggle-button,
.start-utility-row .leaderboard-button,
.start-utility-row .high-score-pill {
flex: 1 1 100%;
width: 100%;
min-width: 0;
padding: 12px 14px;
font-size: 13px;
border-radius: 18px;
}
.start-utility-row .start-help-icon,
.start-utility-row .music-toggle-icon,
.start-utility-row .leaderboard-icon,
.start-utility-row .high-score-icon {
width: 24px;
height: 24px;
font-size: 14px;
}
.start-utility-row .high-score-copy {
gap: 1px;
}
.start-utility-row .high-score-label {
font-size: 8px;
letter-spacing: 0.18em;
}
.start-utility-row .high-score-user {
font-size: 12px;
font-weight: 900;
max-width: 12ch;
}
.start-utility-row .high-score-value {
font-size: 18px;
}
.section-title-row {
gap: 10px;
}
.section-title {
font-size: 14px;
}
.start-select {
padding: 16px 16px;
font-size: 15px;
border-radius: 16px;
}
.start-button {
width: 100%;
font-size: 16px;
padding: 14px 16px;
gap: 12px;
border-radius: 22px;
}
.start-button-icon {
font-size: 0.9em;
}
.start-note {
font-size: 11px;
line-height: 1.35;
text-align: center;
}
.start-footer-row {
grid-template-columns: 1fr;
width: min(100%, 560px);
gap: 8px;
}
.start-footer-item {
justify-content: center;
text-align: center;
padding: 8px 10px;
border-radius: 14px;
}
.start-footer-label {
font-size: 9px;
}
.start-footer-value {
font-size: 10px;
}
.game-stats {
gap: 3px;
}
.stat-block {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
justify-items: center;
align-items: center;
text-align: center;
gap: 1px;
padding: 6px 3px 5px;
}
.stat-icon {
width: 20px;
height: 20px;
font-size: 10px;
grid-row: 1;
grid-column: 1;
justify-self: center;
}
.stat-label {
letter-spacing: 0.1em;
font-size: 6px;
grid-column: 1;
grid-row: 2;
align-self: center;
line-height: 1;
}
.stat-value {
font-size: 10px;
grid-column: 1;
grid-row: 3;
margin-top: 0;
align-self: center;
line-height: 1;
}
.game-state-badge {
padding: 5px 8px;
font-size: 9px;
gap: 4px;
letter-spacing: 0.1em;
}
.game-state-badge .badge-sub {
display: none;
}
.controls-row {
grid-template-columns: 1fr;
width: min(100%, 520px);
margin-inline: auto;
}
.next-level-button {
width: 100%;
grid-column: auto;
}
.memory-grid {
--grid-gap: 6px;
}
.memory-front,
.memory-back {
border-radius: 16px;
}
}
"""
# =========================================================
# JAVASCRIPT
# =========================================================
JS = r"""
const PREVIEW_SECONDS = 4;
class MatchWiseApp {
constructor() {
this.root = element;
this.grid = element.querySelector('#memory-grid');
this.cardsPanel = element.querySelector('.cards-panel');
this.overlay = element.querySelector('#preview-overlay');
this.transitionOverlay = element.querySelector('#transition-overlay');
this.transitionTitle = element.querySelector('#transition-title');
this.transitionSubtitle = element.querySelector('#transition-subtitle');
this.performanceMeterEl = element.querySelector('#meter');
this.performanceMeterSparkEl = element.querySelector('#spark');
this.performanceMeterKnobEl = element.querySelector('#knob');
this.boardFrame = element.querySelector('#board-frame') || element.querySelector('.board-frame');
this.memoryContainer = element.querySelector('.memory-container') || element;
this.popupHost = element.querySelector('.game-layout') || this.memoryContainer;
this.topUtilityBar = element.querySelector('.top-utility-bar');
this.startUtilityRow = element.querySelector('.start-utility-row');
this.countdown = element.querySelector('#preview-countdown');
this.movesEl = element.querySelector('#moves-display');
this.matchesEl = element.querySelector('#matches-display');
this.scoreEl = element.querySelector('#score-display');
this.livesEl = element.querySelector('#lives-display');
this.levelEl = element.querySelector('#level-display');
this.themeChip = element.querySelector('#theme-chip');
this.controlsRow = element.querySelector('#controls-row');
this.startOverlay = element.querySelector('#start-overlay');
this.startGameBtn = element.querySelector('#start-game-btn');
this.startNote = element.querySelector('#start-note');
this.howToPlayBtn = element.querySelector('#how-to-play-btn');
this.howToPlayOverlay = element.querySelector('#how-to-play-overlay');
this.howToPlayCloseBtn = element.querySelector('#how-to-play-close-btn');
this.controlsRow = element.querySelector('#controls-row');
this.hintCountEl = element.querySelector('#hint-count');
this.stateBadge = element.querySelector('#game-state-badge');
this.statusEl = element.querySelector('#game-status-area');
this.statusTextEl = element.querySelector('#status-text');
this.statusSubtextEl = element.querySelector('#status-subtext');
this.resetBtn = element.querySelector('#reset-btn');
this.hintBtn = element.querySelector('#hint-btn');
this.nextBtn = element.querySelector('#next-btn');
this.resetConfirmOverlay = element.querySelector('#reset-confirm-overlay');
this.resetConfirmBtn = element.querySelector('#reset-confirm-btn');
this.resetCancelBtn = element.querySelector('#reset-cancel-btn');
this.gameOverOverlay = element.querySelector('#gameover-overlay');
this.gameOverScore = element.querySelector('#gameover-score');
this.gameOverCloseBtn = element.querySelector('#gameover-close-btn');
this.challengeIntroOverlay = element.querySelector('#challenge-intro-overlay');
this.challengeCloseBtn = element.querySelector('#challenge-close-btn');
this.challengeModalTitleEl = element.querySelector('#challenge-modal-title');
this.challengeModalMessageEl = element.querySelector('#challenge-modal-message');
this.challengeModalSubtitleEl = element.querySelector('#challenge-modal-subtitle');
this.challengeOkBtn = element.querySelector('#challenge-ok-btn');
this.challengePanel = element.querySelector('#challenge-panel');
this.challengeThemeEl = element.querySelector('#challenge-theme');
this.challengeQuestionEl = element.querySelector('#challenge-question');
this.challengeOptionsEl = element.querySelector('#challenge-options');
this.challengeFocusEl = element.querySelector('#challenge-focus');
this.leaderboardBtn = element.querySelector('#leaderboard-btn');
this.leaderboardOverlay = element.querySelector('#leaderboard-overlay');
this.leaderboardCloseBtn = element.querySelector('#leaderboard-close-btn');
this.leaderboardListEl = element.querySelector('#leaderboard-list');
this.leaderboardMyScoreUserEl = element.querySelector('#leaderboard-my-score-user');
this.leaderboardMyScoreValueEl = element.querySelector('#leaderboard-my-score-value');
this.highScoreLabelEl = element.querySelector('#high-score-label');
this.highScoreUserEl = element.querySelector('#high-score-user');
this.highScoreValueEl = element.querySelector('#high-score-value');
this.homeBtn = element.querySelector('#home-btn');
this.pageMode = this.grid ? 'level' : 'start';
this.sessionHash = this.getSessionHashFromUrl();
this.state = JSON.parse(props.state_json || '{}');
this.cards = Array.isArray(this.state.cards) ? [...this.state.cards] : [];
this.flipped = [];
this.matched = [];
const initialHints = Number.parseInt(props.hints ?? this.state.hints ?? '1', 10);
const initialMaxHints = Number.parseInt(props.max_hints ?? this.state.max_hints ?? '5', 10);
this.hints = Number.isNaN(initialHints) ? 1 : initialHints;
this.maxHints = Number.isNaN(initialMaxHints) ? 5 : initialMaxHints;
this.hinting = false;
this.hintedPairs = new Set();
this.moves = 0;
this.locked = false;
this.previewTimer = null;
this.previewShuffleTimer = null;
this.previewShuffleAnimationMs = 520;
this.previewFogTimers = [];
this.previewHideTimers = [];
this.previewCount = PREVIEW_SECONDS;
this.pendingTransition = false;
this.levelStartTime = null;
this.transitionShownAt = 0;
this.transitionHideTimer = null;
this.comboStreak = 0;
this.lastRenderedHints = null;
this.lastRenderedLives = null;
this.lastRenderedPerformanceMeter = null;
this.peekGlowTimer = null;
this.livesGlowTimer = null;
this.wrongMatchTimer = null;
this.distractorSparkleTimer = null;
this.distractorSparkleClearTimer = null;
this.recentlyFlippedCardIndexes = [];
this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
this.challengeStarted = false;
this.challengeSelectionLocked = false;
this.challengeResultIndex = null;
this.challengeCorrectIndex = null;
this.challengeOptions = [];
this.leaderboardData = { entries: [], current_user: {} };
this.leaderboardRefreshTimer = null;
this.loginButtonObserver = null;
this.loginButtonRefreshEl = null;
this.musicToggleStartBtn = element.querySelector('#music-toggle-start-btn');
this.musicToggleTopBtn = element.querySelector('#music-toggle-top-btn');
this.musicToggleButtons = [];
this.musicEnabled = this.loadMusicPreference();
this.audioContext = null;
this.injectPopupRuntimeStyles();
this.ensurePopupRuntimeContainer();
this.installEvents();
this.installButtonHandlers();
this.syncMusicToggleButtons();
this.attachLoginButtonToTopBar();
requestAnimationFrame(() => this.attachLoginButtonToTopBar());
this.installLeaderboardRefreshHooks();
void this.refreshLeaderboardData();
this.installViewportHandlers();
this.hideResetConfirm();
this.hideGameOverModal();
this.stopDistractorSparkles();
this.hideHowToPlayModal();
if (this.pageMode === 'start') {
if (this.startNote) {
this.startNote.textContent = 'Press Start to generate your first level.';
}
return;
}
this.root.classList.add('level-loading');
if (this.memoryContainer && this.memoryContainer !== this.root) {
this.memoryContainer.classList.add('level-loading');
}
this.injectPopupRuntimeStyles();
this.ensurePopupRuntimeContainer();
void this.bootstrapLevelPage();
}
syncControlsVisibility() {
if (!this.controlsRow) return;
this.controlsRow.style.setProperty('display', this.state.game_started ? 'grid' : 'none', 'important');
}
resetComboFeedback() {
this.comboStreak = 0;
}
clearWrongMatchTimer() {
if (this.wrongMatchTimer) {
clearTimeout(this.wrongMatchTimer);
this.wrongMatchTimer = null;
}
}
getSessionHashFromUrl() {
try {
const url = new URL(window.location.href);
return String(url.searchParams.get('session_hash') || '').trim();
} catch (err) {
return '';
}
}
injectPopupRuntimeStyles() {
if (document.getElementById('pop-runtime-style')) {
return;
}
const style = document.createElement('style');
style.id = 'pop-runtime-style';
style.textContent = `
.pop-wrapper-runtime {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 999999;
width: 100%;
height: 100%;
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
}
.pop-text-runtime {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0) rotate(-15deg);
opacity: 0;
font-family: Impact, "Arial Black", system-ui, sans-serif;
font-size: clamp(28px, 5.6vw, 72px);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 1px;
white-space: pre-line;
line-height: 1;
text-align: center;
max-width: min(92vw, 720px);
-webkit-text-stroke: 3px #ffffff;
filter:
drop-shadow(0 4px 0px rgba(0, 0, 0, 0.45))
drop-shadow(0 10px 16px rgba(0, 0, 0, 0.35));
will-change: transform, opacity;
}
.pop-text-runtime.animate {
animation: popBounceRuntime 1.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.pop-good-runtime {
background: linear-gradient(to bottom, #ffffff 18%, #6CF527 58%, #15803d 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pop-win-runtime {
background: linear-gradient(to bottom, #ffffff 18%, #ffe66d 42%, #fb5607 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pop-bad-runtime {
background: linear-gradient(to bottom, #ffffff 18%, #f87171 58%, #dc2626 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.memory-card.distractor-sparkle .memory-front::after {
content: 'โฆ';
position: absolute;
right: 9%;
top: 8%;
width: 28%;
height: 28%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: rgba(255, 255, 255, 0.95);
background: radial-gradient(circle, rgba(255,255,255,0.78), rgba(255,255,255,0.12) 58%, transparent 72%);
filter: drop-shadow(0 0 10px rgba(255,255,255,0.85));
font-size: clamp(14px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.18), 28px);
pointer-events: none;
animation: distractorSparkleRuntime 680ms ease-out forwards;
z-index: 4;
}
.memory-card.distractor-sparkle .memory-back::after {
content: 'โฆ';
position: absolute;
left: 9%;
top: 8%;
width: 28%;
height: 28%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: rgba(255, 255, 255, 0.95);
background: radial-gradient(circle, rgba(255,255,255,0.7), rgba(255,255,255,0.1) 58%, transparent 72%);
filter: drop-shadow(0 0 10px rgba(255,255,255,0.75));
font-size: clamp(14px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.16), 26px);
pointer-events: none;
animation: distractorSparkleRuntime 680ms ease-out forwards;
z-index: 4;
}
.memory-container.preview-fog-blink .memory-card.preview-locked .memory-back,
.matchwise-root.preview-fog-blink .memory-card.preview-locked .memory-back {
animation: previewFogBlinkRuntime 260ms ease-in-out forwards;
}
.memory-card.preview-hidden-card .memory-back {
animation: previewCardHideRuntime 620ms ease-in-out forwards;
}
.memory-card.preview-hidden-card::after {
content: 'โ๏ธ';
position: absolute;
inset: 10%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 24px;
font-size: clamp(24px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.28), 46px);
background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(255,255,255,0.62) 55%, rgba(255,255,255,0.14));
filter: drop-shadow(0 10px 18px rgba(132, 90, 255, 0.2));
pointer-events: none;
z-index: 8;
animation: previewCloudCoverRuntime 620ms ease-in-out forwards;
}
@keyframes distractorSparkleRuntime {
0% { transform: scale(0.35) rotate(-18deg); opacity: 0; }
28% { transform: scale(1.18) rotate(10deg); opacity: 1; }
100% { transform: scale(0.65) rotate(24deg); opacity: 0; }
}
@keyframes previewFogBlinkRuntime {
0% { filter: blur(0px) brightness(1); opacity: 1; }
45% { filter: blur(5px) brightness(0.94); opacity: 0.72; }
100% { filter: blur(0px) brightness(1); opacity: 1; }
}
@keyframes previewCardHideRuntime {
0% { filter: blur(0px); opacity: 1; }
18% { filter: blur(3px); opacity: 0.18; }
82% { filter: blur(3px); opacity: 0.18; }
100% { filter: blur(0px); opacity: 1; }
}
@keyframes previewCloudCoverRuntime {
0% { transform: scale(0.86); opacity: 0; }
18% { transform: scale(1); opacity: 1; }
82% { transform: scale(1); opacity: 1; }
100% { transform: scale(1.06); opacity: 0; }
}
@keyframes popBounceRuntime {
0% {
transform: translate(-50%, -50%) scale(0) rotate(-15deg);
opacity: 0;
}
15% {
transform: translate(-50%, -50%) scale(1.35) rotate(5deg);
opacity: 1;
}
30% {
transform: translate(-50%, -50%) scale(0.92) rotate(-4deg);
opacity: 1;
}
45% {
transform: translate(-50%, -50%) scale(1.08) rotate(3deg);
opacity: 1;
}
60% {
transform: translate(-50%, -50%) scale(0.98) rotate(-1deg);
opacity: 1;
}
72% {
transform: translate(-50%, -50%) scale(1) rotate(0deg);
opacity: 1;
}
100% {
transform: translate(-50%, -85%) scale(0.78) rotate(-5deg);
opacity: 0;
}
}
@media (max-width: 520px) {
.pop-text-runtime {
font-size: clamp(12px, 4.6vw, 24px);
letter-spacing: 0.2px;
max-width: calc(100vw - 24px);
-webkit-text-stroke: 1px #ffffff;
}
}
`;
document.head.appendChild(style);
}
ensurePopupRuntimeContainer() {
const host = this.popupHost || this.memoryContainer;
if (!host) return;
let popupContainer = host.querySelector('.pop-wrapper-runtime');
if (!popupContainer) {
popupContainer = document.createElement('div');
popupContainer.className = 'pop-wrapper-runtime';
host.appendChild(popupContainer);
}
this.popupContainer = popupContainer;
}
loadMusicPreference() {
try {
const stored = window.localStorage.getItem('matchwise_music_enabled');
if (stored === null) return true;
return stored !== '0' && stored !== 'false';
} catch (err) {
return true;
}
}
saveMusicPreference(enabled) {
try {
window.localStorage.setItem('matchwise_music_enabled', enabled ? '1' : '0');
} catch (err) {
// ignore storage failures
}
}
syncMusicToggleButtons() {
this.musicToggleButtons = [
this.musicToggleStartBtn,
this.musicToggleTopBtn,
].filter(Boolean);
this.musicToggleButtons.forEach((button) => {
const textEl = button.querySelector('.music-toggle-text');
if (textEl) {
textEl.textContent = this.musicEnabled ? 'Sound: ON' : 'Sound: OFF';
}
button.classList.toggle('music-toggle-off', !this.musicEnabled);
button.setAttribute('aria-pressed', this.musicEnabled ? 'true' : 'false');
});
}
setMusicEnabled(enabled) {
this.musicEnabled = Boolean(enabled);
this.saveMusicPreference(this.musicEnabled);
this.syncMusicToggleButtons();
}
toggleMusic() {
this.setMusicEnabled(!this.musicEnabled);
}
getAudioContext() {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return null;
if (!this.audioContext) {
this.audioContext = new AudioContextClass();
}
if (this.audioContext.state === 'suspended') {
void this.audioContext.resume();
}
return this.audioContext;
}
playCorrectSound() {
if (!this.musicEnabled) return;
const audioCtx = this.getAudioContext();
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc1 = audioCtx.createOscillator();
const gain1 = audioCtx.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(659, now);
gain1.gain.setValueAtTime(0.3, now);
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.12);
osc1.connect(gain1);
gain1.connect(audioCtx.destination);
const osc2 = audioCtx.createOscillator();
const gain2 = audioCtx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(880, now + 0.08);
gain2.gain.setValueAtTime(0.3, now + 0.08);
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc2.connect(gain2);
gain2.connect(audioCtx.destination);
osc1.start(now);
osc1.stop(now + 0.12);
osc2.start(now + 0.08);
osc2.stop(now + 0.35);
}
playWrongSound() {
if (!this.musicEnabled) return;
const audioCtx = this.getAudioContext();
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(70, now + 0.25);
gain.gain.setValueAtTime(0.4, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.25);
}
async bootstrapLevelPage() {
const requestedSessionHash = this.getSessionHashFromUrl() || this.sessionHash;
if (!requestedSessionHash) {
window.location.replace('/');
return;
}
this.sessionHash = requestedSessionHash;
try {
const response = this.normalizeServerResponse(
await server.load_level_state(JSON.stringify({
session_hash: this.sessionHash,
}))
);
if (!response || !response.state) {
if (response && response.error === 'missing_session') {
window.location.replace('/');
return;
}
throw new Error(response?.error || 'Unable to load session');
}
if (response.session_hash) {
this.sessionHash = String(response.session_hash);
const nextUrl = new URL(window.location.href);
nextUrl.pathname = '/level';
nextUrl.searchParams.set('session_hash', this.sessionHash);
window.history.replaceState({}, '', nextUrl.toString());
}
this.state = {
...this.state,
...response.state,
game_started: true,
};
this.renderState(response.state, true);
this.lastRenderedHints = this.hints;
this.lastRenderedLives = Number(this.state.lives ?? 0);
this.syncControlsVisibility();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.root.classList.remove('level-loading');
if (this.memoryContainer && this.memoryContainer !== this.root) {
this.memoryContainer.classList.remove('level-loading');
}
this.startPreview();
});
});
} catch (err) {
console.error('load_level_state failed', err);
this.root.classList.remove('level-loading');
if (this.memoryContainer && this.memoryContainer !== this.root) {
this.memoryContainer.classList.remove('level-loading');
}
this.setStatus('Could not load the level.', 'error', 'Please return to the start page and try again.');
}
}
triggerPopup(text, styleClass = 'pop-good-runtime') {
if (!this.popupContainer) {
this.ensurePopupRuntimeContainer();
}
if (!this.popupContainer) return;
const popup = document.createElement('div');
popup.className = `pop-text-runtime ${styleClass}`;
popup.textContent = String(text || '').trim() || 'Tasty!';
this.popupContainer.appendChild(popup);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
popup.classList.add('animate');
});
});
window.setTimeout(() => {
if (popup && popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 1300);
}
showWrongMatchPopup() {
this.triggerPopup('Wrong Match:\n -1 lives!', 'pop-bad-runtime');
}
showLevelCompletePopup(message = 'Level Complete!') {
this.triggerPopup(message, 'pop-win-runtime');
}
showFeedbackOverlay(kicker = '', text = '', type = 'positive', lock = false) {
if (type === 'negative') {
this.showWrongMatchPopup();
return;
}
if (type === 'level') {
this.showLevelCompletePopup(text || 'Level Complete!');
return;
}
if (type === 'combo' || type === 'positive') {
this.triggerPopup('Matched!', 'pop-good-runtime');
return;
}
this.triggerPopup('Matched!', 'pop-good-runtime');
}
showComboFeedback() {
this.triggerPopup('Combo!', 'pop-good-runtime');
}
hexToRgb(hex) {
const clean = String(hex || '').replace('#', '');
return {
r: parseInt(clean.substring(0, 2), 16),
g: parseInt(clean.substring(2, 4), 16),
b: parseInt(clean.substring(4, 6), 16),
};
}
rgbToHex(r, g, b) {
return `#${[r, g, b].map((value) => Number(value).toString(16).padStart(2, '0')).join('')}`;
}
mixColor(colorA, colorB, amount) {
const a = this.hexToRgb(colorA);
const b = this.hexToRgb(colorB);
const mix = Math.max(0, Math.min(1, Number(amount)));
const r = Math.round(a.r + (b.r - a.r) * mix);
const g = Math.round(a.g + (b.g - a.g) * mix);
const blue = Math.round(a.b + (b.b - a.b) * mix);
return this.rgbToHex(r, g, blue);
}
hexToRgba(hex, alpha) {
const rgb = this.hexToRgb(hex);
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
}
getMeterGradientColor(value) {
const safeValue = Math.max(0, Math.min(100, Number(value)));
const gradientStops = [
{ point: 0, color: '#17a34a' },
{ point: 28, color: '#97c51e' },
{ point: 50, color: '#ffd522' },
{ point: 72, color: '#ff981f' },
{ point: 100, color: '#ff2e25' },
];
for (let i = 0; i < gradientStops.length - 1; i += 1) {
const start = gradientStops[i];
const end = gradientStops[i + 1];
if (safeValue >= start.point && safeValue <= end.point) {
const localAmount = (safeValue - start.point) / (end.point - start.point);
return this.mixColor(start.color, end.color, localAmount);
}
}
return gradientStops[gradientStops.length - 1].color;
}
updatePerformanceMeter(value, animate = false) {
const safeValue = Math.max(0, Math.min(100, Number(value || 0)));
const dynamicColor = this.getMeterGradientColor(safeValue);
const dynamicGlow = this.hexToRgba(dynamicColor, 0.72);
if (this.performanceMeterEl) {
this.performanceMeterEl.style.setProperty('--meter-value', `${safeValue}%`);
this.performanceMeterEl.style.setProperty('--knob-color', dynamicColor);
this.performanceMeterEl.style.setProperty('--knob-glow', dynamicGlow);
}
if (!animate) {
return;
}
if (this.performanceMeterKnobEl) {
this.performanceMeterKnobEl.classList.remove('bump');
void this.performanceMeterKnobEl.offsetWidth;
this.performanceMeterKnobEl.classList.add('bump');
}
if (this.performanceMeterEl) {
this.performanceMeterEl.classList.remove('flash');
void this.performanceMeterEl.offsetWidth;
this.performanceMeterEl.classList.add('flash');
}
}
installEvents() {
this.root.addEventListener('click', async (e) => {
const card = e.target.closest('.memory-card');
if (card && !this.locked) {
await this.handleCardClick(card);
return;
}
});
}
installButtonHandlers() {
if (this.resetBtn) {
this.resetBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
if (this.state.game_over || this.state.status === 'game_over') {
await this.resetGame();
return;
}
this.showResetConfirm();
});
}
if (this.resetConfirmBtn) {
this.resetConfirmBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
this.hideResetConfirm();
await this.resetGame();
});
}
if (this.resetCancelBtn) {
this.resetCancelBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.hideResetConfirm();
});
}
if (this.homeBtn) {
this.homeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
window.location.assign('/');
});
}
if (this.gameOverCloseBtn) {
this.gameOverCloseBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.hideGameOverModal();
});
}
if (this.challengeCloseBtn) {
this.challengeCloseBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.openChallengeQuestion();
});
}
if (this.challengeOkBtn) {
this.challengeOkBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.openChallengeQuestion();
});
}
if (this.hintBtn) {
this.hintBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.useHint();
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.advanceLevel();
});
}
if (this.startGameBtn) {
this.startGameBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.startGame();
});
}
if (this.howToPlayBtn) {
this.howToPlayBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.showHowToPlayModal();
});
}
if (this.musicToggleStartBtn) {
this.musicToggleStartBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleMusic();
});
}
if (this.musicToggleTopBtn) {
this.musicToggleTopBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleMusic();
});
}
this.installLeaderboardRefreshHooks();
if (this.leaderboardBtn) {
this.leaderboardBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.openLeaderboardModal();
});
}
if (this.howToPlayCloseBtn) {
this.howToPlayCloseBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.hideHowToPlayModal();
});
}
if (this.howToPlayOverlay) {
this.howToPlayOverlay.addEventListener('click', (e) => {
if (e.target === this.howToPlayOverlay) {
this.hideHowToPlayModal();
}
});
}
if (this.leaderboardCloseBtn) {
this.leaderboardCloseBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.hideLeaderboardModal();
});
}
if (this.leaderboardOverlay) {
this.leaderboardOverlay.addEventListener('click', (e) => {
if (e.target === this.leaderboardOverlay) {
this.hideLeaderboardModal();
}
});
}
if (this.challengeOptionsEl) {
this.challengeOptionsEl.addEventListener('click', async (e) => {
const button = e.target.closest('.challenge-option');
if (!button) return;
await this.handleChallengeChoice(button);
});
}
}
installViewportHandlers() {
const rerenderCardSize = () => {
this.updateCardSize();
};
window.addEventListener('resize', rerenderCardSize, { passive: true });
window.addEventListener('orientationchange', () => {
setTimeout(rerenderCardSize, 150);
}, { passive: true });
}
attachLoginButtonToTopBar() {
if (!this.topUtilityBar) return;
const loginButton = document.getElementById('hf-login-button');
if (!loginButton) return;
if (loginButton.parentElement === this.topUtilityBar) return;
this.topUtilityBar.appendChild(loginButton);
}
scheduleLeaderboardRefresh(delayMs = 0) {
if (this.leaderboardRefreshTimer) {
clearTimeout(this.leaderboardRefreshTimer);
this.leaderboardRefreshTimer = null;
}
this.leaderboardRefreshTimer = setTimeout(() => {
this.leaderboardRefreshTimer = null;
void this.refreshLeaderboardData();
}, Math.max(0, Number(delayMs) || 0));
}
installLeaderboardRefreshHooks() {
const loginButton = document.getElementById('hf-login-button');
if (!loginButton || loginButton === this.loginButtonRefreshEl) return;
if (this.loginButtonObserver) {
this.loginButtonObserver.disconnect();
this.loginButtonObserver = null;
}
this.loginButtonRefreshEl = loginButton;
loginButton.addEventListener('click', () => {
this.scheduleLeaderboardRefresh(900);
});
this.loginButtonObserver = new MutationObserver(() => {
this.scheduleLeaderboardRefresh(250);
});
this.loginButtonObserver.observe(loginButton, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
}
updateLeaderboardDisplay() {
const signedIn = Boolean(this.state.leaderboard_signed_in);
const username = String(this.state.leaderboard_username || '').trim();
const highScore = Number(this.state.leaderboard_high_score || 0);
if (this.highScoreLabelEl) {
this.highScoreLabelEl.textContent = signedIn ? 'My High Score' : 'Guest Score';
}
if (this.highScoreUserEl) {
this.highScoreUserEl.textContent = username || (signedIn ? 'Signed in' : 'Guest');
}
if (this.highScoreValueEl) {
this.highScoreValueEl.textContent = String(highScore);
}
if (this.leaderboardMyScoreUserEl) {
this.leaderboardMyScoreUserEl.textContent = username || (signedIn ? 'Signed in' : 'Guest');
}
if (this.leaderboardMyScoreValueEl) {
this.leaderboardMyScoreValueEl.textContent = String(highScore);
}
}
renderLeaderboardEntries(entries) {
if (!this.leaderboardListEl) return;
const rows = this.getRankedLeaderboardEntries(entries);
if (!rows.length) {
this.leaderboardListEl.innerHTML = '
No scores yet. Be the first to play.
';
return;
}
const currentUsername = String(this.state.leaderboard_username || '').trim().toLowerCase();
this.leaderboardListEl.innerHTML = `
| Rank |
HF Username |
High Score |
${rows.map((entry) => {
const username = String(entry.username || 'Guest').trim();
const score = Number(entry.score || 0);
const isCurrentUser = currentUsername && username.toLowerCase() === currentUsername;
return `
| #${Number(entry.rank || 0)} |
${this.escapeHtml(username)}${isCurrentUser ? ' (You)' : ''} |
${score} |
`;
}).join('')}
`;
}
getRankedLeaderboardEntries(entries) {
const rows = Array.isArray(entries) ? entries : [];
return rows
.map((entry) => ({
username: String(entry?.username || 'Guest').trim(),
score: Number(entry?.score || 0),
}))
.filter((entry) => entry.username)
.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
return a.username.localeCompare(b.username, undefined, { sensitivity: 'base' });
})
.map((entry, index) => ({
...entry,
rank: index + 1,
}));
}
async refreshLeaderboardData() {
try {
const response = this.normalizeServerResponse(
await server.get_leaderboard_data(JSON.stringify({
game_id: this.state.game_id || '',
}))
);
if (!response) return null;
const currentUser = response.current_user || {};
this.state.leaderboard_signed_in = Boolean(currentUser.leaderboard_signed_in);
this.state.leaderboard_username = String(currentUser.leaderboard_username || '');
this.state.leaderboard_high_score = Number(currentUser.leaderboard_high_score || 0);
this.state.leaderboard_saved = Boolean(currentUser.leaderboard_saved);
this.leaderboardData = {
entries: Array.isArray(response.entries) ? response.entries : [],
current_user: currentUser,
};
this.updateLeaderboardDisplay();
this.renderLeaderboardEntries(this.leaderboardData.entries);
return this.leaderboardData;
} catch (err) {
return null;
}
}
async openLeaderboardModal() {
this.showLeaderboardModal();
if (this.leaderboardListEl) {
this.leaderboardListEl.innerHTML = '
Loading leaderboard...
';
}
await this.refreshLeaderboardData();
this.showLeaderboardModal();
}
snapshot() {
const activeLevel = {
...(this.activeLevel || {}),
blueprint: {
...((this.activeLevel && this.activeLevel.blueprint) || {}),
},
cards: [...(this.cards || [])],
};
return {
...this.state,
active_level: activeLevel,
cards: [...this.cards],
flipped: [...this.flipped],
matched: [...this.matched],
moves: this.moves,
hints: this.hints,
max_hints: this.maxHints,
performance_meter: this.state.performance_meter,
challenge_due: this.state.challenge_due,
game_started: this.state.game_started,
status: this.state.status,
level_complete: this.state.level_complete,
game_over: this.state.game_over,
preview_seconds: PREVIEW_SECONDS,
};
}
async syncBackend() {
try {
await server.sync_state(JSON.stringify(this.snapshot()));
} catch (err) {
return;
}
}
setBadge(text, kind, subtext = '') {
if (!this.stateBadge) return;
if (kind === 'complete' && subtext) {
this.stateBadge.innerHTML = `
${text}${subtext}`;
} else {
this.stateBadge.textContent = text;
}
this.stateBadge.classList.remove('playing', 'complete', 'gameover', 'challenge');
if (kind) this.stateBadge.classList.add(kind);
}
getCompletionStarCount() {
const totalPairs = Math.max(1, Number(this.state.total_pairs || this.cards.length / 2 || 1));
const moves = Math.max(0, Number(this.state.moves ?? this.moves ?? 0));
const wrongAttempts = Math.max(0, Number(this.state.wrong_attempts || 0));
if (wrongAttempts === 0 && moves === totalPairs) {
return 3;
}
if (wrongAttempts <= 1 && moves <= totalPairs + 2) {
return 2;
}
return 1;
}
getCompletionBadgeText() {
return 'โญ'.repeat(this.getCompletionStarCount());
}
getCompletionRatingLabel() {
const stars = this.getCompletionStarCount();
if (stars >= 3) return 'Perfect';
if (stars === 2) return 'Good';
return 'Average';
}
setStatus(text, kind = '', subtext = '') {
if (this.statusTextEl) this.statusTextEl.textContent = text;
if (this.statusSubtextEl) this.statusSubtextEl.textContent = subtext;
if (!this.statusEl) return;
this.statusEl.classList.remove('show', 'error', 'win');
if (kind) this.statusEl.classList.add(kind);
}
pulseHudGlow(target, glowClass, timerKey) {
if (!target) return;
const timerName = `${timerKey}GlowTimer`;
if (this[timerName]) {
clearTimeout(this[timerName]);
this[timerName] = null;
}
target.classList.remove(glowClass);
void target.offsetWidth;
target.classList.add(glowClass);
this[timerName] = setTimeout(() => {
target.classList.remove(glowClass);
this[timerName] = null;
}, 850);
}
getEffectiveMaxHints() {
const baseMax = Math.max(1, Number(this.maxHints || this.state.max_hints || 5));
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
// Hint cap gently tightens as levels progress.
// L1-5: normal cap, L6-10: -1, L11-16: -2, L17+: -3.
const reduction = level <= 5 ? 0 : level <= 10 ? 1 : level <= 16 ? 2 : 3;
return Math.max(1, baseMax - reduction);
}
syncProgressiveHintLimit(announce = false) {
const effectiveMaxHints = this.getEffectiveMaxHints();
const previousHints = Number(this.hints || 0);
this.hints = Math.max(0, Math.min(effectiveMaxHints, previousHints));
this.state.hints = this.hints;
if (announce && previousHints > this.hints) {
this.setStatus(
'Harder level: peeks are rarer now.',
'show',
`Peek limit for this level: ${this.hints} / ${effectiveMaxHints}`
);
}
return effectiveMaxHints;
}
getHintCooldownLengthMoves() {
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
// Early levels stay friendly. Later levels make Peek a small decision.
if (level < 7) return 0;
if (level < 14) return 1;
return 2;
}
decrementPeekCooldownAfterMove() {
if (this.peekCooldownMoves > 0) {
this.peekCooldownMoves = Math.max(0, this.peekCooldownMoves - 1);
this.state.peek_cooldown_moves = this.peekCooldownMoves;
this.updateHintsDisplay();
}
}
getWrongFlipBackDelayMs() {
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
// Level 1-5: 900ms, Level 6-12: 700ms, Level 13+: 550ms.
if (level <= 5) return 900;
if (level <= 12) return 700;
return 550;
}
getCompletedChallengeLevels() {
return Math.max(0, Number(this.state.completed_challenge_levels || 0));
}
rememberRecentlyFlipped(index) {
const value = Number(index);
if (!Number.isFinite(value)) return;
this.recentlyFlippedCardIndexes = [
value,
...this.recentlyFlippedCardIndexes.filter((item) => item !== value),
].slice(0, 6);
}
getDistractorSparkleIntervalMs() {
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
// Very light distraction: slower early, slightly quicker later.
return Math.max(1700, 3900 - Math.min(1700, (level - 1) * 95));
}
triggerDistractorSparkleOnce() {
if (!this.grid || this.state.status !== 'playing' || this.locked || this.state.game_over || this.state.level_complete) {
return;
}
const faceDownCandidates = Array.from(this.grid.querySelectorAll('.memory-card:not(.matched):not(.flipped):not(.preview-locked)'));
const recentlyFlippedCandidates = this.recentlyFlippedCardIndexes
.map((index) => this.grid.querySelector(`[data-index="${index}"]`))
.filter((card) => card && !card.classList.contains('matched') && !card.classList.contains('preview-locked'));
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
const canUseRecentDecoy = level >= 8 && recentlyFlippedCandidates.length > 0;
const useRecentDecoy = canUseRecentDecoy && Math.random() < 0.34;
const candidates = useRecentDecoy ? recentlyFlippedCandidates : faceDownCandidates;
if (!candidates.length) return;
const card = candidates[Math.floor(Math.random() * candidates.length)];
if (!card) return;
card.classList.remove('distractor-sparkle');
void card.offsetWidth;
card.classList.add('distractor-sparkle');
if (this.distractorSparkleClearTimer) {
clearTimeout(this.distractorSparkleClearTimer);
this.distractorSparkleClearTimer = null;
}
this.distractorSparkleClearTimer = setTimeout(() => {
card.classList.remove('distractor-sparkle');
this.distractorSparkleClearTimer = null;
}, 740);
}
startDistractorSparkles() {
if (this.distractorSparkleTimer || !this.grid || this.state.status !== 'playing') {
return;
}
const interval = this.getDistractorSparkleIntervalMs();
this.distractorSparkleTimer = setInterval(() => {
if (window.__activeMemoryLevelId !== this.levelId || this.state.status !== 'playing') {
this.stopDistractorSparkles();
return;
}
this.triggerDistractorSparkleOnce();
}, interval);
}
stopDistractorSparkles() {
if (this.distractorSparkleTimer) {
clearInterval(this.distractorSparkleTimer);
this.distractorSparkleTimer = null;
}
if (this.distractorSparkleClearTimer) {
clearTimeout(this.distractorSparkleClearTimer);
this.distractorSparkleClearTimer = null;
}
if (this.grid) {
this.grid.querySelectorAll('.memory-card.distractor-sparkle').forEach((card) => {
card.classList.remove('distractor-sparkle');
});
}
}
updateHintsDisplay() {
const effectiveMaxHints = this.syncProgressiveHintLimit(false);
if (this.hintCountEl) {
this.hintCountEl.textContent = `${this.hints} / ${effectiveMaxHints}`;
}
const currentHints = Number(this.hints || 0);
if (this.lastRenderedHints !== null && currentHints > this.lastRenderedHints) {
this.pulseHudGlow(this.hintCountEl || this.hintBtn, 'hud-glow-peek', 'peek');
}
this.lastRenderedHints = currentHints;
if (this.hintBtn) {
const oddFaceUp = this.flipped.length % 2 === 1;
const disabled =
this.state.level_type === 'challenge' ||
this.hints <= 0 ||
this.state.status !== 'playing' ||
this.hinting ||
this.peekCooldownMoves > 0 ||
oddFaceUp ||
this.state.game_over ||
this.state.level_complete;
this.hintBtn.disabled = disabled;
}
}
showResetConfirm() {
if (this.resetConfirmOverlay) {
this.resetConfirmOverlay.classList.remove('hidden');
this.resetConfirmOverlay.setAttribute('aria-hidden', 'false');
}
}
hideResetConfirm() {
if (this.resetConfirmOverlay) {
this.resetConfirmOverlay.classList.add('hidden');
this.resetConfirmOverlay.setAttribute('aria-hidden', 'true');
}
}
showGameOverModal(score) {
if (this.gameOverScore) {
this.gameOverScore.textContent = String(score ?? this.state.score ?? 0);
}
const scorePillValue = this.root.querySelector('#gameover-score-pill');
if (scorePillValue) {
scorePillValue.textContent = String(score ?? this.state.score ?? 0);
}
this.disableControlsForGameOver();
if (this.gameOverOverlay) {
this.gameOverOverlay.classList.remove('hidden');
this.gameOverOverlay.setAttribute('aria-hidden', 'false');
}
}
hideGameOverModal() {
if (this.gameOverOverlay) {
this.gameOverOverlay.classList.add('hidden');
this.gameOverOverlay.setAttribute('aria-hidden', 'true');
}
this.restoreControlsAfterGameOver();
}
disableControlsForGameOver() {
if (this.resetBtn) this.resetBtn.disabled = false;
if (this.hintBtn) this.hintBtn.disabled = true;
if (this.nextBtn) this.nextBtn.disabled = true;
if (this.howToPlayBtn) this.howToPlayBtn.disabled = true;
if (this.homeBtn) this.homeBtn.disabled = true;
if (this.musicToggleStartBtn) this.musicToggleStartBtn.disabled = true;
if (this.musicToggleTopBtn) this.musicToggleTopBtn.disabled = true;
if (this.challengeOkBtn) this.challengeOkBtn.disabled = true;
if (this.challengeCloseBtn) this.challengeCloseBtn.disabled = true;
if (this.gameOverCloseBtn) this.gameOverCloseBtn.disabled = true;
}
restoreControlsAfterGameOver() {
if (this.howToPlayBtn) this.howToPlayBtn.disabled = false;
if (this.homeBtn) this.homeBtn.disabled = false;
if (this.musicToggleStartBtn) this.musicToggleStartBtn.disabled = false;
if (this.musicToggleTopBtn) this.musicToggleTopBtn.disabled = false;
if (this.challengeOkBtn) this.challengeOkBtn.disabled = false;
if (this.challengeCloseBtn) this.challengeCloseBtn.disabled = false;
if (this.gameOverCloseBtn) this.gameOverCloseBtn.disabled = false;
this.updateHintsDisplay();
}
showHowToPlayModal() {
if (this.howToPlayOverlay) {
this.howToPlayOverlay.classList.remove('hidden');
this.howToPlayOverlay.setAttribute('aria-hidden', 'false');
}
}
hideHowToPlayModal() {
if (this.howToPlayOverlay) {
this.howToPlayOverlay.classList.add('hidden');
this.howToPlayOverlay.setAttribute('aria-hidden', 'true');
}
}
showLeaderboardModal() {
if (this.leaderboardOverlay) {
this.leaderboardOverlay.classList.remove('hidden');
this.leaderboardOverlay.setAttribute('aria-hidden', 'false');
}
}
hideLeaderboardModal() {
if (this.leaderboardOverlay) {
this.leaderboardOverlay.classList.add('hidden');
this.leaderboardOverlay.setAttribute('aria-hidden', 'true');
}
}
showChallengeIntroModal() {
if (this.challengeIntroOverlay) {
this.challengeIntroOverlay.classList.remove('hidden');
this.challengeIntroOverlay.setAttribute('aria-hidden', 'false');
}
}
hideChallengeIntroModal() {
if (this.challengeIntroOverlay) {
this.challengeIntroOverlay.classList.add('hidden');
this.challengeIntroOverlay.setAttribute('aria-hidden', 'true');
}
}
showChallengePanel() {
if (this.challengePanel) {
this.challengePanel.classList.remove('hidden');
}
if (this.memoryContainer) {
this.memoryContainer.classList.add('challenge-mode');
}
}
hideChallengePanel() {
if (this.challengePanel) {
this.challengePanel.classList.add('hidden');
}
if (this.memoryContainer) {
this.memoryContainer.classList.remove('challenge-mode');
}
}
getChallengeIntroCopy() {
const title = String(this.state.mode_popup_title || this.blueprint.mode_popup_title || this.state.challenge_modal_title || 'Challenge Level').trim();
const message = String(this.state.mode_popup_message || this.blueprint.mode_popup_message || this.state.challenge_modal_message || '').trim();
return {
title,
message: message || 'Match the cards to continue.',
};
}
renderChallengePanel() {
if (!this.challengePanel) return;
const theme = String(this.state.theme || this.blueprint.theme || '').trim();
const modeLabel = String(this.state.level_mode || this.blueprint.level_mode || 'normal').trim();
const title = String(this.state.mode_popup_title || this.blueprint.mode_popup_title || theme || 'Challenge').trim();
const focus = String(this.state.mode_popup_message || this.blueprint.mode_popup_message || this.state.featured_fact || this.blueprint.featured_fact || theme || '').trim();
if (this.challengeThemeEl) {
this.challengeThemeEl.textContent = modeLabel ? `Mode: ${modeLabel.replace(/_/g, ' ')}` : '';
}
if (this.challengeQuestionEl) {
this.challengeQuestionEl.textContent = title;
}
if (this.challengeFocusEl) {
this.challengeFocusEl.textContent = focus;
}
if (this.challengeOptionsEl) {
this.challengeOptionsEl.innerHTML = '';
}
}
enableChallengeOptions(enabled) {
if (!this.challengeOptionsEl) return;
this.challengeOptionsEl.querySelectorAll('.challenge-option').forEach((button) => {
button.disabled = !enabled || this.state.status === 'complete' || this.state.game_over || this.challengeSelectionLocked;
});
}
setChallengeOptionState(selectedIndex, correctIndex, isCorrect) {
if (!this.challengeOptionsEl) return;
const buttons = Array.from(this.challengeOptionsEl.querySelectorAll('.challenge-option'));
const lockAll = this.challengeSelectionLocked || this.state.status === 'complete' || this.state.game_over || Boolean(this.state.challenge_result);
buttons.forEach((button) => {
const index = Number(button.dataset.index);
button.classList.remove('correct-choice', 'wrong-choice', 'reveal-correct');
if (index === correctIndex) {
button.classList.add('reveal-correct');
}
if (index === selectedIndex) {
button.classList.add(isCorrect ? 'correct-choice' : 'wrong-choice');
}
button.disabled = lockAll || (isCorrect && index !== correctIndex);
});
}
async openChallengeQuestion() {
this.hideChallengeIntroModal();
await this.startChallengePreview();
}
async handleChallengeChoice(button) {
if (this.state.level_type !== 'challenge') return;
if (!this.challengeStarted || this.challengeSelectionLocked || this.state.status === 'complete') return;
const selectedIndex = Number(button.dataset.index);
if (Number.isNaN(selectedIndex)) return;
const correctIndex = Number(this.challengeCorrectIndex);
const isCorrect = selectedIndex === correctIndex;
this.state.challenge_selected_index = selectedIndex;
this.challengeResultIndex = selectedIndex;
this.challengeSelectionLocked = true;
this.state.challenge_result = isCorrect ? 'correct' : 'wrong';
this.state.level_complete = true;
this.state.previous_level_completed = true;
this.state.status = 'complete';
this.state.performance_meter = isCorrect ? 0 : 100;
this.state.challenge_due = false;
this.setChallengeOptionState(selectedIndex, correctIndex, isCorrect);
if (isCorrect) {
this.state.win_streak = Number(this.state.win_streak || 0) + 1;
this.state.lives = Number(this.state.lives || this.lives || MAX_LIVES) + 1;
this.playCorrectSound();
this.lives = Number(this.state.lives);
this.state.performance_rating = this.getCompletionBadgeText();
this.state.performance_label = this.getCompletionRatingLabel();
this.updateHud();
this.setStatus(
this.state.victory_message || 'Great answer! You cleared the challenge.',
'win',
`Correct: ${String(this.challengeOptions[correctIndex] || '').trim()}`
);
this.setBadge('COMPLETE', 'complete', `${this.getCompletionRatingLabel()} ${this.getCompletionBadgeText()}`);
if (this.nextBtn) this.nextBtn.disabled = false;
this.enableChallengeOptions(false);
await this.syncBackendEvent('challenge');
return;
}
this.setStatus(
this.state.failure_message || 'Not quite. Try again.',
'error',
`Correct answer: ${String(this.challengeOptions[correctIndex] || '').trim()}`
);
this.playWrongSound();
if (this.nextBtn) this.nextBtn.disabled = false;
this.enableChallengeOptions(false);
await this.syncBackendEvent('challenge');
}
showStartOverlay() {
if (this.startOverlay) {
this.startOverlay.classList.remove('hidden');
this.startOverlay.setAttribute('aria-hidden', 'false');
}
}
hideStartOverlay() {
if (this.startOverlay) {
this.startOverlay.classList.add('hidden');
this.startOverlay.setAttribute('aria-hidden', 'true');
}
}
showTransitionOverlay(title, subtitle) {
if (this.transitionHideTimer) {
clearTimeout(this.transitionHideTimer);
this.transitionHideTimer = null;
}
this.transitionShownAt = performance.now ? performance.now() : Date.now();
if (this.transitionTitle) this.transitionTitle.textContent = title;
if (this.transitionSubtitle) this.transitionSubtitle.textContent = subtitle;
if (this.transitionOverlay) {
this.transitionOverlay.classList.remove('hidden');
this.transitionOverlay.setAttribute('aria-hidden', 'false');
}
}
hideTransitionOverlay(force = false) {
const now = performance.now ? performance.now() : Date.now();
const elapsed = now - (this.transitionShownAt || 0);
if (!force && elapsed < MIN_TRANSITION_VISIBLE_MS) {
if (this.transitionHideTimer) {
clearTimeout(this.transitionHideTimer);
}
this.transitionHideTimer = setTimeout(() => {
this.transitionHideTimer = null;
this.hideTransitionOverlay(true);
}, MIN_TRANSITION_VISIBLE_MS - elapsed);
return;
}
if (this.transitionHideTimer) {
clearTimeout(this.transitionHideTimer);
this.transitionHideTimer = null;
}
if (this.transitionOverlay) {
this.transitionOverlay.classList.add('hidden');
this.transitionOverlay.setAttribute('aria-hidden', 'true');
}
}
normalizeServerResponse(result) {
if (!result) return null;
if (typeof result === 'string') {
try {
return JSON.parse(result);
} catch (err) {
return null;
}
}
if (typeof result === 'object') {
return result;
}
return null;
}
getFeaturedFact() {
const fact = this.blueprint.featured_fact || this.state.featured_fact || '';
return typeof fact === 'string' ? fact.trim() : '';
}
formatFeaturedFact(fact) {
const text = String(fact || '').trim();
if (!text) return '';
if (/^did you know\b/i.test(text)) {
return text.toUpperCase().startsWith('DID YOU KNOW:') ? text : `DID YOU KNOW: ${text.replace(/^did you know[:\s-]*/i, '').trim()}`;
}
return text.toUpperCase().startsWith('FACT:') ? text : `FACT: ${text}`;
}
escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
getCardDisplayText(card) {
if (card && typeof card === 'object') {
return String(card.display ?? card.value ?? card.text ?? '').trim();
}
return String(card ?? '').trim();
}
getCardMatchKey(card) {
if (card && typeof card === 'object') {
const key = card.match_key ?? card.matchKey ?? card.key ?? card.display ?? card.value ?? card.text ?? '';
return String(key).trim();
}
return String(card ?? '').trim();
}
getCardType(card) {
if (card && typeof card === 'object') {
return String(card.card_type ?? card.type ?? 'emoji').trim();
}
return 'emoji';
}
getCardPairFact(card) {
if (card && typeof card === 'object') {
return String(card.pair_fact ?? card.fact ?? '').trim();
}
return '';
}
shuffleBoardCards(cards) {
const shuffled = Array.isArray(cards) ? [...cards] : [];
for (let i = shuffled.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
if (j !== i) {
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
}
return shuffled;
}
isSpecialChallengeMode() {
return this.state.level_type === 'challenge' && ['fact_match', 'category_match'].includes(String(this.state.level_mode || this.blueprint.level_mode || '').trim());
}
bindActiveLevelState() {
const previousLevelId = this.levelId || '';
this.activeLevel = this.state.active_level || {};
this.blueprint = this.activeLevel.blueprint || {};
this.levelId = this.activeLevel.level_instance_id || this.levelId || '';
this.levelNumber = Number(this.activeLevel.level_number || this.levelNumber || 1);
if (this.levelId && this.levelId !== previousLevelId) {
this.activeLevel.cards = this.shuffleBoardCards(this.activeLevel.cards);
this.challengeStarted = false;
this.challengeSelectionLocked = false;
this.challengeResultIndex = null;
this.challengeCorrectIndex = null;
}
this.cards = Array.isArray(this.activeLevel.cards) ? [...this.activeLevel.cards] : [];
this.state.level = this.levelNumber;
this.state.total_pairs = Number(this.activeLevel.pair_count || this.state.total_pairs || 0);
this.state.grid_rows = Number(this.activeLevel.rows || this.state.grid_rows || 2);
this.state.grid_cols = Number(this.activeLevel.cols || this.state.grid_cols || 2);
this.state.hints = Number.isFinite(Number(this.state.hints)) ? Number(this.state.hints) : this.hints;
this.state.max_hints = Number.isFinite(Number(this.state.max_hints)) ? Number(this.state.max_hints) : this.maxHints;
this.state.max_lives = Number.isFinite(Number(this.state.max_lives)) ? Number(this.state.max_lives) : MAX_LIVES;
this.state.level_title = this.blueprint.level_title || this.state.level_title || '';
this.state.theme = this.blueprint.theme || this.state.theme || '';
this.state.educational_focus = this.blueprint.educational_focus || this.state.educational_focus || '';
this.state.featured_fact_emoji = this.blueprint.featured_fact_emoji || this.state.featured_fact_emoji || '';
this.state.featured_fact = this.blueprint.featured_fact || this.state.featured_fact || '';
this.state.level_type = this.activeLevel.level_type || this.state.level_type || 'normal';
this.state.challenge_modal_title = this.blueprint.challenge_modal_title || this.state.challenge_modal_title || '';
this.state.challenge_modal_message = this.blueprint.challenge_modal_message || this.state.challenge_modal_message || '';
this.state.challenge_question = this.blueprint.challenge_question || this.state.challenge_question || '';
this.state.challenge_options = Array.isArray(this.blueprint.challenge_options) ? [...this.blueprint.challenge_options] : (this.state.challenge_options || []);
this.state.challenge_correct_index = Number.isFinite(Number(this.blueprint.challenge_correct_index))
? Number(this.blueprint.challenge_correct_index)
: this.state.challenge_correct_index;
this.state.level_mode = this.blueprint.level_mode || this.state.level_mode || 'normal';
this.state.mode_popup_title = this.blueprint.mode_popup_title || this.state.mode_popup_title || '';
this.state.mode_popup_message = this.blueprint.mode_popup_message || this.state.mode_popup_message || '';
this.state.match_targets = this.blueprint.match_targets || this.state.match_targets || {};
this.state.pair_facts = this.blueprint.pair_facts || this.state.pair_facts || {};
this.state.victory_message = this.blueprint.victory_message || this.state.victory_message || '';
this.state.failure_message = this.blueprint.failure_message || this.state.failure_message || '';
this.state.grid_advice = this.blueprint.grid_advice || this.state.grid_advice || {};
this.hints = Number.isFinite(Number(this.state.hints)) ? Number(this.state.hints) : this.hints;
this.maxHints = Number.isFinite(Number(this.state.max_hints)) ? Number(this.state.max_hints) : this.maxHints;
this.challengeOptions = Array.isArray(this.state.challenge_options) ? [...this.state.challenge_options] : [];
this.challengeCorrectIndex = Number.isFinite(Number(this.state.challenge_correct_index))
? Number(this.state.challenge_correct_index)
: null;
this.challengeLevelMode = String(this.state.level_mode || this.blueprint.level_mode || 'normal').trim();
}
isChallengeMode() {
return this.state.level_type === 'challenge';
}
updateHud() {
if (this.movesEl) this.movesEl.textContent = String(this.moves);
const matchedPairs = Math.floor(this.matched.length / 2);
if (this.matchesEl) this.matchesEl.textContent = `${matchedPairs} / ${this.state.total_pairs}`;
if (this.scoreEl) this.scoreEl.textContent = String(this.state.score);
const currentMeter = Math.max(0, Math.min(100, Number(this.state.performance_meter || 0)));
const meterChanged = this.lastRenderedPerformanceMeter !== null && currentMeter !== this.lastRenderedPerformanceMeter;
this.updatePerformanceMeter(currentMeter, meterChanged);
this.lastRenderedPerformanceMeter = currentMeter;
const currentLives = Number(this.state.lives || 0);
if (this.livesEl) this.livesEl.textContent = String(currentLives);
const livesBlock = this.livesEl?.closest('.stat-block') || this.livesEl;
if (this.lastRenderedLives !== null && currentLives < this.lastRenderedLives) {
this.pulseHudGlow(livesBlock, 'hud-glow-lives', 'lives');
this.pulseHudGlow(this.livesEl, 'hud-glow-lives', 'livesValue');
} else if (this.lastRenderedLives !== null && currentLives > this.lastRenderedLives) {
this.pulseHudGlow(livesBlock, 'hud-glow-lives-gain', 'lives');
this.pulseHudGlow(this.livesEl, 'hud-glow-lives-gain', 'livesValue');
}
this.lastRenderedLives = currentLives;
this.updateHintsDisplay();
if (this.levelEl) this.levelEl.textContent = String(this.state.level);
if (this.levelTitleEl) this.levelTitleEl.textContent = this.state.level_title || '';
if (this.themeChip) this.themeChip.textContent = this.state.theme || '';
if (this.focusLine) this.focusLine.textContent = this.state.educational_focus || this.state.featured_fact || '';
this.updateLeaderboardDisplay();
}
updateCardSize() {
if (!this.grid) return;
const cols = Math.max(2, Number(this.state.grid_cols || 2));
const rows = Math.max(2, Number(this.state.grid_rows || 2));
const mobileViewport = window.innerWidth <= 760;
const gap = mobileViewport ? (cols > 4 ? 3 : cols > 3 ? 4 : 5) : (cols > 4 ? 8 : cols > 3 ? 10 : 12);
const safeWidth = Math.max(0, window.innerWidth - (mobileViewport ? 22 : 44));
const safeHeight = Math.max(0, window.innerHeight - (mobileViewport ? 320 : 280));
const widthBased = Math.floor((safeWidth - gap * (cols - 1)) / cols);
const heightBased = Math.floor((safeHeight - gap * (rows - 1)) / rows);
const rowPenalty = rows >= 5 ? 0.72 : rows === 4 ? 0.8 : rows === 3 ? 0.9 : 1;
const mobileTightness = mobileViewport ? 0.82 : 1;
const size = Math.max(
mobileViewport ? 42 : 92,
Math.floor(Math.min(widthBased, heightBased) * rowPenalty * mobileTightness)
);
const maxCard = mobileViewport
? (cols <= 2 ? 76 : cols === 3 ? 56 : cols === 4 ? 44 : 34)
: (cols <= 2 ? 160 : cols === 3 ? 150 : cols === 4 ? 132 : 118);
const minCard = mobileViewport
? (cols <= 2 ? 54 : cols === 3 ? 42 : cols === 4 ? 34 : 28)
: (cols <= 2 ? 92 : cols === 3 ? 72 : cols === 4 ? 58 : 44);
const finalSize = Math.max(minCard, Math.min(maxCard, size));
this.grid.style.setProperty('--grid-cols', String(cols));
this.grid.style.setProperty('--grid-rows', String(rows));
this.grid.style.setProperty('--card-fit-size', `${finalSize}px`);
this.grid.style.setProperty('--card-size', `${finalSize}px`);
this.grid.style.setProperty('--grid-gap', `${gap}px`);
}
renderState(nextState, initial = false) {
this.state = {
...this.state,
...nextState,
};
const isPlaying = Boolean(this.state.game_started);
this.root.classList.toggle('game-playing', isPlaying);
if (this.memoryContainer && this.memoryContainer !== this.root) {
this.memoryContainer.classList.toggle('game-playing', isPlaying);
}
this.bindActiveLevelState();
this.resetComboFeedback();
this.flipped = [];
this.matched = [];
this.hintedPairs = new Set();
this.hinting = false;
this.moves = Number(this.state.moves || 0);
this.locked = false;
this.pendingTransition = false;
const isChallenge = this.state.level_type === 'challenge';
this.memoryContainer.classList.toggle('challenge-mode', isChallenge);
if (isChallenge) {
this.state.performance_meter = 100;
this.state.challenge_due = false;
}
if (!isChallenge) {
this.hideChallengeIntroModal();
this.hideChallengePanel();
this.challengeStarted = false;
this.challengeSelectionLocked = false;
this.challengeResultIndex = null;
this.challengeCorrectIndex = null;
} else {
this.challengeStarted = false;
this.challengeSelectionLocked = false;
this.challengeResultIndex = null;
this.challengeCorrectIndex = null;
}
this.stopDistractorSparkles();
this.renderCards();
this.updateHud();
this.updateCardSize();
this.applyStateStyle();
if (!initial) {
const isGameOver = this.state.status === 'game_over' || this.state.game_over;
this.setStatus(
isGameOver
? this.state.failure_message || 'Game over. Press NEW GAME to start again.'
: isChallenge
? this.state.challenge_modal_message || this.state.theme || `Challenge level ${this.state.level} loaded.`
: `Level ${this.state.level} loaded.`,
isGameOver ? 'error' : 'show',
this.state.theme || ''
);
}
}
renderCards() {
if (!this.grid) return;
const html = this.cards.map((card, index) => {
const display = this.getCardDisplayText(card);
const matchKey = this.getCardMatchKey(card);
const cardType = this.getCardType(card);
const isTextCard = cardType === 'text';
const cardClass = isTextCard ? 'text-card' : 'emoji-card';
const pairFact = this.getCardPairFact(card);
const titleAttr = pairFact ? ` title="${this.escapeHtml(pairFact)}"` : '';
return `
${this.escapeHtml(display)}
`;
}).join('');
this.grid.innerHTML = html;
this.grid.querySelectorAll('.memory-card').forEach(card => {
card.classList.add('flipped', 'preview-locked');
});
}
getPreviewShuffleIntervalMs() {
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
// Starts gentle, then gets a little faster as levels progress.
// Lower bound prevents the board from becoming chaotic on later levels.
return Math.max(360, 760 - Math.min(320, (level - 1) * 22));
}
getPreviewShuffleAnimationMs() {
const interval = this.getPreviewShuffleIntervalMs();
// Animation stays slightly shorter than the next shuffle tick.
return Math.max(260, Math.min(560, Math.floor(interval * 0.72)));
}
makeDifferentPreviewOrder(cards) {
const original = [...cards];
const shuffled = [...cards];
for (let i = shuffled.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
// Avoid a weak shuffle where too many cards remain in the same grid cell.
const sameSpotCount = shuffled.reduce((count, card, index) => {
return count + (card === original[index] ? 1 : 0);
}, 0);
if (shuffled.length > 2 && sameSpotCount > Math.floor(shuffled.length * 0.45)) {
shuffled.push(shuffled.shift());
} else if (shuffled.length === 2 && shuffled[0] === original[0]) {
shuffled.reverse();
}
return shuffled;
}
shufflePreviewGridOnce() {
if (!this.grid || this.state.status !== 'preview') return;
const cards = Array.from(this.grid.querySelectorAll('.memory-card.preview-locked:not(.matched)'));
if (cards.length < 2) return;
const firstRects = new Map();
cards.forEach((card) => {
firstRects.set(card, card.getBoundingClientRect());
});
const shuffledCards = this.makeDifferentPreviewOrder(cards);
const fragment = document.createDocumentFragment();
shuffledCards.forEach((card) => {
fragment.appendChild(card);
});
this.grid.appendChild(fragment);
const duration = this.getPreviewShuffleAnimationMs();
this.previewShuffleAnimationMs = duration;
shuffledCards.forEach((card) => {
const first = firstRects.get(card);
const last = card.getBoundingClientRect();
if (!first || !last) return;
const dx = first.left - last.left;
const dy = first.top - last.top;
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
card.getAnimations().forEach((animation) => {
animation.cancel();
});
card.animate(
[
{
transform: `translate(${dx}px, ${dy}px) scale(1.015)`,
zIndex: 5,
},
{
transform: 'translate(0px, 0px) scale(1)',
zIndex: 1,
}
],
{
duration,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
fill: 'both',
}
);
});
}
clearPreviewChallengeEffects() {
this.previewFogTimers.forEach((timer) => clearTimeout(timer));
this.previewFogTimers = [];
this.previewHideTimers.forEach((timer) => clearTimeout(timer));
this.previewHideTimers = [];
const effectRoots = [this.memoryContainer, this.root].filter(Boolean);
effectRoots.forEach((root) => root.classList.remove('preview-fog-blink'));
if (this.grid) {
this.grid.querySelectorAll('.memory-card.preview-hidden-card').forEach((card) => {
card.classList.remove('preview-hidden-card');
});
}
}
triggerPreviewFogBlinkOnce() {
const effectRoot = this.memoryContainer || this.root;
if (!effectRoot || this.state.status !== 'preview') return;
effectRoot.classList.remove('preview-fog-blink');
void effectRoot.offsetWidth;
effectRoot.classList.add('preview-fog-blink');
const timer = setTimeout(() => {
effectRoot.classList.remove('preview-fog-blink');
}, 280);
this.previewFogTimers.push(timer);
}
startPreviewFogChallenge() {
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
if (level < 10) return;
const blinkCount = level >= 18 ? 2 : 1;
const delays = blinkCount >= 2 ? [760, 1760] : [1260];
delays.slice(0, blinkCount).forEach((delay) => {
const timer = setTimeout(() => {
if (window.__activeMemoryLevelId === this.levelId && this.state.status === 'preview') {
this.triggerPreviewFogBlinkOnce();
}
}, delay);
this.previewFogTimers.push(timer);
});
}
getPreviewHideCardCount() {
const completedChallengeLevels = this.getCompletedChallengeLevels();
if (completedChallengeLevels < 1) return 0;
return completedChallengeLevels >= 3 ? 2 : 1;
}
hideRandomPreviewCardsOnce() {
if (!this.grid || this.state.status !== 'preview') return;
const hideCount = this.getPreviewHideCardCount();
if (hideCount <= 0) return;
const cards = Array.from(this.grid.querySelectorAll('.memory-card.preview-locked:not(.matched):not(.preview-hidden-card)'));
if (!cards.length) return;
const shuffled = this.makeDifferentPreviewOrder(cards).slice(0, Math.min(hideCount, cards.length));
shuffled.forEach((card) => {
card.classList.add('preview-hidden-card');
const timer = setTimeout(() => {
card.classList.remove('preview-hidden-card');
}, 650);
this.previewHideTimers.push(timer);
});
}
startPreviewHideChallenge() {
if (this.getPreviewHideCardCount() <= 0) return;
[920, 1880].forEach((delay, index) => {
const timer = setTimeout(() => {
if (window.__activeMemoryLevelId === this.levelId && this.state.status === 'preview') {
// The second pulse only appears once challenge progress is stronger.
if (index === 0 || this.getCompletedChallengeLevels() >= 2) {
this.hideRandomPreviewCardsOnce();
}
}
}, delay);
this.previewHideTimers.push(timer);
});
}
startPreviewDifficultyEffects() {
this.clearPreviewChallengeEffects();
this.startPreviewFogChallenge();
this.startPreviewHideChallenge();
}
startPreviewGridShuffle() {
this.stopPreviewGridShuffle();
if (!this.grid || this.state.status !== 'preview') return;
const interval = this.getPreviewShuffleIntervalMs();
// Start while countdown shows 4.
this.shufflePreviewGridOnce();
this.previewShuffleTimer = setInterval(() => {
if (window.__activeMemoryLevelId !== this.levelId || this.state.status !== 'preview') {
this.stopPreviewGridShuffle();
return;
}
this.shufflePreviewGridOnce();
}, interval);
}
stopPreviewGridShuffle() {
if (this.previewShuffleTimer) {
clearInterval(this.previewShuffleTimer);
this.previewShuffleTimer = null;
}
this.clearPreviewChallengeEffects();
if (!this.grid) return;
this.grid.querySelectorAll('.memory-card').forEach((card) => {
card.getAnimations().forEach((animation) => {
try {
animation.finish();
} catch (err) {
animation.cancel();
}
});
card.style.transform = '';
card.style.zIndex = '';
});
}
shakeCardsDuringPreview() {
if (!this.grid) return;
const allCards = this.grid.querySelectorAll('.memory-card');
allCards.forEach((cardEl, index) => {
cardEl.getAnimations().forEach((animation) => {
animation.cancel();
});
cardEl.animate(
[
{ transform: 'translateX(0px) rotate(0deg)' },
{ transform: 'translateX(-14px) rotate(-4deg)' },
{ transform: 'translateX(14px) rotate(4deg)' },
{ transform: 'translateX(-12px) rotate(-3deg)' },
{ transform: 'translateX(12px) rotate(3deg)' },
{ transform: 'translateX(-8px) rotate(-2deg)' },
{ transform: 'translateX(8px) rotate(2deg)' },
{ transform: 'translateX(0px) rotate(0deg)' }
],
{
duration: 850,
easing: 'ease-in-out',
iterations: 1,
fill: 'both',
delay: index * 70,
}
);
});
}
clearPreviewCardAnimations() {
if (!this.grid) return;
const allCards = this.grid.querySelectorAll('.memory-card');
allCards.forEach((cardEl) => {
cardEl.getAnimations().forEach((animation) => {
animation.cancel();
});
cardEl.style.transform = '';
cardEl.style.zIndex = '';
cardEl.classList.remove('preview-hidden-card', 'distractor-sparkle');
});
}
applyStateStyle() {
const status = this.state.status || 'preview';
if (status === 'playing') {
if (this.state.level_type === 'challenge') {
this.setBadge('CHALLENGE', 'challenge');
} else {
this.setBadge('PLAYING', 'playing');
}
} else if (status === 'complete') {
const ratingLabel = this.state.performance_label || this.getCompletionRatingLabel();
const stars = this.state.performance_rating || this.getCompletionBadgeText();
this.setBadge('COMPLETE', 'complete', `${ratingLabel} ${stars}`);
} else if (status === 'game_over') {
this.setBadge('GAME OVER', 'gameover');
} else {
this.setBadge(this.state.level_type === 'challenge' ? 'CHALLENGE' : 'MEMORIZE', this.state.level_type === 'challenge' ? 'challenge' : '');
}
if (this.nextBtn) {
this.nextBtn.disabled = status !== 'complete';
}
if (status === 'playing') {
this.startDistractorSparkles();
} else {
this.stopDistractorSparkles();
}
this.updateHintsDisplay();
this.syncControlsVisibility();
}
startPreview() {
if (!this.levelId) {
return;
}
if (this.state.level_type === 'challenge') {
this.startChallengeLevel();
return;
}
this.clearPreviewTimer();
this.clearPreviewCardAnimations();
window.__activeMemoryLevelId = this.levelId;
if (window.__memoryGameTimer) {
clearInterval(window.__memoryGameTimer);
window.__memoryGameTimer = null;
}
this.state.status = 'preview';
this.state.level_complete = false;
this.state.game_over = false;
this.recentlyFlippedCardIndexes = [];
this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
this.syncProgressiveHintLimit(true);
this.applyStateStyle();
this.setStatus(
'Memorize the cards before they flip!',
'',
this.state.theme || ''
);
const cards = this.grid ? Array.from(this.grid.querySelectorAll('.memory-card')) : [];
cards.forEach(card => {
card.classList.add('flipped', 'preview-locked');
card.classList.remove('matched', 'wrong');
});
let count = PREVIEW_SECONDS;
if (this.countdown) this.countdown.textContent = String(count);
if (this.overlay) {
this.overlay.classList.remove('hidden');
this.overlay.style.display = 'flex';
this.overlay.style.opacity = '1';
}
this.scrollPreviewIntoView();
setTimeout(() => {
this.shakeCardsDuringPreview();
}, 120);
setTimeout(() => {
this.startPreviewGridShuffle();
this.startPreviewDifficultyEffects();
}, 260);
this.updateHintsDisplay();
this.previewTimer = setInterval(() => {
if (window.__activeMemoryLevelId !== this.levelId) {
this.clearPreviewTimer();
return;
}
count -= 1;
if (this.countdown) this.countdown.textContent = String(Math.max(count, 0));
if (count <= 1) {
this.stopPreviewGridShuffle();
}
if (count <= 0) {
this.clearPreviewTimer();
this.hidePreview();
}
}, 1000);
}
async startChallengePreview() {
this.clearPreviewTimer();
this.clearPreviewCardAnimations();
window.__activeMemoryLevelId = this.levelId;
this.challengeStarted = true;
this.challengeSelectionLocked = false;
this.challengeResultIndex = null;
this.state.challenge_started = true;
this.state.status = 'preview';
this.state.level_complete = false;
this.state.game_over = false;
this.recentlyFlippedCardIndexes = [];
this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
this.syncProgressiveHintLimit(true);
if (this.overlay) {
this.overlay.style.display = 'flex';
this.overlay.style.opacity = '1';
this.overlay.classList.remove('hidden');
}
const cards = this.grid ? Array.from(this.grid.querySelectorAll('.memory-card')) : [];
cards.forEach((card) => {
card.classList.add('flipped', 'preview-locked');
card.classList.remove('matched', 'wrong');
});
let count = PREVIEW_SECONDS;
if (this.countdown) this.countdown.textContent = String(count);
this.scrollPreviewIntoView();
this.applyStateStyle();
this.setStatus(
'Memorize the cards before they flip!',
'',
this.state.theme || this.state.mode_popup_message || this.state.challenge_modal_message || ''
);
this.updateHintsDisplay();
setTimeout(() => {
this.shakeCardsDuringPreview();
}, 120);
setTimeout(() => {
this.startPreviewGridShuffle();
this.startPreviewDifficultyEffects();
}, 260);
this.previewTimer = setInterval(() => {
if (window.__activeMemoryLevelId !== this.levelId) {
this.clearPreviewTimer();
return;
}
count -= 1;
if (this.countdown) this.countdown.textContent = String(Math.max(count, 0));
if (count <= 1) {
this.stopPreviewGridShuffle();
}
if (count <= 0) {
this.clearPreviewTimer();
this.hidePreview();
}
}, 1000);
}
startChallengeLevel() {
this.clearPreviewTimer();
this.clearPreviewCardAnimations();
const introCopy = this.getChallengeIntroCopy();
const modalTitle = introCopy.title || 'Challenge Level';
const modalSubtitle = introCopy.message;
if (this.overlay) {
this.overlay.style.opacity = '0';
setTimeout(() => {
if (this.overlay) this.overlay.style.display = 'none';
}, 420);
}
if (this.grid) {
this.grid.querySelectorAll('.memory-card').forEach((card) => {
card.classList.remove('flipped', 'preview-locked');
});
}
this.scrollPreviewIntoView();
this.challengeCorrectIndex = Number.isFinite(Number(this.state.challenge_correct_index))
? Number(this.state.challenge_correct_index)
: null;
this.renderChallengePanel();
if (this.challengeModalTitleEl) {
this.challengeModalTitleEl.textContent = modalTitle;
}
if (this.challengeModalMessageEl) {
this.challengeModalMessageEl.textContent = modalSubtitle;
}
if (this.challengeModalSubtitleEl) {
this.challengeModalSubtitleEl.textContent = modalSubtitle;
}
const challengeAlreadyStarted = Boolean(this.state.challenge_started);
if (challengeAlreadyStarted) {
this.challengeStarted = true;
this.challengeSelectionLocked = false;
this.hideChallengeIntroModal();
this.hideChallengePanel();
if (this.challengeModalTitleEl) {
this.challengeModalTitleEl.textContent = introCopy.title;
}
if (this.challengeModalMessageEl) {
this.challengeModalMessageEl.textContent = introCopy.message;
}
if (this.state.status === 'complete') {
this.setStatus(
this.state.victory_message || 'Great answer! You cleared the challenge.',
'win',
`Correct: ${String(this.challengeOptions[this.challengeCorrectIndex] || '').trim()}`
);
} else {
this.setStatus(
introCopy.message,
'show',
this.state.theme || ''
);
}
this.applyStateStyle();
return;
}
this.challengeStarted = false;
this.challengeSelectionLocked = false;
this.challengeResultIndex = null;
this.state.status = 'preview';
this.state.level_complete = false;
this.state.game_over = false;
this.hideChallengePanel();
if (this.challengeModalTitleEl) {
this.challengeModalTitleEl.textContent = introCopy.title;
}
if (this.challengeModalMessageEl) {
this.challengeModalMessageEl.textContent = introCopy.message;
}
this.showChallengeIntroModal();
this.scrollPreviewIntoView();
this.setStatus(
introCopy.message,
'show',
this.state.theme || ''
);
this.applyStateStyle();
}
clearPreviewTimer() {
if (this.previewTimer) {
clearInterval(this.previewTimer);
this.previewTimer = null;
}
this.stopPreviewGridShuffle();
}
scrollPreviewIntoView() {
const target = this.state.level_type === 'challenge'
? (this.grid || this.cardsPanel || this.boardFrame)
: (this.grid || this.cardsPanel || this.boardFrame);
if (!target) return;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
try {
target.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
} catch (err) {
const fallbackTarget = target === this.boardFrame ? this.boardFrame : (this.cardsPanel || this.boardFrame);
if (!fallbackTarget) return;
const top = Math.max(
0,
window.scrollY + fallbackTarget.getBoundingClientRect().top - Math.max(12, window.innerHeight * 0.14)
);
window.scrollTo({ top, behavior: 'smooth' });
}
});
});
}
hidePreview() {
if (this.overlay) {
this.overlay.style.opacity = '0';
setTimeout(() => {
if (this.overlay) this.overlay.style.display = 'none';
}, 420);
}
this.clearPreviewCardAnimations();
this.grid.querySelectorAll('.memory-card').forEach((card, idx) => {
if (!this.matched.includes(idx)) {
card.classList.remove('flipped', 'preview-locked');
}
});
if (this.state.level_type === 'challenge') {
this.state.status = 'playing';
this.state.level_complete = false;
this.state.game_over = false;
this.applyStateStyle();
this.updateHintsDisplay();
return;
}
this.state.status = 'playing';
this.levelStartTime = Date.now();
this.applyStateStyle();
this.updateHintsDisplay();
this.setStatus(
'Find matching pairs.',
'show',
this.state.theme || 'Click two cards to test a match.'
);
}
async handleCardClick(card) {
if (this.state.status !== 'playing' || this.locked) return;
const idx = Number(card.dataset.index);
if (this.matched.includes(idx) || this.flipped.includes(idx)) {
return;
}
card.classList.add('flipped');
this.flipped.push(idx);
this.rememberRecentlyFlipped(idx);
if (this.flipped.length === 1) {
this.updateHintsDisplay();
this.setStatus('Pick one more card.', '', this.state.theme || '');
return;
}
if (this.flipped.length < 2) {
return;
}
this.locked = true;
this.moves += 1;
this.state.moves = this.moves;
this.decrementPeekCooldownAfterMove();
this.updateHud();
const [a, b] = this.flipped;
const cardAValue = this.getCardMatchKey(this.cards[a]);
const cardBValue = this.getCardMatchKey(this.cards[b]);
if (cardAValue && cardAValue === cardBValue) {
await this.onMatch(a, b);
} else {
this.onWrong(a, b);
}
}
async onMatch(a, b) {
const cardA = this.grid.querySelector(`[data-index="${a}"]`);
const cardB = this.grid.querySelector(`[data-index="${b}"]`);
if (cardA) cardA.classList.add('matched');
if (cardB) cardB.classList.add('matched');
const pairFact = this.getCardPairFact(this.cards[a]) || this.getCardPairFact(this.cards[b]);
const isChallengeBoard = this.isChallengeMode();
this.matched.push(a, b);
this.flipped = [];
this.state.matched_count = Math.floor(this.matched.length / 2);
this.state.score = Number(this.state.score || 0) + 20;
if (isChallengeBoard) {
this.state.performance_meter = 100;
this.state.challenge_due = false;
} else {
this.state.performance_meter = Math.min(
100,
Number(this.state.performance_meter || 0) + 20
);
this.state.challenge_due = Number(this.state.performance_meter || 0) >= 100;
}
this.comboStreak += 1;
this.state.combo_streak = this.comboStreak;
this.updateHud();
this.playCorrectSound();
if (this.matched.length === this.cards.length) {
const featuredFact = this.formatFeaturedFact(this.getFeaturedFact());
if (isChallengeBoard) {
this.state.lives = Number(this.state.lives || this.lives || MAX_LIVES) + 1;
this.lives = Number(this.state.lives);
this.state.performance_meter = 0;
this.state.challenge_due = false;
}
const effectiveMaxHints = this.getEffectiveMaxHints();
const shouldAwardPeek = this.hints < effectiveMaxHints;
this.showFeedbackOverlay(
'LEVEL COMPLETE',
isChallengeBoard ? 'Level Complete!\n+1 Life!' : (shouldAwardPeek ? 'Level Complete!\n+1 Peek earned!' : 'Level Complete!'),
'level',
false
);
this.state.performance_rating = this.getCompletionBadgeText();
this.state.performance_label = this.getCompletionRatingLabel();
this.state.status = 'complete';
this.state.level_complete = true;
this.state.previous_level_completed = true;
this.state.previous_level_wrong_attempts = Number(this.state.wrong_attempts || 0);
this.state.previous_level_time_seconds = this.levelStartTime
? Math.max(1, Math.round((Date.now() - this.levelStartTime) / 1000))
: Number(this.state.previous_level_time_seconds || 0);
this.state.win_streak = Number(this.state.win_streak || 0) + 1;
this.locked = true;
this.setBadge('COMPLETE', 'complete', `${this.state.performance_label} ${this.state.performance_rating}`);
this.setStatus(
this.state.victory_message || 'Level cleared. Click NEXT LEVEL to continue.',
'win',
pairFact || featuredFact || this.state.theme || `Bonus ready for level ${this.state.level}.`
);
if (!isChallengeBoard) {
this.hints = Math.min(effectiveMaxHints, this.hints + 1);
this.state.hints = this.hints;
}
this.updateHud();
this.applyStateStyle();
if (this.nextBtn) this.nextBtn.disabled = false;
const response = await this.syncBackendEvent('complete');
if (response && response.state) {
this.state = {
...this.state,
...response.state,
};
this.bindActiveLevelState();
this.updateHud();
}
const hintMessage = shouldAwardPeek
? '+1 peek earned!'
: `Max peeks earned for this level: ${this.hints} / ${effectiveMaxHints}`;
const completionDetails = [featuredFact, hintMessage].filter(Boolean).join('\n');
this.setStatus(
this.state.victory_message || 'Level cleared. Click NEXT LEVEL to continue.',
'win',
completionDetails || hintMessage
);
return;
}
if (this.comboStreak >= 2) {
this.showComboFeedback();
} else {
this.triggerPopup('Matched!', 'pop-good-runtime');
}
this.setStatus('Match found.', 'show', pairFact || this.state.theme || '');
this.locked = false;
}
onWrong(a, b) {
const cardA = this.grid.querySelector(`[data-index="${a}"]`);
const cardB = this.grid.querySelector(`[data-index="${b}"]`);
if (cardA) cardA.classList.add('wrong');
if (cardB) cardB.classList.add('wrong');
this.resetComboFeedback();
this.clearWrongMatchTimer();
this.locked = true;
const wrongFlipBackDelay = this.getWrongFlipBackDelayMs();
this.wrongMatchTimer = window.setTimeout(() => {
try {
[a, b].forEach((index) => {
const card = this.grid ? this.grid.querySelector(`[data-index="${index}"]`) : null;
if (card) {
card.classList.remove('flipped', 'wrong');
}
});
} finally {
this.flipped = [];
this.locked = false;
this.wrongMatchTimer = null;
if (this.state.lives <= 0) {
this.gameOver();
}
}
}, wrongFlipBackDelay);
try {
this.showFeedbackOverlay('WRONG MATCH', 'Lost 1 live', 'negative', false);
this.playWrongSound();
this.state.wrong_attempts = Number(this.state.wrong_attempts || 0) + 1;
this.state.score = Math.max(0, Number(this.state.score || 0) - 10);
if (this.isChallengeMode()) {
this.state.performance_meter = 100;
this.state.challenge_due = false;
} else {
this.state.performance_meter = Math.max(
0,
Number(this.state.performance_meter || 0) - 20
);
this.state.challenge_due = Number(this.state.performance_meter || 0) >= 100;
}
this.state.previous_level_completed = false;
this.updateHud();
this.setStatus(
this.state.failure_message || 'No match. Try again.',
'error',
this.state.theme || `Wrong attempts: ${this.state.wrong_attempts}`
);
} catch (err) {
console.error('onWrong UI update failed', err);
}
void this.syncBackendEvent('wrong');
}
async useHint() {
if (this.state.status !== 'playing') return;
this.syncProgressiveHintLimit(false);
if (this.hinting) return;
if (this.peekCooldownMoves > 0) {
this.setStatus('Peek is cooling down.', 'error', `${this.peekCooldownMoves} move${this.peekCooldownMoves === 1 ? '' : 's'} left before next peek.`);
this.updateHintsDisplay();
return;
}
if (this.hints <= 0) {
this.setStatus('No peeks left!', 'error', '');
this.updateHintsDisplay();
return;
}
if (this.flipped.length > 0) return;
this.resetComboFeedback();
const pair = this.findUnmatchedPairForHint();
if (!pair) {
this.setStatus('No peek available!', 'error', '');
return;
}
this.hints = Math.max(0, this.hints - 1);
this.state.hints = this.hints;
this.hinting = true;
this.locked = true;
this.updateHintsDisplay();
const [a, b] = pair;
const cardA = this.grid.querySelector(`[data-index="${a}"]`);
const cardB = this.grid.querySelector(`[data-index="${b}"]`);
if (cardA) {
cardA.classList.add('flipped', 'hint-peek');
}
if (cardB) {
cardB.classList.add('flipped', 'hint-peek');
}
this.setStatus('๐ Peek revealed a pair!', 'show', '');
setTimeout(async () => {
if (!this.matched.includes(a) && cardA) {
cardA.classList.remove('flipped', 'hint-peek');
}
if (!this.matched.includes(b) && cardB) {
cardB.classList.remove('flipped', 'hint-peek');
}
this.hinting = false;
this.locked = false;
this.peekCooldownMoves = this.getHintCooldownLengthMoves();
this.state.peek_cooldown_moves = this.peekCooldownMoves;
this.updateHintsDisplay();
await this.syncBackendEvent('hint');
}, 1200);
}
findUnmatchedPairForHint() {
const groups = new Map();
this.cards.forEach((emoji, idx) => {
if (this.matched.includes(idx)) return;
if (!groups.has(emoji)) {
groups.set(emoji, []);
}
groups.get(emoji).push(idx);
});
const candidates = [...groups.entries()].filter(([, indices]) => indices.length >= 2);
if (!candidates.length) return null;
const fresh = candidates.find(([emoji, indices]) => {
if (this.hintedPairs.has(emoji)) return false;
return indices.every((index) => !this.flipped.includes(index));
});
const selected = fresh || candidates.find(([, indices]) => indices.every((index) => !this.flipped.includes(index))) || candidates[0];
if (!selected) return null;
this.hintedPairs.add(selected[0]);
return selected[1].slice(0, 2);
}
gameOver() {
this.state.status = 'game_over';
this.state.game_over = true;
this.state.previous_level_completed = false;
this.state.previous_level_wrong_attempts = Number(this.state.wrong_attempts || 0);
this.state.previous_level_time_seconds = this.levelStartTime
? Math.max(1, Math.round((Date.now() - this.levelStartTime) / 1000))
: Number(this.state.previous_level_time_seconds || 0);
this.state.win_streak = 0;
this.resetComboFeedback();
this.stopDistractorSparkles();
this.locked = true;
this.applyStateStyle();
this.setStatus(
this.state.failure_message || 'Game over. Press NEW GAME to try again.',
'error',
this.state.theme || 'You used all 5 lives.'
);
if (this.nextBtn) this.nextBtn.disabled = true;
this.clearPreviewTimer();
this.showGameOverModal(this.state.score || 0);
this.updateHintsDisplay();
this.scheduleLeaderboardRefresh(0);
}
async syncBackendEvent(kind) {
try {
let response = null;
if (kind === 'wrong') {
response = this.normalizeServerResponse(await server.on_wrong_match(JSON.stringify(this.snapshot())));
} else if (kind === 'complete') {
response = this.normalizeServerResponse(await server.on_level_complete(JSON.stringify(this.snapshot())));
} else if (kind === 'hint') {
response = this.normalizeServerResponse(await server.on_hint_used(JSON.stringify(this.snapshot())));
} else if (kind === 'challenge') {
response = this.normalizeServerResponse(await server.on_challenge_answer(JSON.stringify(this.snapshot())));
}
const nextState = response && (response.state || response);
if (nextState && (nextState.game_id || nextState.active_level || nextState.lives !== undefined)) {
this.state = {
...this.state,
...nextState,
};
this.bindActiveLevelState();
this.updateHud();
if (this.state.level_type === 'challenge') {
this.renderChallengePanel();
if (this.state.challenge_selected_index !== undefined && this.state.challenge_selected_index !== null) {
const isCorrect = this.state.challenge_result === 'correct';
this.setChallengeOptionState(
Number(this.state.challenge_selected_index),
Number(this.challengeCorrectIndex),
isCorrect
);
}
}
if (this.state.game_over || this.state.status === 'game_over') {
this.showGameOverModal(this.state.score || 0);
this.scheduleLeaderboardRefresh(0);
}
this.applyStateStyle();
}
return response;
} catch (err) {
return null;
}
return null;
}
async mountActiveLevelAndPreview(nextState, showPreparingOverlay = false) {
if (showPreparingOverlay) {
this.showTransitionOverlay('๐ค Preparing Next Level', 'Building the new board and loading the next challenge...');
}
try {
this.renderState(nextState);
this.hideTransitionOverlay(true);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.hintedPairs = new Set();
this.hinting = false;
this.startPreview();
});
});
} catch (err) {
console.error('mountActiveLevelAndPreview failed', err);
this.hideTransitionOverlay(true);
this.pendingTransition = false;
this.locked = false;
this.setStatus(
'Could not load the next level.',
'error',
'Please try again.'
);
}
}
async advanceLevel() {
if (this.pendingTransition || this.state.status !== 'complete') return;
this.pendingTransition = true;
this.locked = true;
this.showTransitionOverlay('๐ค Preparing Next Level', 'Loading your next challenge...');
if (this.nextBtn) this.nextBtn.disabled = true;
try {
const response = this.normalizeServerResponse(
await server.on_next_level_click(JSON.stringify(this.snapshot()))
);
if (!response) {
this.hideTransitionOverlay(true);
return;
}
if (response.ui_action === 'load_level_and_start_preview' && response.state) {
await this.mountActiveLevelAndPreview(response.state, true);
} else {
this.hideTransitionOverlay(true);
this.setStatus(
'Could not load the next level.',
'error',
'The server returned an unexpected response.'
);
}
} catch (err) {
console.error('on_next_level_click failed', err);
this.setStatus('Could not load the next level.', 'error', 'The local generator fell back or failed.');
this.hideTransitionOverlay();
} finally {
this.pendingTransition = false;
}
}
async startGame() {
if (this.pendingTransition) return;
this.pendingTransition = true;
this.sessionHash = this.sessionHash || ((window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`);
if (this.startGameBtn) {
this.startGameBtn.disabled = true;
this.startGameBtn.textContent = 'Starting...';
}
if (this.startNote) {
this.startNote.textContent = 'Generating your first level...';
}
try {
const startResponse = this.normalizeServerResponse(
await server.start_game(JSON.stringify({
session_hash: this.sessionHash,
game_id: this.sessionHash,
}))
);
const startState = startResponse?.state || startResponse;
if (!startState) {
throw new Error(startResponse?.error || 'Could not create session');
}
this.sessionHash = String(startState.game_id || this.sessionHash);
const nextUrl = new URL(window.location.href);
nextUrl.pathname = '/level';
nextUrl.searchParams.set('session_hash', this.sessionHash);
window.location.assign(nextUrl.toString());
} catch (err) {
console.error('start_game failed', err);
if (this.startNote) {
this.startNote.textContent = 'Could not start the game. Please try again.';
}
} finally {
this.pendingTransition = false;
if (this.startGameBtn) {
this.startGameBtn.disabled = false;
this.startGameBtn.textContent = 'Start';
}
}
}
async resetGame() {
if (this.pendingTransition) return;
this.pendingTransition = true;
this.locked = true;
await this.refreshLeaderboardData();
this.hideResetConfirm();
this.hideGameOverModal();
this.stopDistractorSparkles();
this.showTransitionOverlay('๐ค Starting New Game', 'Resetting the board and preparing a fresh run...');
if (this.nextBtn) this.nextBtn.disabled = true;
try {
const freshState = this.normalizeServerResponse(await server.reset_game());
if (freshState) {
this.renderState(freshState, true);
this.hints = Number(freshState.hints || 1);
this.maxHints = Number(freshState.max_hints || 5);
this.hintedPairs = new Set();
this.hinting = false;
this.updateHintsDisplay();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this.hideTransitionOverlay(true);
requestAnimationFrame(() => {
this.startPreview();
});
});
});
}
} catch (err) {
console.error('reset_game failed', err);
this.hideTransitionOverlay();
} finally {
this.pendingTransition = false;
}
}
}
new MatchWiseApp();
"""
JS = JS.replace("__LOGO_DATA_URI__", LOGO_DATA_URI)
# =========================================================
# BUILD DEMO
# =========================================================
GLOBAL_SHELL_CSS = """
html, body {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
background: #040714 !important;
color-scheme: dark;
}
.gradio-container {
width: 100vw !important;
min-height: 100vh;
background: #040714 !important;
color-scheme: dark;
}
.gradio-container nav,
.gradio-container header {
display: none !important;
border: none !important;
box-shadow: none !important;
}
footer {
display: none !important;
}
#hf-login-button {
position: fixed !important;
top: 16px;
right: 16px;
z-index: 1200;
width: auto !important;
min-width: 0 !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
}
.top-utility-bar #hf-login-button {
position: static !important;
top: auto !important;
right: auto !important;
z-index: auto;
display: inline-flex !important;
}
#hf-login-button :is(button, a) {
min-height: 38px !important;
padding: 8px 12px !important;
border-radius: 999px !important;
font-size: 13px !important;
line-height: 1 !important;
white-space: nowrap !important;
}
#game-shell,
.gradio-container .main {
background: #040714 !important;
border-top: none !important;
box-shadow: none !important;
}
@media (max-width: 640px) {
#hf-login-button {
top: 10px;
right: 10px;
transform: scale(0.9);
transform-origin: top right;
}
#hf-login-button :is(button, a) {
min-height: 34px !important;
padding: 7px 10px !important;
font-size: 12px !important;
}
}
"""
GLOBAL_SHELL_HEAD = """
"""
DARK_THEME = gr.themes.Base().set(
body_background_fill="#040714",
body_background_fill_dark="#040714",
body_text_color="#f8fafc",
body_text_color_dark="#f8fafc",
body_text_color_subdued="#cbd5e1",
body_text_color_subdued_dark="#cbd5e1",
)
def build_demo() -> gr.Blocks:
initial_state = build_shell_state()
with gr.Blocks(title="MatchWise", fill_width=True) as demo:
gr.Navbar(visible=False)
gr.LoginButton(
value="Sign in with HF",
logout_value="Logout ({})",
elem_id="hf-login-button",
)
gr.HTML(
value="",
html_template=START_HTML,
css_template=CSS,
js_on_load=JS,
elem_id="game-shell",
server_functions=[
start_game,
get_leaderboard_data,
],
state_json=json.dumps(public_game_state(initial_state), ensure_ascii=False),
game_id=initial_state["game_id"],
active_level_json=json.dumps(initial_state["active_level"], ensure_ascii=False),
generation_status_json=json.dumps(initial_state["generation_status"], ensure_ascii=False),
)
with demo.route("Level", "/level", show_in_navbar=False):
gr.Navbar(visible=False)
gr.LoginButton(
value="Sign in with HF",
logout_value="Logout ({})",
elem_id="hf-login-button",
)
gr.HTML(
value="",
html_template=LEVEL_HTML,
css_template=CSS,
js_on_load=JS,
elem_id="game-shell",
server_functions=[
load_level_state,
sync_state,
on_wrong_match,
on_level_complete,
on_hint_used,
on_challenge_answer,
on_next_level_click,
reset_game,
get_leaderboard_data,
],
state_json=json.dumps(public_game_state(initial_state), ensure_ascii=False),
game_id=initial_state["game_id"],
level=initial_state["active_level"]["level_number"],
score=initial_state["score"],
performance_meter=initial_state["performance_meter"],
lives=initial_state["lives"],
max_lives=initial_state["max_lives"],
hints=initial_state["hints"],
max_hints=initial_state["max_hints"],
total_pairs=initial_state["active_level"]["pair_count"],
grid_cols=initial_state["active_level"]["cols"],
grid_rows=initial_state["active_level"]["rows"],
card_size=initial_state["active_level"]["rows"] <= 2 and 160 or initial_state["active_level"]["rows"] <= 3 and 150 or 132,
level_title=initial_state["active_level"]["blueprint"]["level_title"],
theme=initial_state["active_level"]["blueprint"]["theme"],
educational_focus=initial_state["active_level"]["blueprint"]["educational_focus"],
victory_message=initial_state["active_level"]["blueprint"]["victory_message"],
failure_message=initial_state["active_level"]["blueprint"]["failure_message"],
active_level_json=json.dumps(initial_state["active_level"], ensure_ascii=False),
generation_status_json=json.dumps(initial_state["generation_status"], ensure_ascii=False),
)
return demo
demo: gr.Blocks | None = None
def main() -> None:
global demo
bootstrap_llama_runtime()
demo = build_demo()
demo.queue(default_concurrency_limit=1, max_size=20)
demo.launch(server_name="0.0.0.0", ssr_mode=False, css=GLOBAL_SHELL_CSS, theme=DARK_THEME, head=GLOBAL_SHELL_HEAD)
if __name__ == "__main__":
main()