soroban-abacus-flashcards/test_sectioned_layout.html

8300 lines
291 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Soroban Flashcards</title>
<style>
body {
font-family:
DejaVu Sans,
sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Navigation Styles */
.page-nav {
position: sticky;
top: 0;
background: #fff;
border-bottom: 2px solid #e9ecef;
z-index: 1000;
margin: -20px -20px 30px -20px;
padding: 15px 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.nav-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.nav-btn {
padding: 10px 20px;
border: 2px solid #2c5f76;
background: white;
color: #2c5f76;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
text-decoration: none;
}
.nav-btn:hover {
background: #f8f9fa;
transform: translateY(-1px);
}
.nav-btn.active {
background: #2c5f76;
color: white;
}
/* Section Styles */
.page-section {
display: none;
animation: fadeIn 0.3s ease-in-out;
}
.page-section.active {
display: block;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-header {
text-align: center;
margin-bottom: 30px;
}
.section-title {
font-size: 2rem;
color: #2c5f76;
margin: 0 0 10px 0;
}
.section-subtitle {
color: #666;
font-size: 1.1rem;
margin: 0;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1.2em;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin: 20px 0;
}
.flashcard {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
cursor: pointer;
position: relative;
min-height: 220px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flashcard:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
.abacus-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
margin: 10px 0;
max-width: 100%;
overflow: hidden;
}
.abacus-container svg {
max-width: 100%;
height: auto;
}
.numeral {
font-size: 48pt;
font-weight: bold;
color: #333;
opacity: 0;
transition: opacity 0.3s ease;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 15px 25px;
border-radius: 8px;
border: 2px solid #ddd;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 10;
}
.flashcard:hover .numeral {
opacity: 1;
}
.card-number {
position: absolute;
top: 10px;
left: 10px;
font-size: 0.8em;
color: #999;
background: rgba(255, 255, 255, 0.8);
padding: 2px 6px;
border-radius: 4px;
}
.instructions {
text-align: center;
margin: 30px 0;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.instructions h3 {
color: #333;
margin-bottom: 10px;
}
.instructions p {
color: #666;
line-height: 1.5;
}
.stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.stats div {
background: #f8f9fa;
padding: 10px 15px;
border-radius: 6px;
font-size: 0.9em;
}
@media (max-width: 768px) {
.cards-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
}
.flashcard {
min-height: 120px;
padding: 15px;
}
.numeral {
font-size: calc(48pt * 0.8);
padding: 10px 20px;
}
}
/* Quiz Styling */
.quiz-section {
background: #f8f9fa;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
text-align: center;
}
.quiz-section h2 {
color: #333;
margin-bottom: 10px;
}
.quiz-controls {
max-width: 600px;
margin: 0 auto;
}
.control-group {
margin: 20px 0;
}
.control-group label {
display: block;
font-weight: bold;
margin-bottom: 10px;
color: #555;
}
.count-buttons {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.count-btn {
background: white;
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 16px;
}
.count-btn:hover {
border-color: #4a90e2;
background: #f0f7ff;
}
.count-btn.active {
background: #4a90e2;
color: white;
border-color: #4a90e2;
}
.slider-container {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.slider-container input[type="range"] {
width: 200px;
height: 6px;
border-radius: 3px;
background: #ddd;
outline: none;
-webkit-appearance: none;
}
.slider-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #4a90e2;
cursor: pointer;
}
.slider-value {
font-weight: bold;
color: #4a90e2;
min-width: 50px;
}
.quiz-start-btn {
background: #28a745;
color: white;
border: none;
border-radius: 8px;
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s ease;
margin-top: 20px;
}
.quiz-start-btn:hover {
background: #218838;
}
/* Quiz Game Area */
.quiz-game {
text-align: center;
padding: 40px 20px;
}
.quiz-progress {
margin-bottom: 30px;
}
.progress-bar {
width: 100%;
max-width: 400px;
height: 8px;
background: #ddd;
border-radius: 4px;
margin: 0 auto 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #4a90e2;
transition: width 0.3s ease;
width: 0%;
}
.progress-text {
color: #666;
font-size: 16px;
}
.quiz-display {
position: relative;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.quiz-flashcard {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
width: min(85vw, 700px);
height: min(50vh, 400px);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
transition: transform 0.3s ease;
}
/* Ensure quiz game section fits in viewport */
#quiz-game {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
/* Responsive adjustments for smaller screens */
@media (max-height: 600px) {
.quiz-flashcard {
height: min(40vh, 300px);
padding: 15px;
}
}
@media (max-height: 500px) {
.quiz-flashcard {
height: min(35vh, 250px);
padding: 10px;
}
#quiz-game {
min-height: auto;
padding: 10px;
}
}
.quiz-flashcard svg {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.quiz-flashcard.pulse {
transform: scale(1.05);
}
.quiz-flashcard.card-exit-warning {
border-color: #dc3545;
box-shadow: 0 0 15px rgba(220, 53, 69, 0.4);
}
.quiz-flashcard.card-fade-out {
opacity: 0.3;
transform: scale(0.95);
transition: all 0.1s ease;
}
.countdown {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48px;
font-weight: bold;
color: #4a90e2;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.countdown.ready {
color: #28a745;
}
.countdown.go {
color: #ffc107;
}
.countdown.new-card-flash {
color: #17a2b8;
font-size: 24px;
animation: flashIn 0.15s ease;
}
@keyframes flashIn {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
/* Quiz Input */
.quiz-input {
text-align: center;
padding: 40px 20px;
max-width: 700px;
margin: 0 auto;
}
.quiz-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.stats-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.stats-item span:last-child {
font-size: 24px;
font-weight: bold;
color: #2c5f76;
}
.smart-input-container {
position: relative;
margin: 40px 0;
text-align: center;
}
.smart-input-prompt {
font-size: 16px;
color: #7a8695;
margin-bottom: 15px;
font-weight: normal;
}
.number-display {
min-height: 60px;
padding: 20px;
font-size: 32px;
font-family: "Courier New", "Monaco", monospace;
text-align: center;
font-weight: bold;
color: #2c3e50;
letter-spacing: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.current-typing {
display: inline-block;
transition: all 0.3s ease;
}
.number-display.correct .current-typing {
color: #28a745;
animation: successPulse 0.5s ease;
}
.number-display.incorrect .current-typing {
color: #dc3545;
animation: errorShake 0.5s ease;
}
@keyframes successPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
@keyframes errorShake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
.input-feedback {
margin-top: 10px;
font-weight: bold;
min-height: 20px;
}
.input-feedback.success {
color: #28a745;
}
.input-feedback.error {
color: #dc3545;
}
.found-numbers {
margin: 30px 0;
min-height: 60px;
}
.found-number {
display: inline-block;
margin: 5px;
padding: 10px 15px;
background: #28a745;
color: white;
border-radius: 8px;
font-weight: bold;
font-size: 18px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.finish-btn {
background: #2c5f76;
color: white;
border: none;
padding: 15px 30px;
font-size: 18px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
}
.finish-btn:hover {
background: #1e4a5c;
transform: translateY(-2px);
}
.quiz-finish-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
flex-wrap: wrap;
}
.give-up-btn {
background: #17a2b8;
color: white;
border: none;
padding: 15px 30px;
font-size: 18px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.give-up-btn:hover {
background: #138496;
transform: translateY(-2px);
}
.input-container {
margin-top: 20px;
}
.quiz-input textarea {
width: 100%;
max-width: 400px;
height: 120px;
border: 2px solid #ddd;
border-radius: 8px;
padding: 15px;
font-size: 16px;
resize: vertical;
font-family: inherit;
}
.quiz-input textarea:focus {
outline: none;
border-color: #4a90e2;
}
.quiz-input button {
background: #4a90e2;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
margin-top: 15px;
}
.quiz-input button:hover {
background: #357abd;
}
/* Quiz Results */
.quiz-results {
text-align: center;
padding: 40px 20px;
max-width: 700px;
margin: 0 auto;
}
.score-display {
display: flex;
justify-content: center;
align-items: center;
gap: 40px;
margin: 30px 0;
flex-wrap: wrap;
}
.score-circle {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90e2, #357abd);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: bold;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.score-details {
text-align: left;
}
.score-details p {
margin: 8px 0;
font-size: 16px;
}
.results-breakdown {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 30px 0;
text-align: left;
}
.result-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.result-item:last-child {
border-bottom: none;
}
.result-correct {
color: #28a745;
font-weight: bold;
}
.result-incorrect {
color: #dc3545;
font-weight: bold;
}
.quiz-actions {
margin-top: 30px;
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.quiz-actions button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s ease;
}
#retry-quiz {
background: #ffc107;
color: #333;
}
#retry-quiz:hover {
background: #e0a800;
}
#back-to-cards {
background: #6c757d;
color: white;
}
#back-to-cards:hover {
background: #545b62;
}
/* End Game Button */
.end-game-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s ease;
}
.end-game-btn:hover {
background: #c82333;
}
/* Quiz Header for Game Mode */
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px 20px;
background: rgba(74, 144, 226, 0.1);
border-radius: 8px;
}
.quiz-header .quiz-progress {
flex: 1;
}
@media (max-width: 768px) {
.quiz-section {
padding: 20px;
}
.count-buttons {
gap: 8px;
}
.count-btn {
padding: 8px 16px;
font-size: 14px;
}
.score-display {
flex-direction: column;
gap: 20px;
}
.score-details {
text-align: center;
}
.countdown {
font-size: 36px;
}
.sorting-section {
padding: 20px;
}
.sorting-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
padding: 15px;
}
.sort-card {
padding: 10px;
}
.sort-count-btn {
padding: 8px 16px;
font-size: 14px;
margin: 2px;
}
.sort-start-btn,
.sort-check-btn,
.sort-reveal-btn,
.sort-new-btn {
padding: 10px 16px;
font-size: 14px;
margin: 5px;
}
.sorting-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.challenges-grid {
grid-template-columns: 1fr;
gap: 20px;
margin: 20px 0;
}
.challenge-category {
padding: 20px;
}
.challenge-card {
min-height: 80px;
padding: 15px;
}
.challenge-icon {
width: 40px;
height: 40px;
font-size: 24px;
}
.modal-content {
width: 95%;
max-height: 95vh;
}
.modal-header {
padding: 15px 20px;
}
.modal-body {
padding: 20px;
}
.modal-header h2 {
font-size: 18px;
}
}
/* Card Sorting Styling */
.sorting-section {
background: #e8f4f8;
border-radius: 12px;
padding: 30px;
margin: 30px 0;
text-align: center;
}
.sorting-section h2 {
color: #2c5f76;
margin-bottom: 10px;
}
.sorting-controls {
max-width: 600px;
margin: 0 auto;
}
.sort-count-btn {
background: #ffffff;
color: #2c5f76;
border: 2px solid #2c5f76;
border-radius: 8px;
padding: 10px 20px;
margin: 0 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.sort-count-btn:hover {
background: #2c5f76;
color: white;
transform: translateY(-2px);
}
.sort-count-btn.active {
background: #2c5f76;
color: white;
}
.sorting-actions {
margin-top: 20px;
}
/* Setup modal body for sticky positioning */
.modal-body {
position: relative;
}
/* Sticky header within modal body */
.sorting-header {
position: sticky;
top: 0;
background: #f8f9fa;
border-bottom: 2px solid #e1e5e9;
border-radius: 8px 8px 0 0;
z-index: 50;
padding: 12px 20px;
margin: 0 -20px 15px -20px; /* Extend to edges of modal body */
}
.sorting-header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.sorting-status-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.sorting-timer {
font-size: 18px;
font-weight: bold;
color: #2c5f76;
}
.sorting-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.header-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.check-btn {
background: #28a745;
color: white;
}
.check-btn:hover {
background: #218838;
}
.reveal-btn {
background: #ffc107;
color: #212529;
}
.reveal-btn:hover {
background: #e0a800;
}
.new-btn {
background: #007bff;
color: white;
}
.new-btn:hover {
background: #0056b3;
}
.end-btn {
background: #dc3545;
color: white;
}
.end-btn:hover {
background: #c82333;
}
.sorting-game-actions {
display: none; /* Hide old action buttons */
}
/* Score Modal Styles */
.score-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
}
.score-modal-content {
background: white;
border-radius: 12px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
animation: slideIn 0.3s ease;
}
.score-modal-header {
padding: 20px 30px;
border-bottom: 1px solid #e1e5e9;
display: flex;
justify-content: space-between;
align-items: center;
}
.score-modal-header h3 {
margin: 0;
color: #333;
font-size: 24px;
}
.score-modal-close {
background: none;
border: none;
font-size: 32px;
cursor: pointer;
color: #666;
padding: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 20px;
transition: all 0.2s ease;
}
.score-modal-close:hover {
background: #f8f9fa;
color: #333;
}
.score-modal-body {
padding: 30px;
}
.score-modal-body .feedback-score {
font-size: 48px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
color: #28a745;
}
.score-modal-body .feedback-message {
font-size: 20px;
text-align: center;
margin-bottom: 15px;
color: #333;
}
.score-modal-body .feedback-time {
font-size: 18px;
text-align: center;
margin-bottom: 25px;
color: #666;
font-weight: bold;
}
.score-modal-body .feedback-breakdown {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.score-modal-body .score-component {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.score-modal-body .score-component:last-child {
border-bottom: none;
}
.score-modal-body .component-label {
font-weight: bold;
color: #333;
}
.score-modal-body .component-score {
font-weight: bold;
color: #007bff;
}
.score-modal-body .component-weight {
color: #666;
font-size: 14px;
}
.score-modal-body .feedback-details {
background: #e9ecef;
padding: 15px;
border-radius: 6px;
text-align: center;
font-size: 16px;
color: #495057;
}
.score-modal-footer {
padding: 20px 30px;
border-top: 1px solid #e1e5e9;
display: flex;
gap: 15px;
justify-content: flex-end;
}
.modal-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
}
.new-game-btn {
background: #007bff;
color: white;
}
.new-game-btn:hover {
background: #0056b3;
}
.close-btn {
background: #6c757d;
color: white;
}
.close-btn:hover {
background: #545b62;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.sort-start-btn,
.sort-check-btn,
.sort-reveal-btn,
.sort-new-btn {
background: #2c5f76;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
margin: 0 10px;
transition: all 0.3s ease;
}
.sort-start-btn:hover,
.sort-check-btn:hover,
.sort-reveal-btn:hover,
.sort-new-btn:hover {
background: #1e4a61;
transform: translateY(-2px);
}
.sort-check-btn {
background: #28a745;
}
.sort-check-btn:hover {
background: #218838;
}
.sort-reveal-btn {
background: #ffc107;
color: #333;
}
.sort-reveal-btn:hover {
background: #e0a800;
}
.sort-new-btn {
background: #6f42c1;
}
.sort-new-btn:hover {
background: #5a32a3;
}
.sorting-instructions {
margin: 20px 0;
color: #2c5f76;
}
.sorting-progress {
background: #fff;
border-radius: 8px;
padding: 10px 20px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Horizontal layout for sorting game */
.sorting-game-layout {
display: flex;
gap: 30px;
margin: 20px 0;
}
.available-cards-section {
flex: 1;
}
.sorting-slots-section {
flex: 2;
}
.available-cards-section h4,
.sorting-slots-section h4 {
margin: 0 0 15px 0;
color: #2c5f76;
font-size: 18px;
text-align: center;
}
.sorting-area {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
padding: 15px;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
min-height: 120px;
border: 2px dashed #2c5f76;
}
@media (max-width: 768px) {
.sorting-game-layout {
flex-direction: column;
gap: 20px;
}
.available-cards-section,
.sorting-slots-section {
flex: none;
}
}
.position-slots {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
align-items: center;
padding: 15px;
background: rgba(255, 255, 255, 0.7);
border-radius: 8px;
border: 2px dashed #2c5f76;
}
.insert-button {
width: 32px;
height: 50px;
background: #2c5f76;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 24px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
opacity: 0.3;
}
.insert-button:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.1);
}
.insert-button.active {
opacity: 1;
background: #1976d2;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.insert-button.disabled {
opacity: 0.1;
cursor: not-allowed;
}
.position-slot {
width: 90px;
height: 110px;
border: 2px solid #2c5f76;
border-radius: 8px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.position-slot.gradient-bg {
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
.position-slot:hover {
background: #f0f8ff;
border-color: #1a4a5c;
transform: scale(1.05);
}
.position-slot.active {
background: #e3f2fd;
border-color: #1976d2;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3);
}
.position-slot .slot-label {
font-size: 12px;
text-align: center;
font-weight: 500;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
/* Color will be set dynamically based on background */
}
.position-slot.filled .slot-label {
position: absolute;
bottom: 8px;
left: 8px;
right: 8px;
color: #2c3e50;
text-shadow: none;
font-size: 11px;
font-weight: 600;
opacity: 0;
transition: opacity 0.3s ease;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
padding: 4px 8px;
}
.position-slot.filled:hover .slot-label {
opacity: 1;
}
.position-slot.filled {
background: #e8f5e8;
border-color: #28a745;
}
.position-slot.filled .slot-card {
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.position-slot.correct {
border-color: #28a745;
background: #f8fff9;
}
.position-slot.incorrect {
border-color: #dc3545;
background: #fff8f8;
animation: shake 0.5s ease-in-out;
}
.sorting-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin: 30px 0;
padding: 20px;
background: rgba(255, 255, 255, 0.7);
border-radius: 12px;
min-height: 300px;
}
.sort-card {
background: white;
border-radius: 8px;
padding: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid transparent;
position: relative;
user-select: none;
width: 90px;
height: 90px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.sort-card .card-svg svg {
max-width: 100%;
height: auto;
}
.sort-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
border-color: #2c5f76;
}
.sort-card.selected {
border-color: #1976d2;
background: #e3f2fd;
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(25, 118, 210, 0.3);
}
.sort-card.placed {
opacity: 0.7;
transform: scale(0.9);
cursor: default;
}
.sort-card.placed:hover {
transform: scale(0.95);
border-color: #28a745;
}
.sort-card.correct {
border-color: #28a745;
background: #f8fff9;
}
.sort-card.incorrect {
border-color: #dc3545;
background: #fff8f8;
animation: shake 0.5s ease-in-out;
}
.sort-card .card-position {
position: absolute;
top: 5px;
left: 5px;
background: rgba(44, 95, 118, 0.8);
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
}
.sort-card .revealed-number {
position: absolute;
top: 5px;
right: 5px;
background: #ffc107;
color: #333;
border-radius: 4px;
padding: 2px 8px;
font-size: 14px;
font-weight: bold;
display: none;
}
.sort-card.revealed .revealed-number {
display: block;
}
.sorting-feedback {
background: white;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.feedback-perfect {
background: linear-gradient(135deg, #d4edda, #c3e6cb);
color: #155724;
border: 2px solid #28a745;
}
.feedback-good {
background: linear-gradient(135deg, #fff3cd, #ffeaa7);
color: #856404;
border: 2px solid #ffc107;
}
.feedback-needs-work {
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
color: #721c24;
border: 2px solid #dc3545;
}
.feedback-score {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.feedback-message {
font-size: 18px;
margin: 10px 0;
}
.feedback-details {
margin-top: 15px;
font-size: 14px;
opacity: 0.8;
}
.feedback-breakdown {
margin: 20px 0;
padding: 15px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.score-component {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
padding: 5px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.score-component:last-child {
border-bottom: none;
}
.component-label {
font-weight: bold;
flex: 1;
}
.component-score {
font-weight: bold;
margin: 0 10px;
min-width: 40px;
text-align: right;
}
.component-weight {
font-size: 12px;
opacity: 0.7;
min-width: 80px;
text-align: right;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
@keyframes success-pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.sort-card.success {
animation: success-pulse 0.6s ease-in-out;
}
/* Challenge Buttons */
.challenges-section {
margin: 40px 0;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
.section-header {
text-align: center;
margin-bottom: 30px;
}
.section-header h2 {
margin: 0 0 10px 0;
color: #2c3e50;
font-size: 28px;
}
.section-header p {
margin: 0;
color: #7a8695;
font-size: 16px;
}
.challenges-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 30px;
margin-top: 30px;
}
.challenge-category {
background: white;
border-radius: 16px;
padding: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f2f5;
}
.category-title {
margin: 0 0 20px 0;
color: #2c3e50;
font-size: 18px;
font-weight: 600;
text-align: center;
padding-bottom: 15px;
border-bottom: 2px solid #f0f2f5;
}
.challenge-cards {
display: flex;
flex-direction: column;
gap: 15px;
}
.challenge-card {
background: linear-gradient(135deg, #4a90e2, #357abd);
color: white;
border: none;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 15px;
text-align: left;
min-height: 100px;
}
.challenge-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(74, 144, 226, 0.4);
}
.challenge-icon {
font-size: 32px;
flex-shrink: 0;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
}
.challenge-content {
flex: 1;
}
.challenge-content h4 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
}
.challenge-content p {
margin: 0 0 12px 0;
opacity: 0.9;
font-size: 14px;
line-height: 1.4;
}
.challenge-stats {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.stat {
background: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.sorting-card {
background: linear-gradient(135deg, #2c5f76, #1e4a61);
}
.sorting-card:hover {
box-shadow: 0 8px 25px rgba(44, 95, 118, 0.4);
}
.matching-card {
background: linear-gradient(135deg, #7b4397, #dc2430);
}
.matching-card:hover {
box-shadow: 0 8px 25px rgba(123, 67, 151, 0.4);
}
.start-game-btn {
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: white;
border: none;
padding: 15px 20px;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
min-height: 70px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4px;
}
.start-game-btn:hover {
background: linear-gradient(135deg, #5f4ed6, #9085f5);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(108, 92, 231, 0.4);
}
.start-game-btn .btn-main {
font-size: 16px;
font-weight: 600;
line-height: 1.2;
}
.start-game-btn .btn-sub {
font-size: 13px;
opacity: 0.9;
font-weight: 400;
}
.mode-buttons,
.timer-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.mode-btn,
.timer-btn {
background: #f8f9fa;
border: 2px solid #e9ecef;
padding: 10px 15px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: center;
}
.mode-btn.active,
.timer-btn.active {
background: #007bff;
border-color: #007bff;
color: white;
}
.mode-btn:hover,
.timer-btn:hover {
border-color: #007bff;
background: #e3f2fd;
}
.mode-btn.active:hover,
.timer-btn.active:hover {
background: #0056b3;
}
.mode-icon {
font-size: 18px;
}
.mode-text {
font-weight: 500;
}
.timer-btn {
min-width: 80px;
font-size: 14px;
font-weight: 500;
}
/* Matching Game Styles */
.matching-header {
position: sticky;
top: 0;
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
padding: 15px 20px;
z-index: 100;
margin: 0 -20px 15px -20px;
}
.matching-header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.matching-status-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.matching-stats {
display: flex;
gap: 15px;
font-size: 14px;
color: #6c757d;
}
.matching-timer {
font-weight: 600;
color: #495057;
}
.player-stats {
display: flex;
align-items: center;
gap: 20px;
margin-top: 10px;
}
.player-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 8px;
min-width: 80px;
}
.player-info.active {
color: white;
}
.player-info.player1 {
border: 2px solid #007bff;
}
.player-info.player1.active {
background: #007bff;
}
.player-info.player2 {
border: 2px solid #fd7e14;
}
.player-info.player2.active {
background: #fd7e14;
}
.player-name {
font-size: 12px;
font-weight: 600;
margin-bottom: 2px;
}
.player-score {
font-size: 14px;
font-weight: 500;
}
.current-turn {
font-size: 16px;
font-weight: 600;
color: #007bff;
text-align: center;
}
.turn-timer {
font-size: 20px;
font-weight: 700;
color: #495057;
background: #e9ecef;
padding: 8px 12px;
border-radius: 8px;
min-width: 60px;
text-align: center;
border: 2px solid #dee2e6;
}
.turn-timer.warning {
background: #fff3cd;
color: #856404;
animation: timerPulse 1s infinite;
}
.turn-timer.critical {
background: #f8d7da;
color: #721c24;
animation: timerPulse 0.5s infinite;
}
.turn-timer.waiting {
background: #d1ecf1;
color: #0c5460;
opacity: 0.8;
}
@keyframes timerPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.two-player-results {
display: flex;
align-items: center;
justify-content: center;
gap: 30px;
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 12px;
}
.player-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.player-name {
font-size: 16px;
font-weight: 600;
color: #495057;
}
.player-final-score {
font-size: 24px;
font-weight: 700;
color: #007bff;
}
.vs-divider {
font-size: 18px;
font-weight: 700;
color: #6c757d;
padding: 0 10px;
}
.game-summary {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 15px;
background: #e3f2fd;
border-radius: 8px;
}
.summary-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.summary-label {
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
font-weight: 600;
}
.summary-value {
font-size: 16px;
font-weight: 600;
color: #495057;
}
.matching-grid {
display: grid;
gap: 8px;
padding: 10px;
justify-content: center;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.matching-grid.grid-3x4 {
grid-template-columns: repeat(4, 1fr);
aspect-ratio: 4/3;
width: min(100%, calc(100vh - 280px) * 4/3);
height: min(calc(100vw - 40px) * 3/4, calc(100vh - 280px));
}
.matching-grid.grid-4x4 {
grid-template-columns: repeat(4, 1fr);
aspect-ratio: 1;
width: min(100%, calc(100vh - 280px));
height: min(calc(100vw - 40px), calc(100vh - 280px));
}
.matching-grid.grid-4x6 {
grid-template-columns: repeat(6, 1fr);
aspect-ratio: 6/4;
width: min(100%, calc(100vh - 280px) * 6/4);
height: min(calc(100vw - 40px) * 4/6, calc(100vh - 280px));
}
.matching-grid.grid-5x6 {
grid-template-columns: repeat(6, 1fr);
aspect-ratio: 6/5;
width: min(100%, calc(100vh - 280px) * 6/5);
height: min(calc(100vw - 40px) * 5/6, calc(100vh - 280px));
}
.match-card {
aspect-ratio: 1;
background: #ffffff;
border: 2px solid #e9ecef;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.3s ease;
user-select: none;
width: 100%;
height: 100%;
box-sizing: border-box;
}
/* Cards automatically size to fill grid cells while maintaining aspect ratio */
.match-card {
/* Let the card fill the grid cell */
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.match-card:hover {
border-color: #007bff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.match-card.flipped {
background: #f8f9fa;
border-color: #007bff;
}
.match-card.matched {
background: #d4edda;
border-color: #28a745;
cursor: default;
position: relative;
opacity: 0.7;
filter: grayscale(20%);
transform: scale(0.95);
transition: all 0.3s ease;
}
.match-card.matched:hover {
transform: scale(0.95);
box-shadow: none;
border-color: #28a745;
}
.match-card.matched-player1 {
background: #cce7ff;
border-color: #007bff;
box-shadow: none;
opacity: 0.7;
filter: grayscale(20%);
}
.match-card.matched-player1:hover {
border-color: #007bff;
box-shadow: none;
}
.match-card.matched-player1::before {
border-color: #007bff !important;
background: linear-gradient(
45deg,
rgba(0, 123, 255, 0.1),
rgba(0, 123, 255, 0.05)
) !important;
}
.match-card.matched-player1::after {
background: #007bff !important;
box-shadow:
0 0 0 1px #007bff,
0 0 8px rgba(0, 123, 255, 0.4) !important;
}
.match-card.matched-player2 {
background: #ffe6cc;
border-color: #fd7e14;
box-shadow: none;
opacity: 0.7;
filter: grayscale(20%);
}
.match-card.matched-player2:hover {
border-color: #fd7e14;
box-shadow: none;
}
.match-card.matched-player2::before {
border-color: #fd7e14 !important;
background: linear-gradient(
45deg,
rgba(253, 126, 20, 0.1),
rgba(253, 126, 20, 0.05)
) !important;
}
.match-card.matched-player2::after {
background: #fd7e14 !important;
box-shadow:
0 0 0 1px #fd7e14,
0 0 8px rgba(253, 126, 20, 0.4) !important;
}
.player-badge {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
color: white;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
z-index: 20;
}
.player-badge.player1 {
background: #007bff;
}
.player-badge.player2 {
background: #fd7e14;
}
.match-card.matched::after {
content: "";
position: absolute;
bottom: 3px;
left: 3px;
width: 12px;
height: 12px;
background: #28a745;
border: 2px solid white;
border-radius: 50%;
box-shadow:
0 0 0 1px #28a745,
0 0 8px rgba(40, 167, 69, 0.4);
pointer-events: none;
z-index: 10;
}
.match-card.matched::before {
content: "";
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 2px solid #28a745;
border-radius: 10px;
background: linear-gradient(
45deg,
rgba(40, 167, 69, 0.1),
rgba(40, 167, 69, 0.05)
);
pointer-events: none;
z-index: 1;
}
.match-card.matched .match-card-content {
opacity: 0.6;
}
.match-card.invalid-move {
animation: invalidMoveShake 0.6s ease-in-out;
border-color: #dc3545 !important;
}
@keyframes invalidMoveShake {
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-3px);
}
20%,
40%,
60%,
80% {
transform: translateX(3px);
}
}
.match-card.valid-choice {
border-color: #28a745 !important;
box-shadow: 0 0 8px rgba(40, 167, 69, 0.4);
animation: validChoicePulse 2s infinite;
}
.match-card.invalid-choice {
opacity: 0.5;
cursor: not-allowed;
}
.match-card.invalid-choice:hover {
transform: none;
box-shadow: none;
}
@keyframes validChoicePulse {
0%,
100% {
box-shadow: 0 0 8px rgba(40, 167, 69, 0.4);
}
50% {
box-shadow: 0 0 12px rgba(40, 167, 69, 0.7);
}
}
@keyframes matchedCard {
0% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
50% {
transform: scale(1.1) rotate(2deg);
opacity: 0.9;
}
100% {
transform: scale(0.95) rotate(0deg);
opacity: 0.7;
}
}
.match-card.matched {
animation: matchedCard 0.6s ease-out forwards;
}
.match-card-content {
display: none;
width: 100%;
height: 100%;
padding: 8px;
}
.match-card.flipped .match-card-content,
.match-card.matched .match-card-content {
display: flex;
align-items: center;
justify-content: center;
}
.match-card-number {
font-size: 24px;
font-weight: bold;
color: #495057;
}
.match-card-abacus {
width: 100%;
height: 100%;
}
.match-card-abacus svg {
width: 100%;
height: 100%;
}
.match-card-back {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 600;
gap: 4px;
border-radius: 6px;
overflow: hidden;
}
.match-card-back.abacus-type {
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
}
.match-card-back.number-type {
background: linear-gradient(135deg, #00b894, #00cec9);
}
.match-card-back .card-type-icon {
font-size: 40px;
}
.match-card.flipped .match-card-back,
.match-card.matched .match-card-back {
display: none;
}
.matching-instructions {
background: #e3f2fd;
border: 1px solid #bbdefb;
border-radius: 8px;
padding: 15px;
margin: 0 0 20px 0;
text-align: center;
}
.matching-feedback {
background: white;
border-radius: 12px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
@media (max-width: 768px) {
.matching-grid {
gap: 6px;
padding: 8px;
}
.matching-grid.grid-3x4 {
width: min(100%, calc(100vh - 260px) * 4/3);
height: min(calc(100vw - 30px) * 3/4, calc(100vh - 260px));
}
.matching-grid.grid-4x4 {
width: min(100%, calc(100vh - 260px));
height: min(calc(100vw - 30px), calc(100vh - 260px));
}
.matching-grid.grid-4x6 {
width: min(100%, calc(100vh - 260px) * 6/4);
height: min(calc(100vw - 30px) * 4/6, calc(100vh - 260px));
}
.matching-grid.grid-5x6 {
width: min(100%, calc(100vh - 260px) * 6/5);
height: min(calc(100vw - 30px) * 5/6, calc(100vh - 260px));
}
.match-card-number {
font-size: 16px;
}
.match-card-back .card-type-icon {
font-size: 28px;
}
.matching-header {
padding: 10px 15px;
margin: 0 -20px 10px -20px;
}
.matching-instructions {
padding: 12px;
margin: 0 0 15px 0;
font-size: 14px;
}
.start-game-btn {
min-height: 60px;
padding: 12px 16px;
}
.start-game-btn .btn-main {
font-size: 14px;
}
.start-game-btn .btn-sub {
font-size: 12px;
}
}
@media (max-width: 480px) {
.matching-grid {
gap: 4px;
padding: 5px;
}
.matching-grid.grid-3x4 {
width: min(100%, calc(100vh - 240px) * 4/3);
height: min(calc(100vw - 20px) * 3/4, calc(100vh - 240px));
}
.matching-grid.grid-4x4 {
width: min(100%, calc(100vh - 240px));
height: min(calc(100vw - 20px), calc(100vh - 240px));
}
.matching-grid.grid-4x6 {
width: min(100%, calc(100vh - 240px) * 6/4);
height: min(calc(100vw - 20px) * 4/6, calc(100vh - 240px));
}
.matching-grid.grid-5x6 {
width: min(100%, calc(100vh - 240px) * 6/5);
height: min(calc(100vw - 20px) * 5/6, calc(100vh - 240px));
}
.match-card-number {
font-size: 14px;
}
.match-card-back .card-type-icon {
font-size: 24px;
}
.start-game-btn {
min-height: 50px;
padding: 10px 12px;
}
.start-game-btn .btn-main {
font-size: 13px;
}
.start-game-btn .btn-sub {
font-size: 11px;
}
.match-card.matched::after {
width: 10px;
height: 10px;
bottom: 2px;
left: 2px;
}
}
/* Modal Styling */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s ease-out;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 900px;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: slideIn 0.3s ease-out;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: 1px solid #eee;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 12px 12px 0 0;
}
.modal-header h2 {
margin: 0;
color: #333;
}
.modal-controls {
display: flex;
gap: 10px;
}
.fullscreen-btn,
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 5px 10px;
border-radius: 6px;
transition: background 0.2s ease;
}
.fullscreen-btn:hover,
.close-btn:hover {
background: rgba(0, 0, 0, 0.1);
}
.close-btn {
color: #666;
}
.close-btn:hover {
color: #333;
}
.modal-body {
padding: 30px;
flex: 1;
overflow-y: auto;
}
/* Fullscreen modal styling */
.modal.fullscreen {
background: rgba(0, 0, 0, 0.95);
}
.modal.fullscreen .modal-content {
width: 100%;
height: 100%;
max-width: none;
max-height: none;
border-radius: 0;
}
.modal.fullscreen .modal-header {
border-radius: 0;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: scale(0.9) translateY(-20px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Main Page Header -->
<div class="header">
<h1>Soroban Flashcards</h1>
<p>Interactive learning tools for mastering the Japanese abacus</p>
</div>
<!-- Navigation -->
<nav class="page-nav">
<div class="nav-buttons">
<button class="nav-btn active" data-section="introduction">
📚 Introduction
</button>
<button class="nav-btn" data-section="configuration">
⚙️ Configuration
</button>
<button class="nav-btn" data-section="challenges">
🎯 Challenges
</button>
<button class="nav-btn" data-section="flashcards">
🧮 Flashcards
</button>
</div>
</nav>
<!-- Section 1: Introduction -->
<section id="introduction" class="page-section active">
<div class="section-header">
<h2 class="section-title">📚 How to Learn with Soroban</h2>
<p class="section-subtitle">
Master the Japanese abacus through interactive practice
</p>
</div>
<div class="instructions">
<h3>Understanding the Soroban:</h3>
<p>
The soroban (Japanese abacus) represents numbers using beads
arranged in columns. Each column represents a place value (ones,
tens, hundreds, etc.). In each column:
</p>
<ul style="text-align: left; display: inline-block; margin: 20px 0">
<li><strong>Top bead (heaven):</strong> Represents 5</li>
<li><strong>Bottom beads (earth):</strong> Each represents 1</li>
<li>
<strong>Active beads:</strong> Pushed toward the horizontal bar
</li>
</ul>
<h3>How to Practice:</h3>
<p>
Look at each abacus representation and try to determine the number
before hovering or clicking to reveal the answer. Start with smaller
numbers and work your way up to more complex calculations.
</p>
</div>
</section>
<!-- Section 2: Configuration -->
<section id="configuration" class="page-section">
<div class="section-header">
<h2 class="section-title">⚙️ Current Configuration</h2>
<p class="section-subtitle">Settings used for this flashcard set</p>
</div>
<div class="stats">
<div><strong>Cards:</strong> 10</div>
<div><strong>Range:</strong> 1 - 10</div>
<div><strong>Color Scheme:</strong> All beads are the same color</div>
<div><strong>Bead Shape:</strong> Diamond</div>
</div>
<div class="instructions">
<h3>Configuration Details:</h3>
<p>
This flashcard set has been generated with specific settings to help
you practice. You can generate new sets with different ranges, color
schemes, and visual styles using the command-line tools.
</p>
<h4>Available Options:</h4>
<ul style="text-align: left; display: inline-block; margin: 20px 0">
<li>
<strong>Number Ranges:</strong> Practice with any range from
0-99999
</li>
<li>
<strong>Color Schemes:</strong> Monochrome, place-value,
heaven-earth, alternating
</li>
<li><strong>Bead Shapes:</strong> Diamond, circle, square</li>
<li>
<strong>Visual Styles:</strong> Multiple colorblind-friendly
palettes
</li>
</ul>
</div>
</section>
<!-- Section 3: Challenges -->
<section id="challenges" class="page-section">
<div class="section-header">
<h2 class="section-title">🎯 Interactive Challenges</h2>
<p class="section-subtitle">
Test your soroban skills with engaging games and quizzes
</p>
</div>
<div class="challenges-section">
<div class="challenges-grid">
<div class="challenge-category">
<h3 class="category-title">🧠 Memory & Speed</h3>
<div class="challenge-cards">
<button id="open-quiz-modal" class="challenge-card quiz-card">
<div class="challenge-icon"></div>
<div class="challenge-content">
<h4>Speed Memory Quiz</h4>
<p>Quick card displays test your reading skills</p>
<div class="challenge-stats">
<span class="stat">🎯 Accuracy</span>
<span class="stat">⏱️ Speed</span>
</div>
</div>
</button>
</div>
</div>
<div class="challenge-category">
<h3 class="category-title">🎲 Logic & Patterns</h3>
<div class="challenge-cards">
<button
id="open-sorting-modal"
class="challenge-card sorting-card"
>
<div class="challenge-icon">🔢</div>
<div class="challenge-content">
<h4>Card Sorting Challenge</h4>
<p>Arrange cards using only abacus patterns</p>
<div class="challenge-stats">
<span class="stat">🧩 Logic</span>
<span class="stat">👀 Pattern Recognition</span>
</div>
</div>
</button>
<button
id="open-matching-modal"
class="challenge-card matching-card"
>
<div class="challenge-icon">🧩</div>
<div class="challenge-content">
<h4>Matching Pairs</h4>
<p>Match abacus patterns with their numerals</p>
<div class="challenge-stats">
<span class="stat">🔍 Memory</span>
<span class="stat">⚡ Speed</span>
</div>
</div>
</button>
</div>
</div>
</div>
</div>
<!-- Quiz Modal -->
<div id="quiz-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Speed Memory Quiz</h2>
<div class="modal-controls">
<button
id="quiz-fullscreen-btn"
class="fullscreen-btn"
title="Toggle Fullscreen"
>
</button>
<button id="close-quiz-modal" class="close-btn">&times;</button>
</div>
</div>
<div class="modal-body">
<p>
Test your soroban reading skills! Cards will be shown briefly,
then you'll enter the numbers you remember.
</p>
<div class="quiz-controls">
<div class="control-group">
<label for="quiz-count">Cards to Quiz:</label>
<div class="count-buttons">
<button type="button" class="count-btn" data-count="5">
5
</button>
<button type="button" class="count-btn" data-count="10">
10
</button>
<button
type="button"
class="count-btn active"
data-count="15"
>
15
</button>
<button type="button" class="count-btn" data-count="25">
25
</button>
<button type="button" class="count-btn" data-count="all">
All (10)
</button>
</div>
</div>
<div class="control-group">
<label for="display-time">Display Time per Card:</label>
<div class="slider-container">
<input
type="range"
id="display-time"
min="0.5"
max="10"
step="0.5"
value="2"
/>
<span class="slider-value">2.0s</span>
</div>
</div>
<button id="start-quiz" class="quiz-start-btn">
Start Quiz
</button>
</div>
<!-- Quiz Game Area (hidden initially) -->
<div id="quiz-game" class="quiz-game" style="display: none">
<div class="quiz-header">
<div class="quiz-progress">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<span class="progress-text"
>Card <span id="current-card">1</span> of
<span id="total-cards">10</span></span
>
</div>
<button id="end-quiz" class="end-game-btn">End Quiz</button>
</div>
<div class="quiz-display">
<div id="quiz-card" class="quiz-flashcard">
<!-- Card content will be inserted here -->
</div>
<div id="quiz-countdown" class="countdown">Get Ready...</div>
</div>
</div>
<!-- Quiz Input Phase (hidden initially) -->
<div id="quiz-input" class="quiz-input" style="display: none">
<h3>Enter the Numbers You Remember</h3>
<div class="quiz-stats">
<div class="stats-item">
<span class="stats-label">Cards shown:</span>
<span id="cards-shown-count">0</span>
</div>
<div class="stats-item">
<span class="stats-label">Guesses left:</span>
<span id="guesses-remaining">0</span>
</div>
<div class="stats-item">
<span class="stats-label">Found:</span>
<span id="numbers-found">0</span>
</div>
</div>
<div class="smart-input-container">
<div class="smart-input-prompt">
Type the numbers you remember:
</div>
<div class="number-display" id="number-display">
<span class="current-typing" id="current-typing"></span>
</div>
<input
type="text"
id="smart-input"
style="
position: absolute;
left: -9999px;
opacity: 0;
pointer-events: none;
"
autocomplete="off"
/>
</div>
<div class="found-numbers" id="found-numbers">
<!-- Accepted numbers will appear here -->
</div>
<div class="quiz-finish-buttons">
<button
id="finish-quiz"
class="finish-btn"
style="display: none"
>
Finish Quiz
</button>
<button
id="give-up-quiz"
class="give-up-btn"
style="display: none"
>
Can't Remember More
</button>
</div>
</div>
<!-- Quiz Results (hidden initially) -->
<div id="quiz-results" class="quiz-results" style="display: none">
<h3>Quiz Results</h3>
<div class="score-display">
<div class="score-circle">
<span id="score-percentage">0%</span>
</div>
<div class="score-details">
<p>
<strong>Score:</strong>
<span id="score-correct">0</span> /
<span id="score-total">0</span> correct
</p>
<p>
<strong>Time per card:</strong>
<span id="result-timing">2.0s</span>
</p>
</div>
</div>
<div class="results-breakdown">
<h4>Detailed Results:</h4>
<div id="results-list">
<!-- Results will be inserted here -->
</div>
</div>
<div class="quiz-actions">
<button id="retry-quiz">Try Again</button>
<button id="back-to-cards">Back to Cards</button>
</div>
</div>
</div>
</div>
</div>
<!-- Sorting Modal -->
<div id="sorting-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Card Sorting Challenge</h2>
<div class="modal-controls">
<button
id="sorting-fullscreen-btn"
class="fullscreen-btn"
title="Toggle Fullscreen"
>
</button>
<button id="close-sorting-modal" class="close-btn">
&times;
</button>
</div>
</div>
<div class="modal-body">
<p>
Click cards and positions to arrange them in ascending order
(smallest to largest). No numerals shown - rely on reading the
abacus!
</p>
<div class="sorting-controls">
<div class="control-group">
<label for="sort-count">Cards to Sort:</label>
<div class="count-buttons">
<button
type="button"
class="sort-count-btn active"
data-count="5"
>
5
</button>
<button type="button" class="sort-count-btn" data-count="8">
8
</button>
<button
type="button"
class="sort-count-btn"
data-count="12"
>
12
</button>
<button
type="button"
class="sort-count-btn"
data-count="15"
>
15
</button>
</div>
</div>
<div class="sorting-actions">
<button id="start-sorting" class="sort-start-btn">
Start Sorting Challenge
</button>
</div>
</div>
<!-- Game Action Buttons (now moved to sticky header) -->
<div class="sorting-game-actions" style="display: none">
<!-- Buttons moved to sticky header -->
</div>
<!-- Sorting Game Header (Sticky) -->
<div
id="sorting-header"
class="sorting-header"
style="display: none"
>
<div class="sorting-header-content">
<div class="sorting-status-group">
<span id="sorting-status">Ready to start</span>
<div class="sorting-timer" id="sorting-timer">0:00</div>
</div>
<div class="sorting-controls">
<button id="check-sorting" class="header-btn check-btn">
Check Solution
</button>
<button id="reveal-numbers" class="header-btn reveal-btn">
Show Numbers
</button>
<button id="new-sorting" class="header-btn new-btn">
New Challenge
</button>
<button id="end-sorting" class="header-btn end-btn">
End Game
</button>
</div>
</div>
</div>
<!-- Sorting Game Area -->
<div id="sorting-game" style="display: none">
<div class="sorting-instructions">
<p>
<strong>Instructions:</strong> Click a card → Click position
or + button to place. Click placed cards to move back.
</p>
</div>
<div class="sorting-game-layout">
<div class="available-cards-section">
<h4>Available Cards</h4>
<div id="sorting-area" class="sorting-area">
<!-- Available cards will be shown here -->
</div>
</div>
<div class="sorting-slots-section">
<h4>Sorting Positions (Smallest → Largest)</h4>
<div id="position-slots" class="position-slots">
<!-- Position slots will be created here -->
</div>
</div>
</div>
<div
class="sorting-feedback"
id="sorting-feedback"
style="display: none"
>
<!-- Feedback will be shown here -->
</div>
</div>
<!-- Score Report Modal -->
<div id="score-modal" class="score-modal" style="display: none">
<div class="score-modal-content">
<div class="score-modal-header">
<h3>🎯 Sorting Challenge Results</h3>
<button class="score-modal-close">&times;</button>
</div>
<div id="score-modal-body" class="score-modal-body">
<!-- Score content will be inserted here -->
</div>
<div class="score-modal-footer">
<button
id="score-modal-new-game"
class="modal-btn new-game-btn"
>
New Challenge
</button>
<button
id="score-modal-close-btn"
class="modal-btn close-btn"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Matching Pairs Modal -->
<div id="matching-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Matching Pairs Challenge</h2>
<div class="modal-controls">
<button
id="matching-fullscreen-btn"
class="fullscreen-btn"
title="Toggle Fullscreen"
>
</button>
<button id="close-matching-modal" class="close-btn">
&times;
</button>
</div>
</div>
<div class="modal-body">
<p>
Match abacus patterns with their corresponding numerals. Find
all pairs in minimum moves!
</p>
<div class="matching-controls">
<div class="control-group">
<label>Game Mode:</label>
<div class="mode-buttons">
<button
type="button"
class="mode-btn active"
data-mode="single"
>
<div class="mode-icon">👤</div>
<div class="mode-text">Single Player</div>
</button>
<button
type="button"
class="mode-btn"
data-mode="two-player"
>
<div class="mode-icon">👥</div>
<div class="mode-text">Two Player</div>
</button>
</div>
</div>
<div
class="control-group"
id="timer-controls"
style="display: none"
>
<label>Turn Timer:</label>
<div class="timer-buttons">
<button
type="button"
class="timer-btn active"
data-timer="0"
>
No Timer
</button>
<button type="button" class="timer-btn" data-timer="15">
15 sec
</button>
<button type="button" class="timer-btn" data-timer="30">
30 sec
</button>
<button type="button" class="timer-btn" data-timer="60">
60 sec
</button>
</div>
</div>
<div class="control-group">
<label for="match-grid-size"
>Choose Grid Size to Start:</label
>
<div class="count-buttons">
<button
type="button"
class="match-size-btn start-game-btn"
data-pairs="6"
>
<div class="btn-main">3×4 Grid</div>
<div class="btn-sub">6 pairs</div>
</button>
<button
type="button"
class="match-size-btn start-game-btn"
data-pairs="8"
>
<div class="btn-main">4×4 Grid</div>
<div class="btn-sub">8 pairs</div>
</button>
<button
type="button"
class="match-size-btn start-game-btn"
data-pairs="12"
>
<div class="btn-main">4×6 Grid</div>
<div class="btn-sub">12 pairs</div>
</button>
<button
type="button"
class="match-size-btn start-game-btn"
data-pairs="15"
>
<div class="btn-main">5×6 Grid</div>
<div class="btn-sub">15 pairs</div>
</button>
</div>
</div>
</div>
<!-- Matching Game Header (Sticky) -->
<div
id="matching-header"
class="matching-header"
style="display: none"
>
<div class="matching-header-content">
<div class="matching-status-group">
<span id="matching-status">Ready to start</span>
<div class="matching-stats">
<span id="moves-counter">Moves: 0</span>
<span id="pairs-found">Pairs: 0/0</span>
<div class="matching-timer" id="matching-timer">0:00</div>
</div>
<!-- Two-player specific stats -->
<div
class="player-stats"
id="player-stats"
style="display: none"
>
<div class="player-info player1" id="player1-info">
<span class="player-name">🔵 Player 1</span>
<span class="player-score">0 pairs</span>
</div>
<div class="current-turn" id="current-turn">
🔵 Player 1's Turn
</div>
<div
class="turn-timer"
id="turn-timer"
style="display: none"
>
30s
</div>
<div class="player-info player2" id="player2-info">
<span class="player-name">🟠 Player 2</span>
<span class="player-score">0 pairs</span>
</div>
</div>
</div>
<div class="matching-controls">
<button id="new-matching" class="header-btn new-btn">
New Game
</button>
<button id="end-matching" class="header-btn end-btn">
End Game
</button>
</div>
</div>
</div>
<!-- Matching Game Area -->
<div id="matching-game" style="display: none">
<div class="matching-instructions">
<p>
<strong>Instructions:</strong> Click two cards to flip them.
Purple cards (🧮) contain abacus patterns, green cards (🔢)
contain numbers. Match abacus patterns with their
corresponding numbers. Complete all pairs with minimum
moves!
</p>
</div>
<div id="matching-grid" class="matching-grid">
<!-- Matching cards will be generated here -->
</div>
<div
class="matching-feedback"
id="matching-feedback"
style="display: none"
>
<!-- Feedback will be shown here -->
</div>
</div>
<!-- Matching Score Modal -->
<div
id="matching-score-modal"
class="score-modal"
style="display: none"
>
<div class="score-modal-content">
<div class="score-modal-header">
<h3>🧩 Matching Pairs Results</h3>
<button class="matching-score-modal-close">&times;</button>
</div>
<div id="matching-score-modal-body" class="score-modal-body">
<!-- Score content will be inserted here -->
</div>
<div class="score-modal-footer">
<button
id="matching-score-modal-new-game"
class="modal-btn new-game-btn"
>
New Challenge
</button>
<button
id="matching-score-modal-close-btn"
class="modal-btn close-btn"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Section 4: Flashcards -->
<section id="flashcards" class="page-section">
<div class="section-header">
<h2 class="section-title">🧮 Practice Flashcards</h2>
<p class="section-subtitle">
Interactive cards for hands-on learning
</p>
</div>
<div class="cards-grid" id="cards-grid">
<div class="flashcard" data-number="1">
<div class="card-number">#1</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 10)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 80 L 3 80 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 10)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 53)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 65.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">1</div>
</div>
<div class="flashcard" data-number="2">
<div class="card-number">#2</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 10)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 80 L 3 80 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 10)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 45.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 65.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">2</div>
</div>
<div class="flashcard" data-number="3">
<div class="card-number">#3</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 10)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 80 L 3 80 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 10)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 45.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 58)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">3</div>
</div>
<div class="flashcard" data-number="4">
<div class="card-number">#4</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 10)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 72.5 L 3 72.5 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 10)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 45.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 58)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 70.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">4</div>
</div>
<div class="flashcard" data-number="5">
<div class="card-number">#5</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 17)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 72.5 L 3 72.5 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 17)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 40)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 52.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 65)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 77.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">5</div>
</div>
<div class="flashcard" data-number="6">
<div class="card-number">#6</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 17)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 73 L 3 73 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 17)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 53)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 65.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">6</div>
</div>
<div class="flashcard" data-number="7">
<div class="card-number">#7</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 17)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 73 L 3 73 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 17)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 45.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 65.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">7</div>
</div>
<div class="flashcard" data-number="8">
<div class="card-number">#8</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 17)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 73 L 3 73 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 17)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 45.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 58)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">8</div>
</div>
<div class="flashcard" data-number="9">
<div class="card-number">#9</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(36.1 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 17)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 65.5 L 3 65.5 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 17)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 45.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 58)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 70.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 25 2 L 25 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">9</div>
</div>
<div class="flashcard" data-number="10">
<div class="card-number">#10</div>
<div class="abacus-container">
<svg
class="typst-doc"
viewBox="0 0 108 108"
width="108pt"
height="108pt"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:h5="http://www.w3.org/1999/xhtml"
>
<g>
<g transform="translate(5.400000000000001 5.400000000000001)">
<g class="typst-group">
<g>
<g transform="translate(23.599999999999998 0)">
<g class="typst-group">
<g>
<g transform="translate(0 -11.400000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(11 10)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 80 L 3 80 L 3 0 Z "
/>
</g>
<g
transform="translate(4.1000000000000005 10)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 33)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 53)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 65.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(4.1000000000000005 78)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(36 10)">
<path
class="typst-shape"
fill="#eeeeee"
fill-rule="nonzero"
d="M 0 0 L 0 79.5 L 3 79.5 L 3 0 Z "
/>
</g>
<g
transform="translate(29.099999999999998 10)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(29.099999999999998 40)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(29.099999999999998 52.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(29.099999999999998 65)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g
transform="translate(29.099999999999998 77.5)"
>
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path
class="typst-shape"
fill="#e6e6e6"
fill-rule="nonzero"
stroke="#000000"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="4"
d="M 8.4 0 L 16.8 6 L 8.4 12 L 0 6 Z "
/>
</g>
</g>
</g>
</g>
<g transform="translate(0 30)">
<path
class="typst-shape"
fill="#000000"
fill-rule="nonzero"
d="M 0 0 L 0 2 L 50 2 L 50 0 Z "
/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</div>
<div class="numeral">10</div>
</div>
</div>
<div class="instructions">
<p>
<em
>Interactive flashcards for digital learning. Use the left/right
arrow keys or click the cards to flip them.</em
>
</p>
</div>
</section>
</div>
<script>
// Modal Manager - Handles modal dialogs and fullscreen functionality
class ModalManager {
constructor() {
this.isFullscreen = false;
this.bindEvents();
}
bindEvents() {
// Challenge button events
document
.getElementById("open-quiz-modal")
.addEventListener("click", () => {
this.openModal("quiz-modal");
});
document
.getElementById("open-sorting-modal")
.addEventListener("click", () => {
this.openModal("sorting-modal");
});
document
.getElementById("open-matching-modal")
.addEventListener("click", () => {
this.openModal("matching-modal");
});
// Close button events
document
.getElementById("close-quiz-modal")
.addEventListener("click", () => {
this.closeModal("quiz-modal");
});
document
.getElementById("close-sorting-modal")
.addEventListener("click", () => {
this.closeModal("sorting-modal");
});
document
.getElementById("close-matching-modal")
.addEventListener("click", () => {
this.closeModal("matching-modal");
});
// Fullscreen button events
document
.getElementById("quiz-fullscreen-btn")
.addEventListener("click", () => {
this.toggleFullscreen("quiz-modal");
});
document
.getElementById("sorting-fullscreen-btn")
.addEventListener("click", () => {
this.toggleFullscreen("sorting-modal");
});
document
.getElementById("matching-fullscreen-btn")
.addEventListener("click", () => {
this.toggleFullscreen("matching-modal");
});
// Close modal when clicking outside
document.addEventListener("click", (e) => {
if (
e.target.classList.contains("modal") &&
e.target.classList.contains("show")
) {
this.closeModal(e.target.id);
}
});
// ESC key to close modal
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
const openModal = document.querySelector(".modal.show");
if (openModal) {
this.closeModal(openModal.id);
}
}
});
// Fullscreen change events
document.addEventListener("fullscreenchange", () => {
this.handleFullscreenChange();
});
document.addEventListener("webkitfullscreenchange", () => {
this.handleFullscreenChange();
});
document.addEventListener("mozfullscreenchange", () => {
this.handleFullscreenChange();
});
document.addEventListener("MSFullscreenChange", () => {
this.handleFullscreenChange();
});
}
openModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.add("show");
document.body.style.overflow = "hidden";
}
closeModal(modalId) {
const modal = document.getElementById(modalId);
if (this.isFullscreen) {
this.exitFullscreen();
}
modal.classList.remove("show", "fullscreen");
document.body.style.overflow = "";
}
async toggleFullscreen(modalId) {
const modal = document.getElementById(modalId);
if (!this.isFullscreen) {
try {
if (modal.requestFullscreen) {
await modal.requestFullscreen();
} else if (modal.webkitRequestFullscreen) {
await modal.webkitRequestFullscreen();
} else if (modal.mozRequestFullScreen) {
await modal.mozRequestFullScreen();
} else if (modal.msRequestFullscreen) {
await modal.msRequestFullscreen();
}
modal.classList.add("fullscreen");
} catch (error) {
console.warn("Fullscreen not supported or failed:", error);
// Fallback to CSS fullscreen
modal.classList.add("fullscreen");
}
} else {
this.exitFullscreen();
}
}
exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
handleFullscreenChange() {
const isFullscreen = !!(
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
);
this.isFullscreen = isFullscreen;
if (!isFullscreen) {
// Remove fullscreen class from all modals when exiting fullscreen
document.querySelectorAll(".modal").forEach((modal) => {
modal.classList.remove("fullscreen");
});
}
}
}
// Quiz functionality - No dependencies, pure JavaScript
class SorobanQuiz {
constructor() {
this.cards = [];
this.quizCards = [];
this.currentCardIndex = 0;
this.displayTime = 2.0;
this.selectedCount = 15;
this.foundNumbers = [];
this.correctAnswers = [];
this.guessesRemaining = 0;
this.currentInput = "";
this.incorrectGuesses = 0;
this.finishButtonsBound = false;
this.initializeCards();
this.bindEvents();
}
initializeCards() {
// Extract card data from the DOM
const cardElements = document.querySelectorAll(".flashcard");
this.cards = Array.from(cardElements).map((card) => ({
number: parseInt(card.dataset.number),
svg: card.querySelector(".abacus-container").innerHTML,
element: card,
}));
}
bindEvents() {
// Count buttons
document.querySelectorAll(".count-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
document
.querySelectorAll(".count-btn")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
this.selectedCount =
btn.dataset.count === "all"
? this.cards.length
: parseInt(btn.dataset.count);
});
});
// Display time slider
const slider = document.getElementById("display-time");
const valueDisplay = document.querySelector(".slider-value");
slider.addEventListener("input", (e) => {
this.displayTime = parseFloat(e.target.value);
valueDisplay.textContent = this.displayTime.toFixed(1) + "s";
});
// Start quiz button
document
.getElementById("start-quiz")
.addEventListener("click", () => {
this.startQuiz();
});
// Note: Submit answers button replaced by smart input system
// Note: Finish quiz buttons are bound later in showInputPhase() when they become visible
// End quiz button
document.getElementById("end-quiz").addEventListener("click", () => {
this.endQuiz();
});
// Retry and back buttons
document
.getElementById("retry-quiz")
.addEventListener("click", () => {
this.resetQuiz();
this.startQuiz();
});
document
.getElementById("back-to-cards")
.addEventListener("click", () => {
this.resetQuiz();
});
}
async startQuiz() {
// Select random cards
this.quizCards = this.getRandomCards(this.selectedCount);
this.correctAnswers = this.quizCards.map((card) => card.number);
this.currentCardIndex = 0;
// Hide configuration controls, show only game
document.querySelector(".quiz-controls").style.display = "none";
// Show quiz game section within modal
this.hideQuizSections();
document.getElementById("quiz-game").style.display = "block";
document.getElementById("total-cards").textContent =
this.quizCards.length;
// Start with the first card
this.showNextCard();
}
getRandomCards(count) {
const shuffled = [...this.cards].sort(() => 0.5 - Math.random());
return shuffled.slice(0, Math.min(count, this.cards.length));
}
async showNextCard() {
if (this.currentCardIndex >= this.quizCards.length) {
this.showInputPhase();
return;
}
const card = this.quizCards[this.currentCardIndex];
const progress =
(this.currentCardIndex / this.quizCards.length) * 100;
// Update progress
document.querySelector(".progress-fill").style.width = progress + "%";
document.getElementById("current-card").textContent =
this.currentCardIndex + 1;
// Only show countdown for the very first card
if (this.currentCardIndex === 0) {
await this.showCountdown();
} else {
// Subtle "new card" indicator for subsequent cards
await this.showNewCardIndicator();
}
// Show card
await this.displayCard(card);
this.currentCardIndex++;
// Minimal delay before next card (just enough for the exit animation)
setTimeout(() => {
this.showNextCard();
}, 100);
}
async showNewCardIndicator() {
return new Promise((resolve) => {
const countdownEl = document.getElementById("quiz-countdown");
const cardEl = document.getElementById("quiz-card");
// Hide card temporarily
cardEl.style.display = "none";
countdownEl.style.display = "block";
// Brief flash to indicate new card
countdownEl.textContent = "Next";
countdownEl.className = "quiz-countdown new-card-flash";
setTimeout(() => {
countdownEl.style.display = "none";
resolve();
}, 150); // Very brief indication
});
}
async showCountdown() {
const countdownEl = document.getElementById("quiz-countdown");
const cardEl = document.getElementById("quiz-card");
cardEl.style.visibility = "hidden";
countdownEl.style.display = "block";
// 3, 2, 1 countdown
const counts = ["3", "2", "1", "GO!"];
for (let i = 0; i < counts.length; i++) {
countdownEl.textContent = counts[i];
countdownEl.className = "countdown";
if (i === counts.length - 1) countdownEl.classList.add("go");
await this.delay(400);
}
countdownEl.style.display = "none";
}
async displayCard(card) {
const cardEl = document.getElementById("quiz-card");
const countdownEl = document.getElementById("quiz-countdown");
// Show card content with entry animation
cardEl.innerHTML = card.svg;
cardEl.style.display = "block";
cardEl.style.visibility = "visible";
cardEl.classList.add("pulse");
// Display for most of the time
await this.delay(this.displayTime * 1000 - 300);
// Subtle exit signal - brief red border flash
cardEl.classList.add("card-exit-warning");
await this.delay(200);
// Quick fade out
cardEl.classList.add("card-fade-out");
await this.delay(100);
// Hide card and reset classes
cardEl.classList.remove(
"pulse",
"card-exit-warning",
"card-fade-out",
);
cardEl.style.visibility = "hidden";
}
showInputPhase() {
// Complete progress bar
document.querySelector(".progress-fill").style.width = "100%";
// Initialize smart input system
this.correctAnswers = this.quizCards.map((card) => card.number);
this.foundNumbers = [];
this.guessesRemaining =
this.selectedCount + Math.floor(this.selectedCount / 2); // Allow 50% extra guesses
// Update stats display
document.getElementById("cards-shown-count").textContent =
this.quizCards.length;
document.getElementById("guesses-remaining").textContent =
this.guessesRemaining;
document.getElementById("numbers-found").textContent = "0";
// Hide quiz game, show input
this.hideQuizSections();
document.getElementById("quiz-input").style.display = "block";
// Setup smart input
const smartInput = document.getElementById("smart-input");
const display = document.getElementById("number-display");
smartInput.value = "";
document.getElementById("current-typing").textContent = "";
// Focus the hidden input and make sure it captures keyboard events
smartInput.focus();
// Remove any existing event listeners to prevent duplicates
const newSmartInput = smartInput.cloneNode(true);
smartInput.parentNode.replaceChild(newSmartInput, smartInput);
// Add input event listener for real-time validation
newSmartInput.addEventListener("input", (e) =>
this.handleSmartInput(e),
);
// Make the display area clickable to maintain focus
display.addEventListener("click", () => {
newSmartInput.focus();
});
// Keep focus on the hidden input
newSmartInput.focus();
// Bind finish buttons (they exist now that quiz-input is shown)
this.bindFinishButtons();
// Show finish button when all numbers found or guesses exhausted
this.updateFinishButtonVisibility();
}
bindFinishButtons() {
// Bind finish quiz buttons - called when input phase starts and buttons are visible
// Only bind once to prevent duplicate listeners
if (this.finishButtonsBound) return;
const finishBtn = document.getElementById("finish-quiz");
const giveUpBtn = document.getElementById("give-up-quiz");
if (finishBtn) {
finishBtn.addEventListener("click", () => {
console.log("Finish quiz button clicked");
this.finishQuiz();
});
console.log("Finish button event listener added");
} else {
console.error("finish-quiz button not found in DOM");
}
if (giveUpBtn) {
giveUpBtn.addEventListener("click", () => {
console.log("Give up button clicked");
this.finishQuiz();
});
console.log("Give up button event listener added");
} else {
console.error("give-up-quiz button not found in DOM");
}
this.finishButtonsBound = true;
}
handleSmartInput(event) {
const input = event.target;
const value = input.value.trim();
const display = document.getElementById("number-display");
const typingSpan = document.getElementById("current-typing");
// Reset visual feedback
display.classList.remove("correct", "incorrect");
// Update the visual display
typingSpan.textContent = value;
// Check if input is empty
if (!value) {
this.currentInput = "";
return;
}
// Check if it's a valid number
const number = parseInt(value);
if (isNaN(number)) {
return; // Wait for more input
}
this.currentInput = value;
// Check if this number is in our correct answers and not already found
if (
this.correctAnswers.includes(number) &&
!this.foundNumbers.includes(number)
) {
// Correct number found!
this.acceptCorrectNumber(number, input, display);
} else if (
value.length >= 2 &&
!this.correctAnswers.includes(number)
) {
// Wrong number (only trigger after at least 2 digits to avoid false positives)
this.handleIncorrectGuess(input, display);
}
}
acceptCorrectNumber(number, input, display) {
// Add to found numbers
this.foundNumbers.push(number);
// Visual success feedback
display.classList.add("correct");
// Update stats
document.getElementById("numbers-found").textContent =
this.foundNumbers.length;
// Add to found numbers display
this.addFoundNumberDisplay(number);
// Clear input immediately for fast entry
setTimeout(() => {
input.value = "";
document.getElementById("current-typing").textContent = "";
this.currentInput = "";
// Check if we're done
this.updateFinishButtonVisibility();
// If all numbers found, auto-finish
if (this.foundNumbers.length === this.correctAnswers.length) {
setTimeout(() => this.finishQuiz(), 1000);
}
}, 150); // Much shorter delay - just enough to show the success feedback
// Remove success visual feedback after animation completes
setTimeout(() => {
display.classList.remove("correct");
}, 500);
}
handleIncorrectGuess(input, display) {
// Only penalize if we have guesses remaining
if (this.guessesRemaining > 0) {
this.guessesRemaining--;
this.incorrectGuesses++; // Track incorrect guesses for scoring
document.getElementById("guesses-remaining").textContent =
this.guessesRemaining;
// Visual error feedback
display.classList.add("incorrect");
// Clear input quickly for rapid entry
setTimeout(() => {
input.value = "";
document.getElementById("current-typing").textContent = "";
this.currentInput = "";
// Check if we're out of guesses
this.updateFinishButtonVisibility();
if (this.guessesRemaining === 0) {
setTimeout(() => this.finishQuiz(), 1000);
}
}, 150); // Same fast clearing as correct numbers
// Remove error visual feedback after animation completes
setTimeout(() => {
display.classList.remove("incorrect");
}, 500);
}
}
addFoundNumberDisplay(number) {
const foundContainer = document.getElementById("found-numbers");
const numberElement = document.createElement("span");
numberElement.className = "found-number";
numberElement.textContent = number;
foundContainer.appendChild(numberElement);
}
updateFinishButtonVisibility() {
const finishBtn = document.getElementById("finish-quiz");
const giveUpBtn = document.getElementById("give-up-quiz");
const hasFoundSome = this.foundNumbers.length > 0;
const hasFoundAll =
this.foundNumbers.length === this.correctAnswers.length;
const outOfGuesses = this.guessesRemaining === 0;
const hasGuessesLeft = this.guessesRemaining > 0;
if (hasFoundAll || outOfGuesses) {
// Show finish button when all found or no guesses left
finishBtn.style.display = "block";
giveUpBtn.style.display = "none";
finishBtn.textContent = hasFoundAll
? "Finish Quiz"
: "Show Results";
} else if (hasFoundSome && hasGuessesLeft) {
// Show both buttons when user has found some but could find more
finishBtn.style.display = "block";
giveUpBtn.style.display = "block";
finishBtn.textContent = "Show Results";
} else {
// No buttons when user hasn't found any yet
finishBtn.style.display = "none";
giveUpBtn.style.display = "none";
}
}
finishQuiz() {
console.log("finishQuiz called, foundNumbers:", this.foundNumbers);
// Use found numbers as answers and show results
this.answers = [...this.foundNumbers];
console.log("About to call showResults with answers:", this.answers);
this.showResults();
}
submitAnswers() {
const input = document.getElementById("answer-input").value;
this.answers = this.parseAnswers(input);
this.showResults();
}
parseAnswers(input) {
// Parse comma or space separated numbers
return input
.split(/[,\s]+/)
.map((s) => s.trim())
.filter((s) => s.length > 0)
.map((s) => parseInt(s))
.filter((n) => !isNaN(n));
}
showResults() {
const scoreData = this.calculateScore();
const correct = scoreData.correct;
const finalScore = scoreData.finalScore;
const percentage = Math.round(finalScore);
// Update score display
document.getElementById("score-percentage").textContent =
percentage + "%";
document.getElementById("score-correct").textContent = correct.length;
document.getElementById("score-total").textContent =
this.correctAnswers.length;
document.getElementById("result-timing").textContent =
this.displayTime.toFixed(1) + "s";
// Show detailed results with penalty info
this.showDetailedResults(correct, scoreData);
// Hide input, show results
this.hideQuizSections();
document.getElementById("quiz-results").style.display = "block";
}
calculateScore() {
const correct = [];
const correctSet = new Set(this.correctAnswers);
const answerSet = new Set(this.answers);
// Find correct answers
this.answers.forEach((answer) => {
if (correctSet.has(answer)) {
correct.push(answer);
}
});
// Calculate base score as percentage of correct answers
const baseScore = (correct.length / this.correctAnswers.length) * 100;
// Calculate penalty: lose 5 points per incorrect guess, minimum 0%
const penalty = this.incorrectGuesses * 5;
const finalScore = Math.max(0, baseScore - penalty);
return {
correct: correct,
baseScore: baseScore,
penalty: penalty,
incorrectGuesses: this.incorrectGuesses,
finalScore: finalScore,
};
}
showDetailedResults(correct, scoreData) {
const resultsEl = document.getElementById("results-list");
const correctSet = new Set(correct);
const answerSet = new Set(this.answers);
let html = "";
// Show scoring breakdown if there were penalties
if (scoreData && scoreData.incorrectGuesses > 0) {
html += `<div class="result-item score-breakdown">
<div style="margin-bottom: 10px; font-weight: bold; color: #2c5f76;">Score Breakdown:</div>
<div style="font-size: 0.9em; color: #666;">
Base Score: ${Math.round(scoreData.baseScore)}% (${correct.length} of ${this.correctAnswers.length} correct)<br>
Penalty: -${scoreData.penalty}% (${scoreData.incorrectGuesses} wrong guess${scoreData.incorrectGuesses > 1 ? "es" : ""} × 5 points each)<br>
<strong style="color: #2c5f76;">Final Score: ${Math.round(scoreData.finalScore)}%</strong>
</div>
</div>`;
}
// Show all correct answers and whether user got them
this.correctAnswers.forEach((num) => {
const wasCorrect = correctSet.has(num);
const className = wasCorrect
? "result-correct"
: "result-incorrect";
const status = wasCorrect ? "✓ Correct" : "✗ Missed";
html += `<div class="result-item">
<span>Card: ${num}</span>
<span class="${className}">${status}</span>
</div>`;
});
// Show any extra incorrect answers
const extraAnswers = this.answers.filter(
(a) =>
!correctSet.has(a) && this.correctAnswers.includes(a) === false,
);
extraAnswers.forEach((num) => {
html += `<div class="result-item">
<span>Wrong guess: ${num}</span>
<span class="result-incorrect">✗ Not in quiz (-5 points)</span>
</div>`;
});
resultsEl.innerHTML = html;
}
endQuiz() {
// Stop the current quiz and return to configuration
this.resetQuiz();
}
resetQuiz() {
// Reset state
this.currentCardIndex = 0;
this.answers = [];
this.correctAnswers = [];
this.quizCards = [];
this.foundNumbers = [];
this.guessesRemaining = 0;
this.currentInput = "";
this.incorrectGuesses = 0;
this.finishButtonsBound = false;
// Clear smart input
const smartInput = document.getElementById("smart-input");
if (smartInput) {
smartInput.value = "";
smartInput.classList.remove("correct", "incorrect");
}
// Clear found numbers display
const foundContainer = document.getElementById("found-numbers");
if (foundContainer) {
foundContainer.innerHTML = "";
}
// Reset stats display
document.getElementById("cards-shown-count").textContent = "0";
document.getElementById("guesses-remaining").textContent = "0";
document.getElementById("numbers-found").textContent = "0";
// Hide finish buttons
document.getElementById("finish-quiz").style.display = "none";
document.getElementById("give-up-quiz").style.display = "none";
// Reset to initial quiz state (hide all sections, show controls)
this.hideQuizSections();
document.querySelector(".quiz-controls").style.display = "block";
}
hideQuizSections() {
document.getElementById("quiz-game").style.display = "none";
document.getElementById("quiz-input").style.display = "none";
document.getElementById("quiz-results").style.display = "none";
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Card Sorting Challenge - Drag and drop functionality
class SortingChallenge {
constructor() {
this.cards = [];
this.sortingCards = [];
this.selectedCount = 5;
this.currentOrder = [];
this.correctOrder = [];
this.isDragging = false;
this.draggedElement = null;
this.draggedIndex = -1;
this.previewOrder = [];
this.lastInsertIndex = -1;
this.touchStartElement = null;
this.touchStartIndex = -1;
this.touchStartX = 0;
this.touchStartY = 0;
this.initializeSorting();
this.bindSortingEvents();
}
initializeSorting() {
// Get available cards (same as quiz cards)
const cardElements = document.querySelectorAll(".flashcard");
this.cards = Array.from(cardElements).map((card) => ({
number: parseInt(card.dataset.number),
svg: card.querySelector(".abacus-container").outerHTML,
}));
}
bindSortingEvents() {
// Card count selection
document.querySelectorAll(".sort-count-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
document
.querySelectorAll(".sort-count-btn")
.forEach((b) => b.classList.remove("active"));
e.target.classList.add("active");
this.selectedCount = parseInt(e.target.dataset.count);
});
});
// Action buttons
document
.getElementById("start-sorting")
.addEventListener("click", () => this.startSorting());
document
.getElementById("check-sorting")
.addEventListener("click", () => this.checkSolution());
document
.getElementById("reveal-numbers")
.addEventListener("click", () => this.revealNumbers());
document
.getElementById("new-sorting")
.addEventListener("click", () => this.newChallenge());
document
.getElementById("end-sorting")
.addEventListener("click", () => this.endSorting());
// Score modal event listeners
document
.querySelector(".score-modal-close")
.addEventListener("click", () => this.hideScoreModal());
document
.getElementById("score-modal-close-btn")
.addEventListener("click", () => this.hideScoreModal());
document
.getElementById("score-modal-new-game")
.addEventListener("click", () => {
this.hideScoreModal();
this.newChallenge();
});
// Close modal when clicking outside
document
.getElementById("score-modal")
.addEventListener("click", (e) => {
if (e.target.id === "score-modal") {
this.hideScoreModal();
}
});
}
startSorting() {
// Select random cards for sorting
const shuffledCards = [...this.cards].sort(() => Math.random() - 0.5);
this.sortingCards = shuffledCards.slice(0, this.selectedCount);
this.correctOrder = [...this.sortingCards].sort(
(a, b) => a.number - b.number,
);
// Shuffle the display order
this.currentOrder = [...this.sortingCards].sort(
() => Math.random() - 0.5,
);
// Initialize revealed state
this.numbersRevealed = false;
// Hide configuration controls, show only game
document.querySelector(".sorting-controls").style.display = "none";
// Show sorting game and sticky header
document.getElementById("sorting-game").style.display = "block";
document.getElementById("sorting-header").style.display = "block";
this.renderSortingCards();
this.updateSortingStatus(
`Arrange the ${this.selectedCount} cards in ascending order (smallest to largest)`,
);
// Start timer
this.startTimer();
// Reset reveal numbers button for new game
document.getElementById("reveal-numbers").style.display =
"inline-block";
// Update buttons - hide old controls, sticky header is now visible
document.getElementById("start-sorting").style.display = "none";
document.querySelector(".sorting-game-actions").style.display =
"none";
}
getSequenceStyle(position, totalSlots) {
// Generate neutral warm gray gradient that won't clash with abacus colors
// Position 0 (first) = darkest, last position = lightest
const intensity = position / (totalSlots - 1); // 0 to 1
const lightness = 30 + intensity * 45; // 30% to 75% lightness for better contrast
return {
background: `hsl(220, 8%, ${lightness}%)`, // Very subtle blue-gray, low saturation
color: lightness > 60 ? "#2c3e50" : "#ffffff", // High contrast text
borderColor: lightness > 60 ? "#2c5f76" : "rgba(255,255,255,0.4)",
};
}
renderSortingCards() {
this.createPositionSlots();
this.renderAvailableCards();
}
createPositionSlots() {
const slotsContainer = document.getElementById("position-slots");
if (!slotsContainer) return;
slotsContainer.innerHTML = "";
this.placedCards = new Array(this.selectedCount).fill(null);
// Add insert button before the very first position
const firstInsertBtn = document.createElement("button");
firstInsertBtn.className = "insert-button";
firstInsertBtn.innerHTML = "+";
firstInsertBtn.dataset.insertAt = 0;
firstInsertBtn.addEventListener("click", (e) =>
this.handleInsertClick(0),
);
slotsContainer.appendChild(firstInsertBtn);
for (let i = 0; i < this.selectedCount; i++) {
// Add the position slot
const slot = document.createElement("div");
slot.className = "position-slot";
slot.dataset.position = i;
// Apply gradient to entire slot background
const style = this.getSequenceStyle(i, this.selectedCount);
slot.style.background = style.background;
slot.style.color = style.color;
slot.style.borderColor = style.borderColor;
slot.innerHTML = `
<div class="slot-label" style="color: ${style.color}">${i === 0 ? "Smallest" : i === this.selectedCount - 1 ? "Largest" : ""}</div>
`;
slot.addEventListener("click", (e) => this.handleSlotClick(i));
slotsContainer.appendChild(slot);
// Add insert button after each position
const insertBtn = document.createElement("button");
insertBtn.className = "insert-button";
insertBtn.innerHTML = "+";
insertBtn.dataset.insertAt = i + 1;
insertBtn.addEventListener("click", (e) =>
this.handleInsertClick(i + 1),
);
slotsContainer.appendChild(insertBtn);
}
}
renderAvailableCards() {
const sortingArea = document.getElementById("sorting-area");
if (!sortingArea) return;
sortingArea.innerHTML = "";
// Remove duplicates and filter out placed cards
const uniqueAvailable = this.currentOrder.filter(
(card, index, arr) => {
// Only keep first occurrence of each card number
const firstIndex = arr.findIndex((c) => c.number === card.number);
if (firstIndex !== index) {
console.warn(
`Duplicate card found: ${card.number}, removing duplicate`,
);
return false;
}
// Skip if already placed
if (
this.placedCards.some(
(placed) => placed && placed.number === card.number,
)
) {
console.warn(
`Card ${card.number} is both available and placed, removing from available`,
);
return false;
}
return true;
},
);
// Update currentOrder to clean version
this.currentOrder = uniqueAvailable;
this.currentOrder.forEach((card, index) => {
const cardEl = document.createElement("div");
cardEl.className = "sort-card";
cardEl.dataset.number = card.number;
// Apply revealed state if numbers were previously revealed
if (this.numbersRevealed) {
cardEl.classList.add("revealed");
}
cardEl.innerHTML = `
<div class="revealed-number">${card.number}</div>
<div class="card-svg">${card.svg}</div>
`;
cardEl.addEventListener("click", (e) =>
this.handleCardClick(card, cardEl),
);
sortingArea.appendChild(cardEl);
});
}
handleCardClick(card, cardElement) {
// Clear any previously selected cards
document.querySelectorAll(".sort-card.selected").forEach((el) => {
el.classList.remove("selected");
});
// Select this card
cardElement.classList.add("selected");
this.selectedCard = card;
this.selectedCardElement = cardElement;
// Highlight available positions and insert buttons
document.querySelectorAll(".position-slot").forEach((slot) => {
if (!slot.classList.contains("filled")) {
slot.classList.add("active");
}
});
document.querySelectorAll(".insert-button").forEach((btn) => {
btn.classList.add("active");
});
this.updateSortingStatus(
`Selected card with value ${card.number}. Click a position or + button to place it.`,
);
}
handleInsertClick(insertPosition) {
if (!this.selectedCard) {
this.updateSortingStatus(
"Please select a card first, then click where to insert it.",
);
return;
}
// Handle insertion at the rightmost position (beyond current array bounds)
if (insertPosition >= this.selectedCount) {
// Find the rightmost empty position
let rightmostEmptyPos = -1;
for (let i = this.selectedCount - 1; i >= 0; i--) {
if (this.placedCards[i] === null) {
rightmostEmptyPos = i;
break;
}
}
if (rightmostEmptyPos === -1) {
this.updateSortingStatus(
"All positions are filled. Move a card first to make room.",
);
return;
}
// Place card in the rightmost empty position
this.placedCards[rightmostEmptyPos] = this.selectedCard;
} else {
// Create a new array for placed cards
const newPlacedCards = new Array(this.selectedCount).fill(null);
// Copy existing cards, shifting them as needed
for (let i = 0; i < this.placedCards.length; i++) {
if (this.placedCards[i] !== null) {
if (i < insertPosition) {
// Cards before insert position stay in same place
newPlacedCards[i] = this.placedCards[i];
} else {
// Cards at or after insert position shift right by 1
if (i + 1 < this.selectedCount) {
newPlacedCards[i + 1] = this.placedCards[i];
} else {
// Card would fall off - store temporarily, will handle below
newPlacedCards[i + 1] = this.placedCards[i];
}
}
}
}
// Place the selected card at the insert position
newPlacedCards[insertPosition] = this.selectedCard;
// Now apply gap-filling logic: shift cards left to compress and eliminate gaps
const compactedCards = [];
for (let i = 0; i < newPlacedCards.length; i++) {
if (newPlacedCards[i] !== null) {
compactedCards.push(newPlacedCards[i]);
}
}
// If we have more cards than positions, put excess back in available
if (compactedCards.length > this.selectedCount) {
const excessCards = compactedCards.slice(this.selectedCount);
this.currentOrder.push(...excessCards);
compactedCards.splice(this.selectedCount);
}
// Fill the final array with compacted cards (no gaps)
const finalPlacedCards = new Array(this.selectedCount).fill(null);
for (let i = 0; i < compactedCards.length; i++) {
finalPlacedCards[i] = compactedCards[i];
}
this.placedCards = finalPlacedCards;
}
// Remove card from available cards
this.currentOrder = this.currentOrder.filter(
(c) => c.number !== this.selectedCard.number,
);
// Debug: Check total card count
this.debugCardCount("after insert");
// Clear selection and re-render
this.clearSelection();
this.updatePositionSlots();
this.renderAvailableCards();
// Update status
const placedCount = this.placedCards.filter((c) => c !== null).length;
if (placedCount === this.selectedCount) {
this.updateSortingStatus(
'All cards placed! Click "Check My Solution" to see how you did.',
);
} else {
this.updateSortingStatus(
`${placedCount}/${this.selectedCount} cards placed. Select another card to continue.`,
);
}
}
debugCardCount(context) {
const placedCount = this.placedCards.filter((c) => c !== null).length;
const availableCount = this.currentOrder.length;
const totalCount = placedCount + availableCount;
console.log(
`DEBUG ${context}: Placed=${placedCount}, Available=${availableCount}, Total=${totalCount}, Expected=${this.selectedCount}`,
);
if (totalCount !== this.selectedCount) {
console.error(
"Card count mismatch! Some cards are missing or duplicated.",
);
console.log(
"Placed cards:",
this.placedCards.map((c) => (c ? c.number : "empty")),
);
console.log(
"Available cards:",
this.currentOrder.map((c) => c.number),
);
}
}
updatePositionSlots() {
this.placedCards.forEach((card, position) => {
const slot = document.querySelector(
`[data-position="${position}"]`,
);
if (!slot) return;
if (card) {
slot.classList.add("filled");
// Reset to white background when filled
slot.style.background = "#fff";
slot.style.color = "#333";
slot.style.borderColor = "#2c5f76";
slot.innerHTML = `
<div class="slot-card">
<div class="card-svg">${card.svg}</div>
</div>
<div class="slot-label">← Click to move back</div>
`;
} else {
slot.classList.remove("filled");
// Apply gradient to empty slot
const style = this.getSequenceStyle(position, this.selectedCount);
slot.style.background = style.background;
slot.style.color = style.color;
slot.style.borderColor = style.borderColor;
slot.innerHTML = `
<div class="slot-label" style="color: ${style.color}">${position === 0 ? "Smallest" : position === this.selectedCount - 1 ? "Largest" : ""}</div>
`;
}
});
}
clearSelection() {
this.selectedCard = null;
this.selectedCardElement = null;
// Remove active states
document.querySelectorAll(".sort-card.selected").forEach((el) => {
el.classList.remove("selected");
});
document.querySelectorAll(".position-slot.active").forEach((el) => {
el.classList.remove("active");
});
document.querySelectorAll(".insert-button.active").forEach((el) => {
el.classList.remove("active");
});
}
handleSlotClick(position) {
const slot = document.querySelector(`[data-position="${position}"]`);
// If no card is selected but slot has a card, move it back to available
if (!this.selectedCard && this.placedCards[position]) {
const cardToMove = this.placedCards[position];
// Remove card from this position
this.placedCards[position] = null;
// Add card back to available cards
this.currentOrder.push(cardToMove);
// Debug: Check total card count
this.debugCardCount("after moving card back");
// Update slot appearance
this.updatePositionSlots();
this.renderAvailableCards();
// Auto-select the moved card so user can immediately place it elsewhere
this.selectedCard = cardToMove;
this.selectedCardElement = document.querySelector(
`[data-number="${cardToMove.number}"]`,
);
if (this.selectedCardElement) {
this.selectedCardElement.classList.add("selected");
// Highlight available positions
document.querySelectorAll(".position-slot").forEach((slot) => {
if (!slot.classList.contains("filled")) {
slot.classList.add("active");
}
});
document.querySelectorAll(".insert-button").forEach((btn) => {
btn.classList.add("active");
btn.classList.remove("disabled");
});
}
const placedCount = this.placedCards.filter(
(c) => c !== null,
).length;
this.updateSortingStatus(
`Moved card ${cardToMove.number} back and selected it. ${placedCount}/${this.selectedCount} cards placed.`,
);
return;
}
if (!this.selectedCard) {
this.updateSortingStatus(
"Select a card first, or click a placed card to move it back.",
);
return;
}
// If slot is already filled, replace the card
if (this.placedCards[position]) {
// Move the previous card back to available area
this.currentOrder.push(this.placedCards[position]);
}
// Place the selected card in this position
this.placedCards[position] = this.selectedCard;
// Remove card from current order (available cards)
this.currentOrder = this.currentOrder.filter(
(c) => c.number !== this.selectedCard.number,
);
// Debug: Check total card count
this.debugCardCount("after regular placement");
// Update slot appearance
slot.classList.add("filled");
// Reset to white background when filled
slot.style.background = "#fff";
slot.style.color = "#333";
slot.style.borderColor = "#2c5f76";
slot.innerHTML = `
<div class="slot-card">
<div class="card-svg">${this.selectedCard.svg}</div>
</div>
<div class="slot-label">← Click to move back</div>
`;
// Clear selection and re-render
this.clearSelection();
this.renderAvailableCards();
// Update status
const placedCount = this.placedCards.filter((c) => c !== null).length;
if (placedCount === this.selectedCount) {
this.updateSortingStatus(
'All cards placed! Click "Check My Solution" to see how you did.',
);
} else {
this.updateSortingStatus(
`${placedCount}/${this.selectedCount} cards placed. Select another card to continue.`,
);
}
}
handleDragEnd(e) {
if (this.previewOrder.length > 0) {
// Commit the preview order to the actual order
this.currentOrder = [...this.previewOrder];
}
// Clean up
this.resetDragState();
// Re-render with final positions
this.renderSortingCards();
// Add success animation
setTimeout(() => {
document.querySelectorAll(".sort-card").forEach((card) => {
card.classList.add("success");
setTimeout(() => card.classList.remove("success"), 600);
});
}, 100);
}
handleDragOver(e) {
if (e.preventDefault) e.preventDefault();
e.dataTransfer.dropEffect = "move";
// Calculate where to insert based on mouse position
const insertIndex = this.calculateInsertIndex(e.clientX, e.clientY);
console.log(
"DragOver - insertIndex:",
insertIndex,
"lastInsertIndex:",
this.lastInsertIndex,
);
if (insertIndex !== -1 && insertIndex !== this.lastInsertIndex) {
console.log(
"Calling updatePreviewOrder with insertIndex:",
insertIndex,
);
this.updatePreviewOrder(insertIndex);
}
return false;
}
handleDragEnter(e) {
// Handle drag enter for the sorting area
}
handleDragLeave(e) {
// Handle drag leave
}
handleDrop(e) {
if (e.stopPropagation) e.stopPropagation();
// The preview order is already set, just let dragEnd handle the commit
return false;
}
calculateInsertIndex(clientX, clientY) {
const sortingArea = document.getElementById("sorting-area");
const cards = Array.from(
sortingArea.querySelectorAll(".sort-card:not(.dragging)"),
);
if (cards.length === 0) return 0;
// Simple approach: find which card the mouse is over
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
const rect = card.getBoundingClientRect();
// Check if mouse is over this card
if (
clientX >= rect.left &&
clientX <= rect.right &&
clientY >= rect.top &&
clientY <= rect.bottom
) {
const cardIndex = parseInt(card.dataset.index);
const cardCenterX = rect.left + rect.width / 2;
const insertIndex =
clientX < cardCenterX ? cardIndex : cardIndex + 1;
return insertIndex;
}
}
return this.currentOrder.length;
}
updatePreviewOrder(insertIndex) {
if (this.lastInsertIndex === insertIndex) return; // No change needed
this.lastInsertIndex = insertIndex;
console.log(
"updatePreviewOrder called with insertIndex:",
insertIndex,
);
console.log(
"currentOrder:",
this.currentOrder.map((c) => c.number),
);
console.log("draggedIndex:", this.draggedIndex);
// Create new preview order
const newOrder = [...this.currentOrder];
const draggedCard = newOrder.splice(this.draggedIndex, 1)[0];
// Adjust insert index if we removed an item before it
let adjustedInsertIndex = insertIndex;
if (this.draggedIndex < insertIndex) {
adjustedInsertIndex--;
}
// Insert at new position
newOrder.splice(adjustedInsertIndex, 0, draggedCard);
this.previewOrder = newOrder;
console.log(
"New preview order:",
this.previewOrder.map((c) => c.number),
);
// Update the visual layout immediately
this.renderPreview();
}
renderPreview() {
console.log("renderPreview called");
const sortingArea = document.getElementById("sorting-area");
// Get all cards except the dragged one
const cards = Array.from(
sortingArea.querySelectorAll(".sort-card:not(.dragging)"),
);
console.log("Found cards (excluding dragged):", cards.length);
const draggedNumber = parseInt(this.draggedElement.dataset.number);
// Simply compare the full orders - if they're different, reorder
const previewNumbers = this.previewOrder.map((c) => c.number);
const currentNumbers = this.currentOrder.map((c) => c.number);
console.log("Preview order:", previewNumbers);
console.log("Current order:", currentNumbers);
// Check if the orders are different
const needsReorder = !previewNumbers.every(
(num, index) => currentNumbers[index] === num,
);
console.log("Needs reorder:", needsReorder);
if (needsReorder) {
console.log("Performing reorder...");
// Update currentOrder to match preview (this is key!)
this.currentOrder = [...this.previewOrder];
// Create a simple reordering without removing cards from DOM
// Just change their visual order using CSS flexbox order or reinsert in correct order
const previewWithoutDragged = this.previewOrder.filter(
(card) => card.number !== draggedNumber,
);
console.log(
"Cards to reorder:",
previewWithoutDragged.map((c) => c.number),
);
console.log(
"Available DOM cards:",
cards.map((c) => parseInt(c.dataset.number)),
);
// Store references to all current cards
const cardElements = new Map();
cards.forEach((card) => {
cardElements.set(parseInt(card.dataset.number), card);
});
// Reorder by moving each card to the correct position
previewWithoutDragged.forEach((card, targetIndex) => {
const cardEl = cardElements.get(card.number);
console.log(
"Finding card",
card.number,
"found element:",
!!cardEl,
);
if (cardEl) {
cardEl.dataset.index = targetIndex;
cardEl.querySelector(".card-position").textContent =
targetIndex + 1;
// Move to correct position in DOM
const currentIndex = Array.from(sortingArea.children).indexOf(
cardEl,
);
const targetPosition = targetIndex;
if (currentIndex !== targetPosition) {
if (targetPosition >= sortingArea.children.length - 1) {
sortingArea.appendChild(cardEl);
} else {
const nextSibling = sortingArea.children[targetPosition];
sortingArea.insertBefore(cardEl, nextSibling);
}
}
}
});
// Ensure dragged element stays at the end for z-index
if (
this.draggedElement &&
this.draggedElement.parentNode === sortingArea
) {
sortingArea.appendChild(this.draggedElement);
}
}
}
resetDragState() {
if (this.draggedElement) {
this.draggedElement.classList.remove("dragging");
this.draggedElement.style.transform = "";
this.draggedElement.style.position = "";
this.draggedElement.style.pointerEvents = "";
}
this.isDragging = false;
this.draggedElement = null;
this.draggedIndex = -1;
this.previewOrder = [];
this.lastInsertIndex = -1;
}
// Enhanced touch support for mobile with real-time reordering
handleTouchStart(e) {
const touch = e.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.touchStartElement = e.target.closest(".sort-card");
this.touchStartIndex = this.touchStartElement
? parseInt(this.touchStartElement.dataset.index)
: -1;
if (this.touchStartElement) {
this.isDragging = true;
this.draggedElement = this.touchStartElement;
this.draggedIndex = this.touchStartIndex;
this.previewOrder = [...this.currentOrder];
this.lastInsertIndex = -1;
this.touchStartElement.classList.add("dragging");
this.createPlaceholder();
}
}
handleTouchMove(e) {
e.preventDefault();
if (this.touchStartElement && this.isDragging) {
const touch = e.touches[0];
// Update dragged element position
const deltaX = touch.clientX - this.touchStartX;
const deltaY = touch.clientY - this.touchStartY;
this.touchStartElement.style.transform = `translate(${deltaX}px, ${deltaY}px) rotate(5deg) scale(1.05)`;
// Calculate insert position and update preview
const insertIndex = this.calculateInsertIndex(
touch.clientX,
touch.clientY,
);
if (insertIndex !== -1) {
this.updatePreviewOrder(insertIndex);
}
}
}
handleTouchEnd(e) {
if (!this.touchStartElement || !this.isDragging) return;
// Commit the preview order
this.currentOrder = [...this.previewOrder];
// Clean up
this.touchStartElement.classList.remove("dragging");
this.touchStartElement.style.transform = "";
this.removePlaceholder();
this.isDragging = false;
this.draggedElement = null;
this.draggedIndex = -1;
this.previewOrder = [];
this.lastInsertIndex = -1;
// Re-render final positions
this.renderSortingCards();
// Success animation
setTimeout(() => {
document.querySelectorAll(".sort-card").forEach((card) => {
card.classList.add("success");
setTimeout(() => card.classList.remove("success"), 600);
});
}, 100);
this.touchStartElement = null;
this.touchStartIndex = -1;
}
checkSolution() {
if (this.placedCards.some((card) => card === null)) {
this.updateSortingStatus(
"Please place all cards before checking your solution.",
);
return;
}
// Get the sequences for comparison
const userSequence = this.placedCards.map((card) => card.number);
const correctSequence = this.correctOrder.map((card) => card.number);
// Calculate fair score using sequence alignment
const scoreResult = this.calculateSequenceScore(
userSequence,
correctSequence,
);
// Update visual feedback for each position
this.placedCards.forEach((card, index) => {
const slot = document.querySelector(`[data-position="${index}"]`);
const isExactMatch =
card.number === this.correctOrder[index].number;
if (isExactMatch) {
slot.classList.add("correct");
slot.classList.remove("incorrect");
} else {
slot.classList.add("incorrect");
slot.classList.remove("correct");
}
});
this.showAdvancedFeedback(userSequence, correctSequence);
}
calculateSequenceScore(userSeq, correctSeq) {
// Calculate Longest Common Subsequence (LCS) score
const lcsLength = this.longestCommonSubsequence(userSeq, correctSeq);
// Calculate how many cards are in correct relative order
const relativeOrderScore = (lcsLength / correctSeq.length) * 100;
// Calculate exact position matches
let exactMatches = 0;
for (let i = 0; i < userSeq.length; i++) {
if (userSeq[i] === correctSeq[i]) {
exactMatches++;
}
}
const exactScore = (exactMatches / correctSeq.length) * 100;
// Calculate inversion count (how "scrambled" the sequence is)
const inversions = this.countInversions(userSeq, correctSeq);
const maxInversions =
(correctSeq.length * (correctSeq.length - 1)) / 2;
const inversionScore = Math.max(
0,
((maxInversions - inversions) / maxInversions) * 100,
);
// Weighted final score:
// - 50% for having cards in correct relative order (LCS)
// - 30% for exact position matches
// - 20% for overall sequence organization (inversions)
const finalScore = Math.round(
relativeOrderScore * 0.5 + exactScore * 0.3 + inversionScore * 0.2,
);
return {
percentage: finalScore,
exactMatches: exactMatches,
correctRelativeOrder: lcsLength,
totalCards: correctSeq.length,
details: {
relativeOrderScore: Math.round(relativeOrderScore),
exactScore: Math.round(exactScore),
inversionScore: Math.round(inversionScore),
inversions: inversions,
},
};
}
longestCommonSubsequence(seq1, seq2) {
const m = seq1.length;
const n = seq2.length;
const dp = Array(m + 1)
.fill()
.map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (seq1[i - 1] === seq2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
countInversions(userSeq, correctSeq) {
// Create a mapping from value to correct position
const correctPositions = {};
correctSeq.forEach((val, idx) => {
correctPositions[val] = idx;
});
// Convert user sequence to "correct position" sequence
const userCorrectPositions = userSeq.map(
(val) => correctPositions[val],
);
// Count inversions in this position sequence
let inversions = 0;
for (let i = 0; i < userCorrectPositions.length; i++) {
for (let j = i + 1; j < userCorrectPositions.length; j++) {
if (userCorrectPositions[i] > userCorrectPositions[j]) {
inversions++;
}
}
}
return inversions;
}
showAdvancedFeedback(userSeq, correctSeq) {
const feedbackEl = document.getElementById("sorting-feedback");
// Calculate advanced scoring metrics
const lcsLength = this.longestCommonSubsequence(userSeq, correctSeq);
const relativeOrderScore = (lcsLength / correctSeq.length) * 100;
// Calculate exact position matches
let exactMatches = 0;
for (
let i = 0;
i < Math.min(userSeq.length, correctSeq.length);
i++
) {
if (userSeq[i] === correctSeq[i]) {
exactMatches++;
}
}
const exactScore = (exactMatches / correctSeq.length) * 100;
// Calculate inversion score
const inversions = this.countInversions(userSeq, correctSeq);
const maxInversions =
(correctSeq.length * (correctSeq.length - 1)) / 2;
const inversionScore = Math.max(
0,
((maxInversions - inversions) / maxInversions) * 100,
);
// Weighted final score
const finalScore = Math.round(
relativeOrderScore * 0.5 + exactScore * 0.3 + inversionScore * 0.2,
);
const isPerfect = finalScore === 100;
let feedbackClass, message;
if (isPerfect) {
feedbackClass = "feedback-perfect";
message = "🎉 Perfect! All cards in correct order!";
} else if (finalScore >= 80) {
feedbackClass = "feedback-good";
message = "👍 Excellent! Very close to perfect!";
} else if (finalScore >= 60) {
feedbackClass = "feedback-good";
message = "👍 Good job! You understand the pattern!";
} else {
feedbackClass = "feedback-needs-work";
message =
"💪 Keep practicing! Focus on reading each abacus carefully.";
}
// Show score in modal instead of inline
this.showScoreModal({
finalScore,
message,
relativeOrderScore,
exactScore,
inversionScore,
lcsLength,
correctSeq,
exactMatches,
feedbackClass,
elapsedTime: this.getElapsedTime(),
});
// Keep inline feedback hidden
feedbackEl.style.display = "none";
this.updateSortingStatus(
isPerfect ? "Perfect solution!" : `${finalScore}% score`,
);
}
revealNumbers() {
// Track revealed state
this.numbersRevealed = true;
document.querySelectorAll(".sort-card").forEach((card) => {
card.classList.add("revealed");
});
document.getElementById("reveal-numbers").style.display = "none";
this.updateSortingStatus(
"Numbers revealed - now you can see the correct order!",
);
}
startTimer() {
this.startTime = Date.now();
this.timerInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
document.getElementById("sorting-timer").textContent =
`${minutes}:${seconds.toString().padStart(2, "0")}`;
}, 1000);
}
stopTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
getElapsedTime() {
if (this.startTime) {
return Math.floor((Date.now() - this.startTime) / 1000);
}
return 0;
}
showScoreModal(scoreData) {
const {
finalScore,
message,
relativeOrderScore,
exactScore,
inversionScore,
lcsLength,
correctSeq,
exactMatches,
feedbackClass,
elapsedTime,
} = scoreData;
const minutes = Math.floor(elapsedTime / 60);
const seconds = elapsedTime % 60;
const timeDisplay = `${minutes}:${seconds.toString().padStart(2, "0")}`;
document.getElementById("score-modal-body").innerHTML = `
<div class="feedback-score">${finalScore}%</div>
<div class="feedback-message">${message}</div>
<div class="feedback-time">⏱️ Time: ${timeDisplay}</div>
<div class="feedback-breakdown">
<div class="score-component">
<span class="component-label">Sequence Order:</span>
<span class="component-score">${Math.round(relativeOrderScore)}%</span>
<span class="component-weight">(50% weight)</span>
</div>
<div class="score-component">
<span class="component-label">Exact Positions:</span>
<span class="component-score">${Math.round(exactScore)}%</span>
<span class="component-weight">(30% weight)</span>
</div>
<div class="score-component">
<span class="component-label">Organization:</span>
<span class="component-score">${Math.round(inversionScore)}%</span>
<span class="component-weight">(20% weight)</span>
</div>
</div>
<div class="feedback-details">
Cards in correct order: ${lcsLength}/${correctSeq.length}
Exact position matches: ${exactMatches}/${correctSeq.length}
</div>
`;
document.getElementById("score-modal").style.display = "flex";
}
hideScoreModal() {
document.getElementById("score-modal").style.display = "none";
}
endSorting() {
// End the current sorting and return to configuration
this.resetSorting();
}
resetSorting() {
// Reset state
this.currentOrder = [];
this.correctOrder = [];
this.placedCards = [];
this.selectedCard = null;
this.selectedCardElement = null;
// Clear feedback
document.getElementById("sorting-feedback").style.display = "none";
// Hide game and sticky header, show configuration controls
document.getElementById("sorting-game").style.display = "none";
document.getElementById("sorting-header").style.display = "none";
document.querySelector(".sorting-controls").style.display = "block";
// Stop timer
this.stopTimer();
// Reset buttons
document.getElementById("start-sorting").style.display =
"inline-block";
document.querySelector(".sorting-game-actions").style.display =
"none";
}
newChallenge() {
// Reset state but stay in game mode
this.currentOrder = [];
this.correctOrder = [];
this.placedCards = [];
this.selectedCard = null;
this.selectedCardElement = null;
this.numbersRevealed = false;
// Clear feedback
document.getElementById("sorting-feedback").style.display = "none";
// Clear card states
document.querySelectorAll(".sort-card").forEach((card) => {
card.classList.remove("correct", "incorrect", "revealed");
});
// Start a new challenge (keeping game mode active)
this.startSorting();
}
updateSortingStatus(message) {
document.getElementById("sorting-status").textContent = message;
}
}
// Matching Pairs Challenge
class MatchingChallenge {
constructor() {
this.cards = [];
this.gameCards = [];
this.flippedCards = [];
this.matchedPairs = 0;
this.totalPairs = 0;
this.moves = 0;
this.selectedPairs = 6;
this.gameStartTime = null;
this.timerInterval = null;
// Two-player mode variables
this.gameMode = "single";
this.turnTimer = 0; // 0 = no timer
this.currentPlayer = 1;
this.player1Score = 0;
this.player2Score = 0;
this.turnTimeLeft = 0;
this.turnTimerInterval = null;
this.playerTurnActive = true;
this.isFirstMove = true;
this.initializeMatching();
this.bindMatchingEvents();
}
initializeMatching() {
// Get available cards (same as quiz cards)
const cardElements = document.querySelectorAll(".flashcard");
this.cards = Array.from(cardElements).map((card) => ({
number: parseInt(card.dataset.number),
svg: card.querySelector(".abacus-container").outerHTML,
}));
}
bindMatchingEvents() {
// Remove existing event listeners to prevent duplicates
document.querySelectorAll(".mode-btn").forEach((btn) => {
// Clone the button to remove all event listeners
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
});
document.querySelectorAll(".timer-btn").forEach((btn) => {
// Clone the button to remove all event listeners
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
});
document.querySelectorAll(".start-game-btn").forEach((btn) => {
// Clone the button to remove all event listeners
const newBtn = btn.cloneNode(true);
btn.parentNode.replaceChild(newBtn, btn);
});
// Mode selection
document.querySelectorAll(".mode-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
document
.querySelectorAll(".mode-btn")
.forEach((b) => b.classList.remove("active"));
e.target.classList.add("active");
this.gameMode = e.target.dataset.mode;
// Show/hide timer controls for two-player mode
const timerControls = document.getElementById("timer-controls");
if (this.gameMode === "two-player") {
timerControls.style.display = "block";
} else {
timerControls.style.display = "none";
}
});
});
// Timer selection
document.querySelectorAll(".timer-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
document
.querySelectorAll(".timer-btn")
.forEach((b) => b.classList.remove("active"));
e.target.classList.add("active");
this.turnTimer = parseInt(e.target.dataset.timer);
});
});
// Grid size selection and immediate game start
document.querySelectorAll(".start-game-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
// Get the pairs count from the clicked button
const target = e.currentTarget; // Use currentTarget to get the button itself
this.selectedPairs = parseInt(target.dataset.pairs);
// Start the game immediately
this.startMatching();
});
});
// Action buttons
document
.getElementById("new-matching")
.addEventListener("click", () => this.newChallenge());
document
.getElementById("end-matching")
.addEventListener("click", () => this.endMatching());
// Score modal event listeners
document
.querySelector(".matching-score-modal-close")
.addEventListener("click", () => this.hideScoreModal());
document
.getElementById("matching-score-modal-close-btn")
.addEventListener("click", () => this.hideScoreModal());
document
.getElementById("matching-score-modal-new-game")
.addEventListener("click", () => {
this.hideScoreModal();
this.newChallenge();
});
// Close modal when clicking outside
document
.getElementById("matching-score-modal")
.addEventListener("click", (e) => {
if (e.target.id === "matching-score-modal") {
this.hideScoreModal();
}
});
}
startMatching() {
// Select random cards for matching
const shuffledCards = [...this.cards].sort(() => Math.random() - 0.5);
const selectedCards = shuffledCards.slice(0, this.selectedPairs);
// Create pairs: abacus and number
this.gameCards = [];
selectedCards.forEach((card) => {
// Add abacus card
this.gameCards.push({
id: `abacus_${card.number}`,
type: "abacus",
number: card.number,
content: card.svg,
matched: false,
});
// Add number card
this.gameCards.push({
id: `number_${card.number}`,
type: "number",
number: card.number,
content: card.number.toString(),
matched: false,
});
});
// Shuffle the game cards
this.gameCards = this.gameCards.sort(() => Math.random() - 0.5);
this.totalPairs = this.selectedPairs;
this.matchedPairs = 0;
this.moves = 0;
this.flippedCards = [];
// Initialize two-player mode variables
if (this.gameMode === "two-player") {
this.currentPlayer = 1;
this.player1Score = 0;
this.player2Score = 0;
this.playerTurnActive = true;
}
// Hide configuration controls, show game
document.querySelector(".matching-controls").style.display = "none";
document.getElementById("matching-game").style.display = "block";
document.getElementById("matching-header").style.display = "block";
this.renderMatchingGrid();
this.adjustCardSizes();
this.startTimer();
this.setupGameInterface();
this.updateMatchingStatus(
this.gameMode === "two-player"
? "Player 1 starts!"
: "Find all matching pairs!",
);
this.updateMatchingStats();
// For two-player mode with timer, show timer but don't start counting until first move
if (this.gameMode === "two-player" && this.turnTimer > 0) {
this.turnTimeLeft = this.turnTimer;
this.updateTurnTimerDisplay();
this.updateTurnTimerStatus("waiting"); // Show timer is waiting for first move
}
}
renderMatchingGrid() {
const grid = document.getElementById("matching-grid");
// Set grid class based on number of pairs
grid.className = "matching-grid";
if (this.selectedPairs === 6) grid.classList.add("grid-3x4");
else if (this.selectedPairs === 8) grid.classList.add("grid-4x4");
else if (this.selectedPairs === 12) grid.classList.add("grid-4x6");
else if (this.selectedPairs === 15) grid.classList.add("grid-5x6");
grid.innerHTML = "";
this.gameCards.forEach((card, index) => {
const cardElement = document.createElement("div");
cardElement.className = "match-card";
cardElement.dataset.index = index;
cardElement.dataset.cardId = card.id;
cardElement.dataset.number = card.number;
const typeClass =
card.type === "abacus" ? "abacus-type" : "number-type";
const typeIcon = card.type === "abacus" ? "🧮" : "🔢";
cardElement.innerHTML = `
<div class="match-card-back ${typeClass}">
<div class="card-type-icon">${typeIcon}</div>
</div>
<div class="match-card-content">
${
card.type === "number"
? `<div class="match-card-number">${card.content}</div>`
: `<div class="match-card-abacus">${card.content}</div>`
}
</div>
`;
cardElement.addEventListener("click", () =>
this.cardClicked(index),
);
grid.appendChild(cardElement);
});
}
cardClicked(index) {
const card = this.gameCards[index];
const cardElement = document.querySelector(`[data-index="${index}"]`);
// Ignore if card is already flipped or matched
if (card.matched || cardElement.classList.contains("flipped")) return;
// Ignore if two cards are already flipped
if (this.flippedCards.length >= 2) return;
// In two-player mode, ignore clicks if it's not the player's turn
if (this.gameMode === "two-player" && !this.playerTurnActive) return;
// Start timer on first move in two-player mode
if (
this.gameMode === "two-player" &&
this.turnTimer > 0 &&
this.isFirstMove
) {
this.isFirstMove = false;
this.startTurnTimer();
this.updateTurnTimerStatus("active");
}
// If one card is already flipped, only allow clicking cards of opposite type
if (this.flippedCards.length === 1) {
const flippedCardType = this.flippedCards[0].card.type;
if (card.type === flippedCardType) {
// Same type - show visual feedback but don't flip
this.showInvalidMoveHint(cardElement);
return;
}
}
// Flip the card
cardElement.classList.add("flipped");
this.flippedCards.push({ index, element: cardElement, card });
// If this is the first card flipped, highlight valid choices
if (this.flippedCards.length === 1) {
this.highlightValidCards(card.type);
}
// If two cards are flipped, check for match
if (this.flippedCards.length === 2) {
this.clearHighlights();
this.stopTurnTimer();
if (this.gameMode === "single") {
this.moves++;
}
this.updateMatchingStats();
setTimeout(() => this.checkMatch(), 1000);
}
}
checkMatch() {
const [first, second] = this.flippedCards;
// Check if numbers match and types are different
if (
first.card.number === second.card.number &&
first.card.type !== second.card.type
) {
// Match found!
first.element.classList.add("matched");
second.element.classList.add("matched");
first.card.matched = true;
second.card.matched = true;
this.matchedPairs++;
// In two-player mode, award point to current player and mark cards
if (this.gameMode === "two-player") {
const playerClass = `matched-player${this.currentPlayer}`;
first.element.classList.add(playerClass);
second.element.classList.add(playerClass);
// Add player badges
this.addPlayerBadge(first.element, this.currentPlayer);
this.addPlayerBadge(second.element, this.currentPlayer);
if (this.currentPlayer === 1) {
this.player1Score++;
} else {
this.player2Score++;
}
// Player gets another turn after a match
this.updatePlayerStats();
} else {
this.updateMatchingStats();
}
// Check if game is complete
if (this.matchedPairs === this.totalPairs) {
this.endGame();
}
} else {
// No match - flip cards back
first.element.classList.remove("flipped");
second.element.classList.remove("flipped");
// In two-player mode, switch turns after a miss
if (this.gameMode === "two-player") {
this.switchPlayer();
}
}
this.flippedCards = [];
this.clearHighlights();
this.playerTurnActive = true; // Re-enable clicks
}
endGame() {
this.stopTimer();
this.stopTurnTimer();
const elapsedTime = Date.now() - this.gameStartTime;
const seconds = Math.floor(elapsedTime / 1000);
if (this.gameMode === "two-player") {
// Two-player results
let winner, description;
if (this.player1Score > this.player2Score) {
winner = "Player 1 Wins!";
description = `${this.player1Score} - ${this.player2Score}`;
} else if (this.player2Score > this.player1Score) {
winner = "Player 2 Wins!";
description = `${this.player2Score} - ${this.player1Score}`;
} else {
winner = "It's a Tie!";
description = `${this.player1Score} - ${this.player2Score}`;
}
this.showScoreModal({
gameMode: "two-player",
winner,
description,
player1Score: this.player1Score,
player2Score: this.player2Score,
totalPairs: this.totalPairs,
time: seconds,
});
} else {
// Single player results
const minimumMoves = this.totalPairs;
const efficiency = this.moves / minimumMoves;
let medal, description;
if (efficiency <= 1.5) {
medal = "🏆 Gold";
description = "Perfect efficiency!";
} else if (efficiency <= 2.0) {
medal = "🥈 Silver";
description = "Great job!";
} else if (efficiency <= 3.0) {
medal = "🥉 Bronze";
description = "Good effort!";
} else {
medal = "🎯 Practice";
description = "Keep practicing!";
}
this.showScoreModal({
gameMode: "single",
pairs: this.totalPairs,
moves: this.moves,
time: seconds,
efficiency: efficiency.toFixed(1),
medal,
description,
});
}
}
showScoreModal(results) {
const modal = document.getElementById("matching-score-modal");
const body = document.getElementById("matching-score-modal-body");
const timeStr = this.formatTime(results.time);
if (results.gameMode === "two-player") {
// Two-player results layout
body.innerHTML = `
<div class="score-summary">
<div class="score-medal">${results.winner}</div>
<div class="score-description">${results.description}</div>
</div>
<div class="two-player-results">
<div class="player-result">
<div class="player-name">🔵 Player 1</div>
<div class="player-final-score" style="color: #007bff;">${results.player1Score} pairs</div>
</div>
<div class="vs-divider">VS</div>
<div class="player-result">
<div class="player-name">🟠 Player 2</div>
<div class="player-final-score" style="color: #fd7e14;">${results.player2Score} pairs</div>
</div>
</div>
<div class="game-summary">
<div class="summary-item">
<span class="summary-label">Total Pairs:</span>
<span class="summary-value">${results.totalPairs}</span>
</div>
<div class="summary-item">
<span class="summary-label">Game Time:</span>
<span class="summary-value">${timeStr}</span>
</div>
</div>
`;
} else {
// Single player results layout
body.innerHTML = `
<div class="score-summary">
<div class="score-medal">${results.medal}</div>
<div class="score-description">${results.description}</div>
</div>
<div class="score-details">
<div class="score-item">
<span class="score-label">Pairs Matched:</span>
<span class="score-value">${results.pairs}</span>
</div>
<div class="score-item">
<span class="score-label">Total Moves:</span>
<span class="score-value">${results.moves}</span>
</div>
<div class="score-item">
<span class="score-label">Time:</span>
<span class="score-value">${timeStr}</span>
</div>
<div class="score-item">
<span class="score-label">Efficiency:</span>
<span class="score-value">${results.efficiency}x</span>
</div>
</div>
<div class="score-explanation">
<h4>Scoring System</h4>
<div class="score-tiers">
<div>🏆 Gold: ≤1.5x minimum moves</div>
<div>🥈 Silver: ≤2.0x minimum moves</div>
<div>🥉 Bronze: ≤3.0x minimum moves</div>
<div>🎯 Practice: >3.0x minimum moves</div>
</div>
</div>
`;
}
modal.style.display = "block";
}
hideScoreModal() {
document.getElementById("matching-score-modal").style.display =
"none";
}
newChallenge() {
this.endMatching();
setTimeout(() => this.startMatching(), 100);
}
endMatching() {
// Stop timer
this.stopTimer();
this.stopTurnTimer();
// Hide game and sticky header, show configuration controls
document.getElementById("matching-game").style.display = "none";
document.getElementById("matching-header").style.display = "none";
document.querySelector(".matching-controls").style.display = "block";
// Reset game state
this.flippedCards = [];
this.matchedPairs = 0;
this.moves = 0;
this.currentPlayer = 1;
this.player1Score = 0;
this.player2Score = 0;
this.playerTurnActive = true;
this.isFirstMove = true;
// Reset mode selection display but preserve selected values
this.updateModeDisplay();
// Re-bind events to ensure they work after DOM changes
this.bindMatchingEvents();
}
updateModeDisplay() {
// Ensure the current game mode button is properly highlighted
document.querySelectorAll(".mode-btn").forEach((btn) => {
btn.classList.remove("active");
if (btn.dataset.mode === this.gameMode) {
btn.classList.add("active");
}
});
// Show/hide timer controls based on current mode
const timerControls = document.getElementById("timer-controls");
if (this.gameMode === "two-player") {
timerControls.style.display = "block";
} else {
timerControls.style.display = "none";
}
// Ensure timer button is highlighted
document.querySelectorAll(".timer-btn").forEach((btn) => {
btn.classList.remove("active");
if (parseInt(btn.dataset.timer) === this.turnTimer) {
btn.classList.add("active");
}
});
}
startTimer() {
this.gameStartTime = Date.now();
this.timerInterval = setInterval(() => {
const elapsed = Date.now() - this.gameStartTime;
const seconds = Math.floor(elapsed / 1000);
document.getElementById("matching-timer").textContent =
this.formatTime(seconds);
}, 1000);
}
stopTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
this.timerInterval = null;
}
}
formatTime(totalSeconds) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
updateMatchingStatus(message) {
document.getElementById("matching-status").textContent = message;
}
updateMatchingStats() {
document.getElementById("moves-counter").textContent =
`Moves: ${this.moves}`;
document.getElementById("pairs-found").textContent =
`Pairs: ${this.matchedPairs}/${this.totalPairs}`;
}
adjustCardSizes() {
// CSS now handles responsive sizing automatically
// This method can be kept for any future dynamic adjustments if needed
// Just ensure font sizes scale with container
const grid = document.getElementById("matching-grid");
const cards = grid.querySelectorAll(".match-card");
cards.forEach((card) => {
const cardRect = card.getBoundingClientRect();
const cardSize = Math.min(cardRect.width, cardRect.height);
// Adjust font sizes proportionally to actual card size
const baseFontSize = Math.max(cardSize * 0.25, 12);
const iconSize = Math.max(cardSize * 0.4, 16);
const number = card.querySelector(".match-card-number");
if (number) number.style.fontSize = `${baseFontSize}px`;
const icon = card.querySelector(".card-type-icon");
if (icon) icon.style.fontSize = `${iconSize}px`;
});
}
showInvalidMoveHint(cardElement) {
// Add temporary visual feedback for invalid moves
cardElement.classList.add("invalid-move");
// Remove the class after animation
setTimeout(() => {
cardElement.classList.remove("invalid-move");
}, 600);
}
highlightValidCards(flippedCardType) {
// Highlight cards of the opposite type
const validType = flippedCardType === "abacus" ? "number" : "abacus";
this.gameCards.forEach((card, index) => {
const cardElement = document.querySelector(
`[data-index="${index}"]`,
);
if (!card.matched && !cardElement.classList.contains("flipped")) {
if (card.type === validType) {
cardElement.classList.add("valid-choice");
} else {
cardElement.classList.add("invalid-choice");
}
}
});
}
clearHighlights() {
// Remove all highlight classes
const cards = document.querySelectorAll(".match-card");
cards.forEach((card) => {
card.classList.remove("valid-choice", "invalid-choice");
});
}
setupGameInterface() {
// Show/hide appropriate UI elements based on game mode
const singleStats = document.querySelector(".matching-stats");
const playerStats = document.getElementById("player-stats");
if (this.gameMode === "two-player") {
singleStats.style.display = "none";
playerStats.style.display = "flex";
this.updatePlayerStats();
// Show turn timer if enabled
const turnTimer = document.getElementById("turn-timer");
if (this.turnTimer > 0) {
turnTimer.style.display = "block";
turnTimer.textContent = `${this.turnTimer}s`;
} else {
turnTimer.style.display = "none";
}
} else {
singleStats.style.display = "flex";
playerStats.style.display = "none";
}
}
updatePlayerStats() {
// Update player scores and current turn indicator
document.querySelector("#player1-info .player-score").textContent =
`${this.player1Score} pairs`;
document.querySelector("#player2-info .player-score").textContent =
`${this.player2Score} pairs`;
// Update active player highlighting
const player1Info = document.getElementById("player1-info");
const player2Info = document.getElementById("player2-info");
const currentTurn = document.getElementById("current-turn");
if (this.currentPlayer === 1) {
player1Info.classList.add("active");
player2Info.classList.remove("active");
currentTurn.textContent = "🔵 Player 1's Turn";
} else {
player1Info.classList.remove("active");
player2Info.classList.add("active");
currentTurn.textContent = "🟠 Player 2's Turn";
}
}
addPlayerBadge(cardElement, player) {
// Create player badge element
const badge = document.createElement("div");
badge.className = `player-badge player${player}`;
badge.textContent = player;
// Add badge to card
cardElement.appendChild(badge);
}
switchPlayer() {
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
this.updatePlayerStats();
this.updateMatchingStatus(`Player ${this.currentPlayer}'s turn`);
// Start turn timer immediately for new player if enabled
if (this.turnTimer > 0) {
this.startTurnTimer();
}
}
startTurnTimer() {
if (this.turnTimer <= 0) return;
this.turnTimeLeft = this.turnTimer;
this.updateTurnTimerDisplay();
this.turnTimerInterval = setInterval(() => {
this.turnTimeLeft--;
this.updateTurnTimerDisplay();
if (this.turnTimeLeft <= 0) {
this.handleTurnTimeout();
}
}, 1000);
}
stopTurnTimer() {
if (this.turnTimerInterval) {
clearInterval(this.turnTimerInterval);
this.turnTimerInterval = null;
}
}
updateTurnTimerDisplay() {
const timerElement = document.getElementById("turn-timer");
if (!timerElement || this.turnTimer <= 0) return;
timerElement.textContent = `${this.turnTimeLeft}s`;
// Add warning/critical classes based on time left
timerElement.classList.remove("warning", "critical");
if (this.turnTimeLeft <= 5) {
timerElement.classList.add("critical");
} else if (this.turnTimeLeft <= 10) {
timerElement.classList.add("warning");
}
}
updateTurnTimerStatus(status) {
const timerElement = document.getElementById("turn-timer");
if (!timerElement) return;
if (status === "waiting") {
timerElement.classList.add("waiting");
const timeString = `${this.turnTimeLeft}s`;
timerElement.textContent = `⏸️ ${timeString}`;
} else if (status === "active") {
timerElement.classList.remove("waiting");
this.updateTurnTimerDisplay();
}
}
handleTurnTimeout() {
this.stopTurnTimer();
this.playerTurnActive = false;
// Clear any flipped cards
if (this.flippedCards.length > 0) {
this.flippedCards.forEach((item) => {
item.element.classList.remove("flipped");
});
this.flippedCards = [];
this.clearHighlights();
}
// Switch to next player
setTimeout(() => {
this.switchPlayer();
this.playerTurnActive = true;
}, 500);
}
}
// Function to handle print requests by opening PDF in print dialog
function handlePrintRequest() {
console.log("Print request intercepted!");
// Get the companion PDF URL
const pdfUrl = window.location.href.replace(/\.html$/, ".pdf");
// Create a modal overlay explaining the situation
const modal = document.createElement("div");
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
`;
const content = document.createElement("div");
content.style.cssText = `
background: white;
border-radius: 8px;
padding: 30px;
max-width: 500px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
`;
const title = document.createElement("h3");
title.textContent = "Print High-Quality Flashcards";
title.style.margin = "0 0 20px 0";
title.style.color = "#333";
const explanation = document.createElement("p");
explanation.innerHTML =
"The web version is not optimized for printing.<br>For best results, download the PDF version which includes:<br><br>• Proper card sizing and layout<br>• Cut marks for easy trimming<br>• Registration marks for alignment";
explanation.style.textAlign = "left";
explanation.style.margin = "0 0 25px 0";
explanation.style.lineHeight = "1.6";
explanation.style.color = "#555";
const buttonContainer = document.createElement("div");
buttonContainer.style.cssText = "display: flex; gap: 15px;";
const downloadButton = document.createElement("button");
downloadButton.textContent = "📄 Open PDF";
downloadButton.style.cssText = `
padding: 12px 24px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 500;
`;
const cancelButton = document.createElement("button");
cancelButton.textContent = "Cancel";
cancelButton.style.cssText = `
padding: 12px 24px;
background: #6c757d;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
`;
// Download button handler
downloadButton.onclick = function () {
showNotification("📄 Opening PDF in new tab...");
window.open(pdfUrl, "_blank");
modal.remove();
};
// Cancel button handler
cancelButton.onclick = function () {
modal.remove();
};
// Close on backdrop click
modal.onclick = function (e) {
if (e.target === modal) {
modal.remove();
}
};
// Prevent clicks inside content from closing modal
content.onclick = function (e) {
e.stopPropagation();
};
// Assemble the modal
buttonContainer.appendChild(downloadButton);
buttonContainer.appendChild(cancelButton);
content.appendChild(title);
content.appendChild(explanation);
content.appendChild(buttonContainer);
modal.appendChild(content);
document.body.appendChild(modal);
}
// Fallback function to download PDF
function downloadPDF(pdfUrl) {
const link = document.createElement("a");
link.href = pdfUrl;
link.download = pdfUrl.split("/").pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Function to show brief notifications
function showNotification(message) {
const notification = document.createElement("div");
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 15px 20px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
animation: slideInFromRight 0.3s ease-out;
`;
notification.textContent = message;
// Add animation styles
const style = document.createElement("style");
style.textContent = `
@keyframes slideInFromRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// Auto-remove after 3 seconds
setTimeout(() => {
notification.style.animation =
"slideInFromRight 0.3s ease-out reverse";
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification);
}
if (style.parentNode) {
document.head.removeChild(style);
}
}, 300);
}, 3000);
}
// Section Navigation Manager
class SectionNavigator {
constructor() {
this.currentSection = "introduction";
this.setupEventListeners();
}
setupEventListeners() {
document.querySelectorAll(".nav-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
const section = e.target.getAttribute("data-section");
this.showSection(section);
});
});
}
showSection(sectionId) {
// Hide all sections
document.querySelectorAll(".page-section").forEach((section) => {
section.classList.remove("active");
});
// Remove active from all nav buttons
document.querySelectorAll(".nav-btn").forEach((btn) => {
btn.classList.remove("active");
});
// Show selected section
const targetSection = document.getElementById(sectionId);
if (targetSection) {
targetSection.classList.add("active");
}
// Activate corresponding nav button
const targetBtn = document.querySelector(
`[data-section="${sectionId}"]`,
);
if (targetBtn) {
targetBtn.classList.add("active");
}
this.currentSection = sectionId;
// Scroll to top when switching sections
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
// Initialize quiz and sorting when DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
new ModalManager();
new SectionNavigator();
new SorobanQuiz();
new SortingChallenge();
new MatchingChallenge();
// Intercept print attempts and download PDF instead
window.addEventListener("beforeprint", (e) => {
e.preventDefault();
handlePrintRequest();
});
// Also intercept Ctrl+P / Cmd+P
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "p") {
e.preventDefault();
handlePrintRequest();
}
});
});
</script>
</body>
</html>