Compare commits

..

6 Commits

Author SHA1 Message Date
semantic-release-bot
dde7ca39cc chore(release): 4.68.2 [skip ci]
## [4.68.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.68.1...v4.68.2) (2025-10-23)

### Bug Fixes

* **complement-race:** prevent passenger delivery render loop causing thrashing ([f637ddf](f637ddfdb8))
2025-10-23 11:01:37 +00:00
Thomas Hallock
f637ddfdb8 fix(complement-race): prevent passenger delivery render loop causing thrashing
Fix render loop that was dispatching DELIVER_PASSENGER hundreds of times
per second when train remained at station, causing:
- Train stoppage despite correct answers
- Violent flashing in passenger HUD list
- Server rejecting moves: "Player does not have this passenger"

Solution:
- Add pendingDeliveryRef to track passengers with pending deliveries
- Check if delivery already dispatched before dispatching again
- Mark passenger as pending immediately before dispatch
- Clean up pending set when passengers are delivered
- Clear pending set on route changes

This mirrors the existing pendingBoardingRef pattern used for boarding.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 06:00:09 -05:00
semantic-release-bot
128da7f3d2 chore(release): 4.68.1 [skip ci]
## [4.68.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.68.0...v4.68.1) (2025-10-23)

### Performance Improvements

* **complement-race:** increase spring animation responsiveness to reduce lag ([5bd0dad](5bd0dadfdf))
2025-10-23 00:51:31 +00:00
Thomas Hallock
5bd0dadfdf perf(complement-race): increase spring animation responsiveness to reduce lag
Increased spring animation config from tension:280/friction:60 to
tension:600/friction:35 for both local and ghost trains. This makes the
animations much more responsive and reduces visual lag behind the actual
game state position.

The previous slow springs caused the visual train to lag noticeably behind
the actual position, especially when answering questions (which adds momentum
and increases speed). The train would appear "stuck" even though position was
updating correctly in the game loop.

The new faster config still provides smooth interpolation to hide 100ms update
jitter, but responds quickly enough to avoid noticeable lag.

Changes:
- useTrainTransforms: Update locomotive and car spring configs to tension:600, friction:35
- GhostTrain: Update locomotive and car spring configs to match local train

Fixes:
- Train now moves immediately when answering questions
- No more "needing multiple answers" to see train movement
- Reduces perceived flashing/stuttering from render lag

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 19:50:16 -05:00
semantic-release-bot
755487c42d chore(release): 4.68.0 [skip ci]
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-10-22)

### Features

* **complement-race:** add react-spring animations to local train for smooth movement ([e5b58c8](e5b58c844c))
2025-10-22 19:11:36 +00:00
Thomas Hallock
e5b58c844c feat(complement-race): add react-spring animations to local train for smooth movement
Apply the same react-spring animation treatment to the local player's train
that was previously added to ghost trains. This eliminates the low-resolution
"choppy" movement by smoothly interpolating between position updates.

Changes:
- Convert useTrainTransforms hook to use react-spring (useSpring, useSprings)
- Update TrainAndCars component to use animated.g and to() interpolation
- Animate locomotive position, rotation, and opacity
- Animate all train cars with individual springs
- Use tension:280, friction:60 config for smooth but responsive movement

Both local and ghost trains now have butter-smooth 60fps interpolated movement.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 14:10:18 -05:00
487 changed files with 13964 additions and 136995 deletions

View File

@@ -19,18 +19,12 @@ yarn-error.log*
# Testing
coverage
.nyc_output
**/__tests__
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
# IDE
.vscode
.idea
*.swp
*.swo
.claude
# OS
.DS_Store
@@ -40,8 +34,6 @@ Thumbs.db
README.md
docs/
*.md
# EXCEPTION: Include blog content markdown files
!apps/web/content/**/*.md
# Python cache
__pycache__
@@ -54,21 +46,7 @@ packages/core/.venv/
# Storybook
storybook-static
**/*.stories.tsx
**/*.stories.ts
.storybook
# Deployment files
nas-deployment/
DEPLOYMENT_PLAN.md
# SQLite database files (created at runtime)
**/data/*.db
**/data/*.db-shm
**/data/*.db-wal
# Build artifacts (rebuilt during Docker build)
**/dist
**/.next
**/build
**/styled-system
DEPLOYMENT_PLAN.md

View File

@@ -1,16 +0,0 @@
{
"mcpServers": {
"sqlite": {
"command": "/Users/antialias/.nvm/versions/node/v20.19.3/bin/npx",
"args": [
"-y",
"mcp-server-sqlite-npx",
"/Users/antialias/projects/soroban-abacus-flashcards/apps/web/data/sqlite.db"
],
"env": {
"PATH": "/Users/antialias/.nvm/versions/node/v20.19.3/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin",
"NODE_PATH": "/Users/antialias/.nvm/versions/node/v20.19.3/lib/node_modules"
}
}
}
}

36884
CHANGELOG.md

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ COPY packages/core/client/node/package.json ./packages/core/client/node/
COPY packages/abacus-react/package.json ./packages/abacus-react/
COPY packages/templates/package.json ./packages/templates/
# Install ALL dependencies for build stage
# Install dependencies (will use .npmrc with hoisted mode)
RUN pnpm install --frozen-lockfile
# Builder stage
@@ -44,79 +44,21 @@ RUN cd apps/web && npx @pandacss/dev
# Build using turbo for apps/web and its dependencies
RUN turbo build --filter=@soroban/web
# Production dependencies stage - install only runtime dependencies
# IMPORTANT: Must use same base as runner stage for binary compatibility (better-sqlite3)
FROM node:18-slim AS deps
WORKDIR /app
# Install build tools temporarily for better-sqlite3 installation
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Install pnpm
RUN npm install -g pnpm@9.15.4
# Copy package files
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
COPY packages/core/client/node/package.json ./packages/core/client/node/
COPY packages/abacus-react/package.json ./packages/abacus-react/
COPY packages/templates/package.json ./packages/templates/
# Install ONLY production dependencies
RUN pnpm install --frozen-lockfile --prod
# Typst builder stage - download and prepare typst binary
FROM node:18-slim AS typst-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
xz-utils \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
TYPST_ARCH="x86_64-unknown-linux-musl"; \
elif [ "$ARCH" = "aarch64" ]; then \
TYPST_ARCH="aarch64-unknown-linux-musl"; \
else \
echo "Unsupported architecture: $ARCH" && exit 1; \
fi && \
TYPST_VERSION="v0.13.0" && \
wget -q "https://github.com/typst/typst/releases/download/${TYPST_VERSION}/typst-${TYPST_ARCH}.tar.xz" && \
tar -xf "typst-${TYPST_ARCH}.tar.xz" && \
mv "typst-${TYPST_ARCH}/typst" /usr/local/bin/typst && \
chmod +x /usr/local/bin/typst
# Production image
FROM node:18-slim AS runner
FROM node:18-alpine AS runner
WORKDIR /app
# Install ONLY runtime dependencies (no build tools)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
qpdf \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy typst binary from typst-builder stage
COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst
# Install Python, pip, build tools for better-sqlite3, Typst, and qpdf (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst qpdf
# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built Next.js application
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./apps/web/.next
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
# Copy blog content (markdown files)
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/content ./apps/web/content
# Copy Panda CSS generated styles
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/styled-system ./apps/web/styled-system
@@ -127,9 +69,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/dist ./apps/web/dist
# Copy database migrations
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/drizzle ./apps/web/drizzle
# Copy PRODUCTION node_modules only (no dev dependencies)
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=deps --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
# Copy node_modules (for dependencies)
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
# Copy core package (needed for Python flashcard generation scripts)
COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
@@ -137,9 +79,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
# Copy templates package (needed for Typst templates)
COPY --from=builder --chown=nextjs:nodejs /app/packages/templates ./packages/templates
# Copy abacus-react package (needed for calendar generation scripts)
COPY --from=builder --chown=nextjs:nodejs /app/packages/abacus-react ./packages/abacus-react
# Install Python dependencies for flashcard generation
RUN pip3 install --no-cache-dir --break-system-packages -r packages/core/requirements.txt
@@ -160,4 +99,4 @@ ENV HOSTNAME "0.0.0.0"
ENV NODE_ENV production
# Start the application
CMD ["node", "server.js"]
CMD ["node", "server.js"]

View File

@@ -1,335 +0,0 @@
# 3D Printing Docker Setup
## Summary
The 3D printable abacus customization feature is fully containerized with optimized Docker multi-stage builds.
**Key Technologies:**
- OpenSCAD 2021.01 (for rendering STL/3MF from .scad files)
- BOSL2 v2.0.0 (minimized library, .scad files only)
- Typst v0.11.1 (pre-built binary)
**Image Size:** ~257MB (optimized with multi-stage builds, saved ~38MB)
**Build Stages:** 7 total (base → builder → deps → typst-builder → bosl2-builder → runner)
## Overview
The 3D printable abacus customization feature requires OpenSCAD and the BOSL2 library to be available in the Docker container.
## Size Optimization Strategy
The Dockerfile uses **multi-stage builds** to minimize the final image size:
1. **typst-builder stage** - Downloads and extracts typst, discards wget/xz-utils
2. **bosl2-builder stage** - Clones BOSL2 and removes unnecessary files (tests, docs, examples, images)
3. **runner stage** - Only copies final binaries and minimized libraries
### Size Reductions
- **Removed from runner**: git, wget, curl, xz-utils (~40MB)
- **BOSL2 minimized**: Removed .git, tests, tutorials, examples, images, markdown files (~2-3MB savings)
- **Kept only .scad files** in BOSL2 library
## Dockerfile Changes
### Build Stages Overview
The Dockerfile now has **7 stages**:
1. **base** (Alpine) - Install build tools and dependencies
2. **builder** (Alpine) - Build Next.js application
3. **deps** (Alpine) - Install production node_modules
4. **typst-builder** (Debian) - Download and extract typst binary
5. **bosl2-builder** (Debian) - Clone and minimize BOSL2 library
6. **runner** (Debian) - Final production image
### Stage 1-3: Base, Builder, Deps (unchanged)
Uses Alpine Linux for building the application (smaller and faster builds).
### Stage 4: Typst Builder (lines 68-87)
```dockerfile
FROM node:18-slim AS typst-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
xz-utils \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN ARCH=$(uname -m) && \
... download and install typst from GitHub releases
```
**Purpose:** Download typst binary in isolation, then discard build tools (wget, xz-utils).
**Result:** Only the typst binary is copied to runner stage (line 120).
### Stage 5: BOSL2 Builder (lines 90-103)
```dockerfile
FROM node:18-slim AS bosl2-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /bosl2 && \
cd /bosl2 && \
git clone --depth 1 --branch v2.0.0 https://github.com/BelfrySCAD/BOSL2.git . && \
# Remove unnecessary files to minimize size
rm -rf .git .github tests tutorials examples images *.md CONTRIBUTING* LICENSE* && \
# Keep only .scad files and essential directories
find . -type f ! -name "*.scad" -delete && \
find . -type d -empty -delete
```
**Purpose:** Clone BOSL2 and aggressively minimize by removing:
- `.git` directory
- Tests, tutorials, examples
- Documentation (markdown files)
- Images
- All non-.scad files
**Result:** Minimized BOSL2 library (~1-2MB instead of ~5MB) copied to runner (line 124).
### Stage 6: Runner - Production Image (lines 106-177)
**Base Image:** `node:18-slim` (Debian) - Required for OpenSCAD availability
**Runtime Dependencies (lines 111-117):**
```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
qpdf \
openscad \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
```
**Removed from runner:**
- ❌ git (only needed in bosl2-builder)
- ❌ wget (only needed in typst-builder)
- ❌ curl (not needed at runtime)
- ❌ xz-utils (only needed in typst-builder)
**Artifacts Copied from Other Stages:**
```dockerfile
# From typst-builder (line 120)
COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst
# From bosl2-builder (line 124)
COPY --from=bosl2-builder /bosl2 /usr/share/openscad/libraries/BOSL2
# From builder (lines 131-159)
# Next.js app, styled-system, server files, etc.
# From deps (lines 145-146)
# Production node_modules only
```
BOSL2 v2.0.0 (minimized) is copied to `/usr/share/openscad/libraries/BOSL2/`, which is OpenSCAD's default library search path. This allows `include <BOSL2/std.scad>` to work in the abacus.scad file.
### Temp Directory for Job Outputs (line 168)
```dockerfile
RUN mkdir -p tmp/3d-jobs && chown nextjs:nodejs tmp
```
Creates the directory where JobManager stores generated 3D files.
## Files Included in Docker Image
The following files are automatically included via the `COPY` command at line 132:
```
apps/web/public/3d-models/
├── abacus.scad (parametric OpenSCAD source)
└── simplified.abacus.stl (base model, 4.8MB)
```
These files are NOT excluded by `.dockerignore`.
## Testing the Docker Build
### Local Testing
1. **Build the Docker image:**
```bash
docker build -t soroban-abacus-test .
```
2. **Run the container:**
```bash
docker run -p 3000:3000 soroban-abacus-test
```
3. **Test OpenSCAD inside the container:**
```bash
docker exec -it <container-id> sh
openscad --version
ls /usr/share/openscad/libraries/BOSL2
```
4. **Test the 3D printing endpoint:**
- Visit http://localhost:3000/3d-print
- Adjust parameters and generate a file
- Monitor job progress
- Download the result
### Verify BOSL2 Installation
Inside the running container:
```bash
# Check OpenSCAD version
openscad --version
# Verify BOSL2 library exists
ls -la /usr/share/openscad/libraries/BOSL2/
# Test rendering a simple file
cd /app/apps/web/public/3d-models
openscad -o /tmp/test.stl abacus.scad
```
## Production Deployment
### Environment Variables
No additional environment variables are required for the 3D printing feature.
### Volume Mounts (Optional)
For better performance and to avoid rebuilding the image when updating 3D models:
```bash
docker run -p 3000:3000 \
-v $(pwd)/apps/web/public/3d-models:/app/apps/web/public/3d-models:ro \
soroban-abacus-test
```
### Disk Space Considerations
- **BOSL2 library**: ~5MB (cloned during build)
- **Base STL file**: 4.8MB (in public/3d-models/)
- **Generated files**: Vary by parameters, typically 1-10MB each
- **Job cleanup**: Old jobs are automatically cleaned up after 1 hour
## Image Size
The final image is Debian-based (required for OpenSCAD), but optimized using multi-stage builds:
**Before optimization (original Debian approach):**
- Base runner: ~250MB
- With all build tools (git, wget, curl, xz-utils): ~290MB
- With BOSL2 (full): ~295MB
- **Total: ~295MB**
**After optimization (current multi-stage approach):**
- Base runner: ~250MB
- Runtime deps only (no build tools): ~250MB
- BOSL2 (minimized, .scad only): ~252MB
- 3D models (STL): ~257MB
- **Total: ~257MB**
**Savings: ~38MB (~13% reduction)**
### What Was Removed
- ❌ git (~15MB)
- ❌ wget (~2MB)
- ❌ curl (~5MB)
- ❌ xz-utils (~1MB)
- ❌ BOSL2 .git directory (~1MB)
- ❌ BOSL2 tests, examples, tutorials (~10MB)
- ❌ BOSL2 images and docs (~4MB)
**Total removed: ~38MB**
This trade-off (Debian vs Alpine) is necessary for OpenSCAD availability, but the multi-stage approach minimizes the size impact.
## Troubleshooting
### OpenSCAD Not Found
If you see "openscad: command not found" in logs:
1. Verify OpenSCAD is installed:
```bash
docker exec -it <container-id> which openscad
docker exec -it <container-id> openscad --version
```
2. Check if the Debian package install succeeded:
```bash
docker exec -it <container-id> dpkg -l | grep openscad
```
### BOSL2 Include Error
If OpenSCAD reports "Can't open library 'BOSL2/std.scad'":
1. Check BOSL2 exists:
```bash
docker exec -it <container-id> ls /usr/share/openscad/libraries/BOSL2/std.scad
```
2. Test include path:
```bash
docker exec -it <container-id> sh -c "cd /tmp && echo 'include <BOSL2/std.scad>; cube(10);' > test.scad && openscad -o test.stl test.scad"
```
### Job Fails with "Permission Denied"
Check tmp directory permissions:
```bash
docker exec -it <container-id> ls -la /app/apps/web/tmp
# Should show: drwxr-xr-x ... nextjs nodejs ... 3d-jobs
```
### Large File Generation Timeout
Jobs timeout after 60 seconds. For complex models, increase the timeout in `jobManager.ts:138`:
```typescript
timeout: 120000, // 2 minutes instead of 60 seconds
```
## Performance Notes
- **Cold start**: First generation takes ~5-10 seconds (OpenSCAD initialization)
- **Warm generations**: Subsequent generations take ~3-5 seconds
- **STL size**: Typically 5-15MB depending on scale parameters
- **3MF size**: Similar to STL (no significant compression)
- **SCAD size**: ~1KB (just text parameters)
## Monitoring
Job processing is logged to stdout:
```
Executing: openscad -o /app/apps/web/tmp/3d-jobs/abacus-abc123.stl ...
Job abc123 completed successfully
```
Check logs with:
```bash
docker logs <container-id> | grep "Job"
```

View File

@@ -33,7 +33,6 @@ In arcade sessions:
The arcade system supports three synchronization patterns:
#### Local Play (No Network Sync)
**Route**: Custom route or dedicated local page
**Use Case**: Practice, offline play, or games that should never be visible to others
@@ -45,7 +44,6 @@ The arcade system supports three synchronization patterns:
- State is NOT shared across the network, only within the browser session
#### Room-Based with Spectator Mode (RECOMMENDED PATTERN)
**Route**: `/arcade/room` (or use room context anywhere)
**Use Case**: Most arcade games - enables spectating even for single-player games
@@ -58,14 +56,12 @@ The arcade system supports three synchronization patterns:
- CAN have multiple ACTIVE PLAYERS per USER (networked + local multiplayer combined)
**✅ This is the PREFERRED pattern** - even for single-player games like Card Sorting, because:
- Enables spectator mode automatically
- Creates social experience ("watch me solve this!")
- No extra code needed
- Works seamlessly with multiplayer games too
#### Pure Multiplayer (Room-Only)
**Route**: `/arcade/room` with validation
**Use Case**: Games that REQUIRE multiple players (e.g., competitive battles)
@@ -78,15 +74,16 @@ The arcade system supports three synchronization patterns:
```typescript
// ❌ WRONG: Always checking for room data
const { roomData } = useRoomData();
useArcadeSession({ roomId: roomData?.id })<// This causes the bug!
// ✅ CORRECT: Explicit mode control via separate providers
LocalMemoryPairsProvider>;
{
/* Never passes roomId */
}
<RoomMemoryPairsProvider>{
/* Always passes roomId */
};
useArcadeSession({ roomId: roomData?.id }) < // This causes the bug!
// ✅ CORRECT: Explicit mode control via separate providers
LocalMemoryPairsProvider >
{
/* Never passes roomId */
} <
RoomMemoryPairsProvider >
{
/* Always passes roomId */
};
```
**Key principle:** The presence of a `roomId` parameter in `useArcadeSession` determines synchronization behavior:
@@ -304,7 +301,6 @@ sendMove({
Spectator mode is automatically enabled when using room-based sync (`roomId: roomData?.id`). Any room member who is not actively playing becomes a spectator and can watch the game in real-time.
**Key Benefits**:
- Creates social/collaborative experience even for single-player games
- "Watch me solve this!" engagement
- Learning by observation
@@ -366,7 +362,6 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
```
**Key Implementation Points**:
- Always check `if (!localPlayerId)` before allowing moves
- Return early or show "Spectating..." message
- Don't throw errors - spectating is a valid state
@@ -418,7 +413,6 @@ For games that support multiple players, show "Join Game" option:
#### 4. Real-Time Updates
Ensure spectators see smooth updates:
- Use optimistic UI updates (same as players)
- Show animations for state changes
- Display current player's moves as they happen
@@ -426,7 +420,6 @@ Ensure spectators see smooth updates:
### When to Use Spectator Mode
**✅ Use Spectator Mode (room-based sync) For**:
- Single-player puzzle games (Card Sorting, Sudoku, etc.)
- Turn-based competitive games (Matching Pairs Battle)
- Cooperative games (Memory Lightning)
@@ -435,7 +428,6 @@ Ensure spectators see smooth updates:
- Classroom settings (teacher demonstrates, students watch)
**❌ Avoid Spectator Mode (use local-only) For**:
- Private practice sessions
- Timed competitive games where watching gives unfair advantage
- Games with personal/sensitive content
@@ -506,32 +498,31 @@ The server must handle spectators correctly:
```typescript
// Validate move ownership
socket.on("game-move", ({ move, roomId }) => {
const session = getSession(roomId);
socket.on('game-move', ({ move, roomId }) => {
const session = getSession(roomId)
// Check if PLAYER making move is in the active players list
if (!session.activePlayers.includes(move.playerId)) {
return {
error: "PLAYER not in game - spectators cannot make moves",
};
error: 'PLAYER not in game - spectators cannot make moves'
}
}
// Check if USER owns this PLAYER
const playerOwner = getPlayerOwner(move.playerId);
const playerOwner = getPlayerOwner(move.playerId)
if (playerOwner !== socket.userId) {
return {
error: "USER does not own this PLAYER",
};
error: 'USER does not own this PLAYER'
}
}
// Valid move - apply and broadcast
const newState = validator.validateMove(session.state, move);
io.to(`game:${roomId}`).emit("state-update", newState); // ALL room members get update
});
const newState = validator.validateMove(session.state, move)
io.to(`game:${roomId}`).emit('state-update', newState) // ALL room members get update
})
```
**Key Server Logic**:
- Validate PLAYER is in `session.activePlayers`
- Validate USER owns PLAYER
- Broadcast to entire room (players + spectators)
@@ -540,37 +531,37 @@ socket.on("game-move", ({ move, roomId }) => {
### Testing Spectator Mode
```typescript
describe("Spectator Mode", () => {
it("should allow room members to spectate single-player games", () => {
describe('Spectator Mode', () => {
it('should allow room members to spectate single-player games', () => {
// Setup: USER A and USER B in same room
// Action: USER A starts Card Sorting (single-player)
// Assert: USER B receives game state updates
// Assert: USER B cannot make moves
// Assert: USER B sees USER A's card placements in real-time
});
})
it("should prevent spectators from making moves", () => {
it('should prevent spectators from making moves', () => {
// Setup: USER A playing, USER B spectating
// Action: USER B attempts to place a card
// Assert: Server rejects move (PLAYER not in activePlayers)
// Assert: Client UI disables controls for USER B
});
})
it("should show spectator indicator in UI", () => {
it('should show spectator indicator in UI', () => {
// Setup: USER B spectating USER A's game
// Assert: UI shows "Spectating [Player Name]" banner
// Assert: Interactive controls are disabled
// Assert: Game state is visible
});
})
it("should allow spectator to join next round", () => {
it('should allow spectator to join next round', () => {
// Setup: USER B spectating USER A's Card Sorting game
// Action: USER A finishes game, returns to setup
// Action: USER B starts new game
// Assert: USER A becomes spectator
// Assert: USER B becomes active player
});
});
})
})
```
### Migration Path

View File

@@ -7,14 +7,12 @@
**Purpose:** The main arcade landing page - displays the "Champion Arena"
**Key Components:**
- `ArcadeContent()` - Renders the main arcade interface
- Uses `EnhancedChampionArena` component which contains `GameSelector`
- The `GameSelector` displays all available games as cards
- `GameSelector` includes both legacy games and registry games
**Current Flow:**
1. User navigates to `/arcade`
2. Page renders `FullscreenProvider` wrapper
3. Displays `PageWithNav` with title "🏟️ Champion Arena"
@@ -25,7 +23,6 @@
8. For legacy games, URL would be direct to their page
**State Management:**
- `GameModeContext` provides player selection (emoji, name, color)
- `PageWithNav` wraps content and provides mini-nav with:
- Active player list
@@ -42,12 +39,10 @@
**Three States:**
### State 1: Loading
- Shows "Loading room..." message
- Waits for `useRoomData()` hook to resolve
### State 2: Game Selection UI (when `!roomData.gameName`)
- Shows large game selection buttons
- User clicks to select a game
- Calls `setRoomGame()` mutation to save selection to room
@@ -58,9 +53,8 @@
4. Game selection is persisted to the room database
### State 3: Game Display (when `roomData.gameName` is set)
- Checks game registry first via `hasGame(roomData.gameName)`
- If registry game:
- If registry game:
- Gets game definition via `getGame(roomData.gameName)`
- Renders: `<Provider><GameComponent /></Provider>`
- Provider and GameComponent come from game registry definition
@@ -69,13 +63,11 @@
- Currently only shows "Game not yet supported"
**Key Hook:**
- `useRoomData()` - Fetches current room from API and subscribes to socket updates
- Returns `roomData` with fields: `id`, `name`, `code`, `gameName`, `gameConfig`, `members`, `memberPlayers`
- Also returns `isLoading` boolean
**Navigation Flow:**
1. User navigates to `/arcade`
2. `GameCard` onClick calls `router.push('/arcade/room?game={gameName}')`
3. User arrives at `/arcade/room`
@@ -91,7 +83,6 @@
The "mini app nav" is actually a sophisticated component within the `PageWithNav` wrapper that intelligently shows different UI based on context:
**Components & Props:**
- `navTitle` - Current page title (e.g., "Champion Arena", "Choose Game", "Speed Complement Race")
- `navEmoji` - Icon emoji for current page
- `gameMode` - Computed from active player count: 'none' | 'single' | 'battle' | 'tournament'
@@ -105,7 +96,6 @@ The "mini app nav" is actually a sophisticated component within the `PageWithNav
**Three Display Modes:**
### Mode 1: Fullscreen Player Selection
- When `showFullscreenSelection === true`
- Displays:
- Large title with emoji
@@ -114,7 +104,6 @@ The "mini app nav" is actually a sophisticated component within the `PageWithNav
- Shows all inactive players for selection
### Mode 2: Solo Mode (NOT in room)
- When `roomInfo` is undefined
- Shows:
- **Game Title Section** (left side):
@@ -126,7 +115,6 @@ The "mini app nav" is actually a sophisticated component within the `PageWithNav
- `AddPlayerButton` - add more players
### Mode 3: Room Mode (IN a room)
- When `roomInfo` is defined
- Shows:
- **Hidden:** Game title section (display: none)
@@ -140,7 +128,6 @@ The "mini app nav" is actually a sophisticated component within the `PageWithNav
- Add player button (for local players only)
**Key Sub-Components:**
- `GameTitleMenu` - Menu for game options (setup, new game, quit)
- `GameModeIndicator` - Shows 🎯 Solo, ⚔️ Battle, 🏆 Tournament, 👥 Select
- `RoomInfo` - Displays room metadata
@@ -151,7 +138,6 @@ The "mini app nav" is actually a sophisticated component within the `PageWithNav
- `PendingInvitations` - Banner for room invitations
**State Management:**
- Lifted from `PageWithNav` to preserve state across remounts:
- `showPopover` / `setShowPopover` - AddPlayerButton popover state
- `activeTab` / `setActiveTab` - 'add' or 'invite' tab selection
@@ -196,7 +182,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameSelector.tsx` (lines 1-112)
**How It Works:**
1. `GameSelector` component gets all games from both sources:
- Legacy `GAMES_CONFIG` (currently empty)
- Registry games via `getAllGames()`
@@ -213,7 +198,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
**Two Game Systems:**
### Registry Games (NEW - Modular)
- Location: `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/arcade-games/`
- File: `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/lib/arcade/game-registry.ts`
- Examples: `complement-race`, `memory-quiz`, `matching`
@@ -221,14 +205,12 @@ User B: Sees same game selection (if set) or game selector (if not set)
- Games registered globally via `registerGame()` function
### Legacy Games (OLD)
- Location: Directly in `/app/arcade/` directory
- Examples: `/app/arcade/complement-race/page.tsx`
- Currently, only complement-race is partially migrated
- Direct URL structure: `/arcade/{gameName}/page.tsx`
**Game Config Structure (for display):**
```javascript
{
name: string, // Display name
@@ -249,11 +231,9 @@ User B: Sees same game selection (if set) or game selector (if not set)
## 6. Key Components Summary
### PageWithNav - Main Layout Wrapper
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/PageWithNav.tsx` (lines 1-192)
**Responsibilities:**
- Wraps all game/arcade pages
- Manages GameContextNav state (mini-nav)
- Handles player configuration dialog
@@ -261,7 +241,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
- Renders top navigation bar via `AppNavBar`
**Key Props:**
- `navTitle` - Passed to GameContextNav
- `navEmoji` - Passed to GameContextNav
- `gameName` - Internal game name for API
@@ -271,16 +250,13 @@ User B: Sees same game selection (if set) or game selector (if not set)
- `children` - Page content
### AppNavBar - Top Navigation Bar
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/AppNavBar.tsx` (lines 1-625)
**Variants:**
- `full` - Standard navigation (default for non-game pages)
- `minimal` - Game navigation (auto-selected for `/arcade` and `/games`)
**Minimal Nav Features:**
- Hamburger menu (left) with:
- Site navigation (Home, Create, Guide, Games)
- Controls (Fullscreen, Exit Arcade)
@@ -289,32 +265,26 @@ User B: Sees same game selection (if set) or game selector (if not set)
- Fullscreen indicator badge
### EnhancedChampionArena - Main Arcade Display
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/EnhancedChampionArena.tsx` (lines 1-40)
**Responsibilities:**
- Container for game selector
- Full-height flex layout
- Passes configuration to `GameSelector`
### GameSelector - Game Grid
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameSelector.tsx` (lines 1-112)
**Responsibilities:**
- Fetches all games from registry
- Arranges in responsive grid
- Shows header "🎮 Available Games"
- Renders GameCard for each game
### GameCard - Individual Game Button
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameCard.tsx` (lines 1-241)
**Responsibilities:**
- Displays game with icon, name, description
- Shows feature chips and player count indicator
- Validates player count against game requirements
@@ -324,25 +294,21 @@ User B: Sees same game selection (if set) or game selector (if not set)
## 7. State Management
### GameModeContext
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/contexts/GameModeContext.tsx` (lines 1-325)
**Manages:**
- Local players (Map<string, Player>)
- Active players (Set<string>)
- Game mode (computed from active player count)
- Player CRUD operations (add, update, remove)
**Key Features:**
- Fetches players from user's local DB via `useUserPlayers()`
- Creates 4 default players if none exist
- When in room: merges room members' players (marked as isLocal: false)
- Syncs to room members via `notifyRoomOfPlayerUpdate()`
**Computed Values:**
- `activePlayerCount` - Size of activePlayers set
- `gameMode`:
- 1 player → 'single'
@@ -350,18 +316,15 @@ User B: Sees same game selection (if set) or game selector (if not set)
- 3+ players → 'tournament'
### useRoomData Hook
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/hooks/useRoomData.ts` (lines 1-450+)
**Manages:**
- Current room fetching via TanStack Query
- Socket.io real-time updates
- Room state (members, players, game name)
- Moderation events (kicked, banned, invitations)
**Key Operations:**
- `fetchCurrentRoom()` - GET `/api/arcade/rooms/current`
- `createRoomApi()` - POST `/api/arcade/rooms`
- `joinRoomApi()` - POST `/api/arcade/rooms/{id}/join`
@@ -369,7 +332,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
- `setRoomGame()` - Updates room's gameName and gameConfig
**Socket Events:**
- `join-user-channel` - Personal notifications
- `join-room` - Subscribe to room updates
- `room-joined` - Refresh when entering room
@@ -398,22 +360,21 @@ User B: Sees same game selection (if set) or game selector (if not set)
```
**Query Parameters:**
- `/arcade/room?game={gameName}` - Optional game selection (parsed by GameCard)
## 9. Key Differences: /arcade vs /arcade/room
| Aspect | /arcade | /arcade/room |
| --------------------- | --------------------------- | ------------------------------------------------- |
| **Purpose** | Game selection hub | Active game display or selection within room |
| **Displays** | GameSelector with all games | Selected game OR game selector if no game in room |
| **Room Context** | Optional (can start solo) | Usually in a room (fetches via useRoomData) |
| **Navigation** | Click game → /arcade/room | Click game → Saves to room → Displays game |
| **GameContextNav** | Shows player selector | Shows room info when joined |
| **Player State** | Local only | Local + remote (room members) |
| **Exit Button** | Usually hidden | Shows "Exit Session" to return to /arcade |
| **Socket Connection** | Optional | Always connected (in room) |
| **Page Transition** | User controls | Driven by room state updates |
| Aspect | /arcade | /arcade/room |
|--------|---------|--------------|
| **Purpose** | Game selection hub | Active game display or selection within room |
| **Displays** | GameSelector with all games | Selected game OR game selector if no game in room |
| **Room Context** | Optional (can start solo) | Usually in a room (fetches via useRoomData) |
| **Navigation** | Click game → /arcade/room | Click game → Saves to room → Displays game |
| **GameContextNav** | Shows player selector | Shows room info when joined |
| **Player State** | Local only | Local + remote (room members) |
| **Exit Button** | Usually hidden | Shows "Exit Session" to return to /arcade |
| **Socket Connection** | Optional | Always connected (in room) |
| **Page Transition** | User controls | Driven by room state updates |
## 10. Planning the Merge (/arcade/room → /arcade)
@@ -453,7 +414,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
**Merge Strategy Options:**
### Option A: Single Route with Modes
```
/arcade
├── Mode: browse (default, show GameSelector)
@@ -462,7 +422,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
```
### Option B: Sub-routes
```
/arcade
├── /arcade (selector)
@@ -471,7 +430,6 @@ User B: Sees same game selection (if set) or game selector (if not set)
```
### Option C: Query-Parameter Driven
```
/arcade
├── /arcade (default - selector)

View File

@@ -1,226 +0,0 @@
# Blog Post Example Generation Pattern
## Overview
We have a **reusable pattern for generating single-problem worksheet examples** for blog posts. This ensures blog post examples use the **exact same rendering code** as the live UI preview, maintaining perfect consistency between documentation and the actual tool.
## The Pattern
### 1. Single Source of Truth
**Location**: `src/app/api/create/worksheets/addition/example/route.ts`
This API route contains the `generateExampleTypst()` function that:
- Takes display options (showCarryBoxes, showTenFrames, etc.)
- Takes specific addends (addend1, addend2)
- Generates a single compact problem using the same Typst helpers as full worksheets
- Compiles to SVG
### 2. Blog Post Generator Scripts
**Pattern**: Copy the `generateExampleTypst()` logic into a script that:
1. Imports `generateTypstHelpers` and `generateProblemStackFunction` from `typstHelpers.ts`
2. Defines examples with specific problems and display options
3. Generates Typst source for each example
4. Compiles to SVG using `typst compile`
5. Saves to `public/blog/[post-name]/`
### 3. Existing Examples
**Ten-frames blog post**:
- Script: `scripts/generateTenFrameExamples.ts`
- Output: `public/blog/ten-frame-examples/`
- Usage: Shows same problem (47 + 38) with/without ten-frames
- Blog post: `content/blog/ten-frames-for-regrouping.md`
**Difficulty progression blog post**:
- Script: `scripts/generateBlogExamples.ts`
- Output: `public/blog/difficulty-examples/`
- Usage: Shows same regrouping level with different scaffolding
- Blog post: `content/blog/beyond-easy-and-hard.md`
## Why This Pattern Matters
### Benefits
1. **Consistency**: Blog examples use the exact same rendering as the live tool
2. **Single Source of Truth**: One set of Typst helpers for both UI and docs
3. **Easy Updates**: When worksheet rendering changes, re-run scripts to update examples
4. **Specific Examples**: Can choose exact problems that demonstrate specific features
5. **Version Control**: Static SVGs committed to repo, no runtime generation needed
### Anti-Pattern (Don't Do This)
**Don't** manually create example SVGs in a design tool
**Don't** screenshot the live UI (inconsistent sizing, quality)
**Don't** duplicate the Typst rendering logic in separate files
**Don't** use the full worksheet generator for blog examples (creates 2x2 grids)
## How to Create New Blog Examples
### Step 1: Create Generator Script
```typescript
// scripts/generateYourFeatureExamples.ts
import fs from 'fs'
import path from 'path'
import { execSync } from 'child_process'
import {
generateTypstHelpers,
generateProblemStackFunction,
} from '../src/app/create/worksheets/addition/typstHelpers'
const outputDir = path.join(process.cwd(), 'public', 'blog', 'your-feature-examples')
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
interface ExampleOptions {
showCarryBoxes?: boolean
showAnswerBoxes?: boolean
showPlaceValueColors?: boolean
showTenFrames?: boolean
showProblemNumbers?: boolean
fontSize?: number
addend1: number
addend2: number
}
function generateExampleTypst(config: ExampleOptions): string {
const a = config.addend1
const b = config.addend2
const fontSize = config.fontSize || 16
const cellSize = 0.45 // Larger than UI preview (0.35) for blog readability
const showCarries = config.showCarryBoxes ?? false
const showAnswers = config.showAnswerBoxes ?? false
const showColors = config.showPlaceValueColors ?? false
const showNumbers = config.showProblemNumbers ?? false
const showTenFrames = config.showTenFrames ?? false
return String.raw`
#set page(width: auto, height: auto, margin: 12pt, fill: white)
#set text(size: ${fontSize}pt, font: "New Computer Modern Math")
#let heavy-stroke = 0.8pt
#let show-ten-frames-for-all = false
${generateTypstHelpers(cellSize)}
${generateProblemStackFunction(cellSize)}
#let a = ${a}
#let b = ${b}
#let aT = calc.floor(calc.rem(a, 100) / 10)
#let aO = calc.rem(a, 10)
#let bT = calc.floor(calc.rem(b, 100) / 10)
#let bO = calc.rem(b, 10)
#align(center + horizon)[
#problem-stack(
a, b, aT, aO, bT, bO,
${showNumbers ? '0' : 'none'},
${showCarries},
${showAnswers},
${showColors},
${showTenFrames},
${showNumbers}
)
]
`
}
const examples = [
{
filename: 'example-1.svg',
description: 'Your feature demonstrated',
options: {
addend1: 47,
addend2: 38,
showCarryBoxes: true,
showAnswerBoxes: true,
showPlaceValueColors: true,
showTenFrames: true,
showProblemNumbers: true,
},
},
// Add more examples...
] as const
for (const example of examples) {
const typstSource = generateExampleTypst(example.options)
const svg = execSync('typst compile --format svg - -', {
input: typstSource,
encoding: 'utf8',
maxBuffer: 2 * 1024 * 1024,
})
fs.writeFileSync(path.join(outputDir, example.filename), svg, 'utf-8')
}
```
### Step 2: Run Generator
```bash
npx tsx scripts/generateYourFeatureExamples.ts
```
### Step 3: Use in Blog Post
```markdown
---
title: "Your Feature Title"
---
## Feature Overview
![Example showing feature](/blog/your-feature-examples/example-1.svg)
*Caption explaining what the example demonstrates.*
```
## Tips for Good Examples
### Problem Selection
- **Choose problems that require the feature**: For ten-frames, use 7+8=15 (requires regrouping)
- **Use simple, clear numbers**: 47 + 38 is better than 387 + 694 for demonstrating basics
- **Show edge cases when relevant**: Double regrouping (57 + 68) shows ten-frames in both columns
### Display Options
- **Minimize non-essential scaffolding**: Turn off unrelated features to focus attention
- **Use consistent options across related examples**: Same colors, same carry boxes, etc.
- **Consider cell size**: Blog examples use 0.45in vs UI preview 0.35in for readability
### File Organization
- **One directory per blog post**: `public/blog/[post-slug]/`
- **Descriptive filenames**: `with-ten-frames.svg`, not `example1.svg`
- **Keep generator script**: Document what examples show and why
## Maintenance
### When to Regenerate Examples
- ✅ When `generateProblemStackFunction()` changes (new rendering logic)
- ✅ When `generateTypstHelpers()` changes (new visual styling)
- ✅ When Typst compiler updates (may affect rendering)
- ❌ When blog post text changes (examples are independent)
### Updating All Examples
```bash
# Regenerate all blog examples
npx tsx scripts/generateBlogExamples.ts
npx tsx scripts/generateTenFrameExamples.ts
# Add more as needed
```
## Reference Implementation
See `scripts/generateTenFrameExamples.ts` for a complete, documented example of this pattern.
Key features demonstrated:
- Clear header documentation explaining the pattern
- Reusable `generateExampleTypst()` function
- Declarative example definitions
- Helpful inline comments explaining problem choices
- Error handling for Typst compilation

View File

@@ -33,7 +33,6 @@ const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
```
**Actual Behavior (CORRECT)**:
- ✅ When a USER plays Card Sorting in a room, the game state SYNCS ACROSS THE ROOM NETWORK
- ✅ This enables **spectator mode** - other room members can watch the game in real-time
- ✅ Card Sorting is single-player (`maxPlayers: 1`), but spectators can watch and cheer
@@ -41,12 +40,10 @@ const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
- ✅ Creates social/collaborative experience ("Watch me solve this!")
**Supported By Architecture** (ARCADE_ARCHITECTURE.md, Spectator Mode section):
> Spectator mode is automatically enabled when using room-based sync (`roomId: roomData?.id`).
> Any room member who is not actively playing becomes a spectator and can watch the game in real-time.
>
> **✅ This is the PREFERRED pattern** - even for single-player games like Card Sorting, because:
>
> - Enables spectator mode automatically
> - Creates social experience ("watch me solve this!")
> - No extra code needed
@@ -57,30 +54,29 @@ const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
```typescript
// For single-player games WITH spectator mode support:
export function CardSortingProvider({ children }: { children: ReactNode }) {
const { data: viewerId } = useViewerId();
const { roomData } = useRoomData(); // ✅ Fetch room data for spectator mode
const { data: viewerId } = useViewerId()
const { roomData } = useRoomData() // ✅ Fetch room data for spectator mode
const { state, sendMove, exitSession } = useArcadeSession<CardSortingState>({
userId: viewerId || "",
roomId: roomData?.id, // ✅ Enable spectator mode - room members can watch
userId: viewerId || '',
roomId: roomData?.id, // ✅ Enable spectator mode - room members can watch
initialState: mergedInitialState,
applyMove: applyMoveOptimistically,
});
})
// Actions check for localPlayerId - spectators won't have one
const startGame = useCallback(() => {
if (!localPlayerId) {
console.warn("[CardSorting] No local player - spectating only");
return; // ✅ Spectators blocked from starting game
console.warn('[CardSorting] No local player - spectating only')
return // ✅ Spectators blocked from starting game
}
// ... send move
}, [localPlayerId, sendMove]);
}, [localPlayerId, sendMove])
}
```
**Why This Pattern is Used**:
This enables spectator mode as a first-class user experience. Room members can:
- Watch other players solve puzzles
- Learn strategies by observation
- Cheer and coach
@@ -105,7 +101,6 @@ memory-quiz/Provider.tsx: const { roomData } = useRoomData()
```
All providers pass `roomId: roomData?.id` to `useArcadeSession`. This means:
-**All games** support spectator mode automatically
-**Single-player games** (card-sorting) enable "watch me play" experience
-**Multiplayer games** (matching, memory-quiz, complement-race) support both players and spectators
@@ -123,14 +118,14 @@ All providers pass `roomId: roomData?.id` to `useArcadeSession`. This means:
The provider correctly uses `useGameMode()` to access active players:
```typescript
const { activePlayers, players } = useGameMode();
const { activePlayers, players } = useGameMode()
const localPlayerId = useMemo(() => {
return Array.from(activePlayers).find((id) => {
const player = players.get(id);
return player?.isLocal !== false;
});
}, [activePlayers, players]);
const player = players.get(id)
return player?.isLocal !== false
})
}, [activePlayers, players])
```
✅ Only includes players with `isActive = true`
@@ -144,18 +139,17 @@ const localPlayerId = useMemo(() => {
**Location**: Provider.tsx lines 383-491 (all move creators)
All moves correctly use:
- `playerId: localPlayerId` (PLAYER makes the move)
- `userId: viewerId || ''` (USER owns the session)
```typescript
// Example from startGame (lines 383-391)
sendMove({
type: "START_GAME",
playerId: localPlayerId, // ✅ PLAYER ID
userId: viewerId || "", // ✅ USER ID
type: 'START_GAME',
playerId: localPlayerId, // ✅ PLAYER ID
userId: viewerId || '', // ✅ USER ID
data: { playerMetadata, selectedCards },
});
})
```
✅ Follows USER/PLAYER distinction correctly
@@ -193,18 +187,14 @@ export class CardSortingValidator implements GameValidator<CardSortingState, Car
Uses the modular game system correctly:
```typescript
export const cardSortingGame = defineGame<
CardSortingConfig,
CardSortingState,
CardSortingMove
>({
export const cardSortingGame = defineGame<CardSortingConfig, CardSortingState, CardSortingMove>({
manifest,
Provider: CardSortingProvider,
GameComponent,
validator: cardSortingValidator,
defaultConfig,
validateConfig: validateCardSortingConfig,
});
})
```
✅ Proper TypeScript generics
@@ -272,7 +262,6 @@ export function GameComponent() {
```
**Also Consider**:
- Show "Join Game" prompt during setup phase for spectators
- Display spectator count ("2 people watching")
- Add smooth real-time animations for spectators
@@ -284,14 +273,12 @@ export function GameComponent() {
All arcade games currently support spectator mode. Consider documenting this in each game's README:
**Games with Spectator Mode**:
-`card-sorting` - Single-player puzzle with spectators
-`matching` - Multiplayer battle with spectators
-`memory-quiz` - Cooperative with spectators
-`complement-race` - Competitive with spectators
**Documentation to Add**:
- How spectator mode works in each game
- Example scenarios (family game night, classroom)
- Best practices for spectator experience
@@ -303,28 +290,28 @@ All arcade games currently support spectator mode. Consider documenting this in
Following ARCADE_ARCHITECTURE.md Spectator Mode section, add tests:
```typescript
describe("Card Sorting - Spectator Mode", () => {
it("should sync state to spectators when USER plays in a room", async () => {
describe('Card Sorting - Spectator Mode', () => {
it('should sync state to spectators when USER plays in a room', async () => {
// Setup: USER A and USER B in same room
// Action: USER A plays Card Sorting
// Assert: USER B (spectator) sees card placements in real-time
// Assert: USER B cannot place cards (no localPlayerId)
});
})
it("should prevent spectators from making moves", () => {
it('should prevent spectators from making moves', () => {
// Setup: USER A playing, USER B spectating
// Action: USER B attempts to place card
// Assert: Action blocked (localPlayerId check)
// Assert: Server rejects if somehow sent
});
})
it("should allow spectator to play after current player finishes", () => {
it('should allow spectator to play after current player finishes', () => {
// Setup: USER A playing, USER B spectating
// Action: USER A finishes, USER B starts new game
// Assert: USER B becomes player
// Assert: USER A becomes spectator
});
});
})
})
```
---
@@ -332,7 +319,6 @@ describe("Card Sorting - Spectator Mode", () => {
### 4. Architecture Documentation
**✅ COMPLETED**: ARCADE_ARCHITECTURE.md has been updated with comprehensive spectator mode documentation:
- Added "SPECTATOR" to core terminology
- Documented three synchronization modes (Local, Room-Based with Spectator, Pure Multiplayer)
- Complete "Spectator Mode" section with:
@@ -380,7 +366,6 @@ Based on ARCADE_ARCHITECTURE.md Spectator Mode Pattern:
## Summary
The Card Sorting Challenge game is **correctly implemented** with:
- ✅ Active players (only `isActive = true` players participate)
- ✅ Player ID vs User ID distinction
- ✅ Validator pattern
@@ -393,7 +378,6 @@ The Card Sorting Challenge game is **correctly implemented** with:
**CORRECT**: Room sync enables spectator mode as a first-class feature
The `roomId: roomData?.id` pattern is **intentional and correct**:
1. ✅ Enables spectator mode automatically
2. ✅ Room members can watch games in real-time
3. ✅ Creates social/collaborative experience
@@ -401,7 +385,6 @@ The `roomId: roomData?.id` pattern is **intentional and correct**:
5. ✅ Follows ARCADE_ARCHITECTURE.md recommended pattern
**Recommended Enhancements** (not critical):
1. Add spectator UI indicators ("👀 Spectating...")
2. Disable controls visually for spectators
3. Add spectator mode tests

View File

@@ -1,449 +0,0 @@
# Card Sorting: Multiplayer & Spectator Features Plan
## Overview
Add collaborative and competitive multiplayer modes to the card-sorting game, plus enhanced spectator experience with real-time player indicators.
---
## 1. Core Feature: Player Emoji on Moving Cards
**When any player (including network players) moves a card, show their emoji on it.**
### Data Structure Changes
#### `CardPosition` type enhancement:
```typescript
export interface CardPosition {
cardId: string;
x: number; // % of viewport width (0-100)
y: number; // % of viewport height (0-100)
rotation: number; // degrees (-15 to 15)
zIndex: number;
draggedByPlayerId?: string; // NEW: ID of player currently dragging this card
}
```
### Implementation Details
1. **When starting drag (local player):**
- Set `draggedByPlayerId` to current player's ID
- Broadcast position update immediately with this field
2. **During drag:**
- Continue including `draggedByPlayerId` in position updates
- Other clients show the emoji overlay
3. **When ending drag:**
- Clear `draggedByPlayerId` (set to `undefined`)
- Broadcast final position without this field
4. **Visual indicator:**
- Show player emoji in top-right corner of card
- Semi-transparent background circle
- Small size (24-28px diameter)
- Positioned absolutely within card container
- Example styling:
```typescript
{
position: 'absolute',
top: '4px',
right: '4px',
width: '28px',
height: '28px',
borderRadius: '50%',
background: 'rgba(255, 255, 255, 0.9)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
border: '2px solid rgba(59, 130, 246, 0.6)',
zIndex: 10
}
```
5. **Access to player metadata:**
- Need to map `playerId` → `PlayerMetadata`
- Current state only has single `playerMetadata`
- For multiplayer, Provider needs to maintain `players: Map<string, PlayerMetadata>`
- Get from room members data
---
## 2. Spectator Mode UI Enhancements
### 2.1 Spectator Banner
**Top banner that clearly indicates spectator status**
```typescript
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: '48px',
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
zIndex: 100,
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
}}>
<div>👀 Spectating: {playerName} {playerEmoji}</div>
<div>Progress: {cardsPlaced}/{totalCards} cards placed</div>
</div>
```
### 2.2 Educational Mode Toggle
**Allow spectators to see the correct answer (for learning)**
- Toggle button in spectator banner
- When enabled: show faint green checkmarks on correctly positioned cards
- Don't show actual numbers unless player revealed them
### 2.3 Player Stats Sidebar
**Show real-time stats (optional, can collapse)**
- Time elapsed
- Cards placed vs. total
- Number of moves made
- Current accuracy (% of cards in correct relative order)
---
## 3. Collaborative Mode: "Team Sort"
### 3.1 Core Mechanics
- Multiple players share the same board and card set
- Anyone can move any card at any time
- Shared timer and shared score
- Team wins/loses together
### 3.2 State Changes
#### `CardSortingState` additions:
```typescript
export interface CardSortingState extends GameState {
// ... existing fields ...
gameMode: "solo" | "collaborative" | "competitive" | "relay"; // NEW
players: Map<string, PlayerMetadata>; // NEW: all active players
activePlayers: string[]; // NEW: players currently in game (not spectators)
}
```
### 3.3 Visual Indicators
1. **Colored cursors for each player:**
- Show a small colored dot/cursor at other players' pointer positions
- Color derived from player's emoji or assigned color
- Update positions via WebSocket (throttled to 30Hz)
2. **Card claiming indicator:**
- When player starts dragging, show their emoji on card (as per feature #1)
- Other players see animated emoji bouncing slightly
- Prevents confusion about who's moving what
3. **Activity feed (optional):**
- Small toast notifications for key actions
- "🎭 Bob placed card #3"
- "🦊 Alice revealed numbers"
- Auto-dismiss after 3 seconds
### 3.4 New Moves
```typescript
// In CardSortingMove union:
| {
type: 'JOIN_COLLABORATIVE_GAME'
playerId: string
userId: string
timestamp: number
data: {
playerMetadata: PlayerMetadata
}
}
| {
type: 'LEAVE_COLLABORATIVE_GAME'
playerId: string
userId: string
timestamp: number
data: Record<string, never>
}
```
### 3.5 Scoring
- Same scoring algorithm but labeled as "Team Score"
- All players see the same results
- Leaderboard entry records all participants
---
## 4. Competitive Mode: "Race Sort"
### 4.1 Core Mechanics
- 2-4 players get the **same** card set
- Each player has their **own separate board**
- Race to finish first OR best score after time limit
- Live leaderboard shows current standings
### 4.2 State Architecture
**Problem:** Current state is single-player only.
**Solution:** Each player needs their own game state, but they're in the same room.
#### Option A: Separate Sessions
- Each competitive player creates their own session
- Room tracks all session IDs
- Client fetches all sessions and displays them
- **Pros:** Minimal changes to game logic
- **Cons:** Complex room management
#### Option B: Multi-Player State (RECOMMENDED)
```typescript
export interface CompetitiveGameState extends GameState {
gameMode: "competitive";
sharedCards: SortingCard[]; // Same cards for everyone
correctOrder: SortingCard[]; // Shared answer
playerBoards: Map<string, PlayerBoard>; // Each player's board state
gameStartTime: number;
gameEndTime: number | null;
winners: string[]; // Player IDs who completed, in order
}
export interface PlayerBoard {
playerId: string;
placedCards: (SortingCard | null)[];
cardPositions: CardPosition[];
availableCards: SortingCard[];
numbersRevealed: boolean;
completedAt: number | null;
scoreBreakdown: ScoreBreakdown | null;
}
```
### 4.3 UI Layout
**Split-screen view:**
```
┌─────────────────────────────────────┐
│ Leaderboard (top bar) │
├──────────────┬──────────────────────┤
│ │ │
│ Your Board │ Opponent Preview │
│ (full size) │ (smaller, ghosted) │
│ │ │
└──────────────┴──────────────────────┘
```
**Your board:**
- Normal interactive gameplay
- Full size, left side
**Opponent preview(s):**
- Right side (or bottom on mobile)
- Smaller scale (50-70% size)
- Semi-transparent cards
- Shows their real-time positions
- Can toggle between different opponents
**Leaderboard bar:**
```
┌─────────────────────────────────────┐
│ 🥇 Alice (5/8) • 🥈 You (4/8) • ... │
└─────────────────────────────────────┘
```
### 4.4 Spectator View for Competitive
- Can watch all players simultaneously
- Grid layout showing all boards
- Highlight current leader with gold border
---
## 5. Hybrid Mode: "Relay Sort" (Future)
### 5.1 Core Mechanics
- Players take turns (30-60 seconds each)
- Cumulative team score
- Can "pass" turn early
- Strategy: communicate via chat about optimal moves
### 5.2 Turn Management
```typescript
export interface RelayGameState extends GameState {
gameMode: "relay";
turnOrder: string[]; // Player IDs
currentTurnIndex: number;
turnStartTime: number;
turnDuration: number; // seconds
// ... rest similar to collaborative
}
```
---
## 6. Implementation Phases
### Phase 1: Foundation (Do First) ✅
- [x] Add `draggedByPlayerId` to `CardPosition`
- [x] Show player emoji on cards being dragged
- [x] Add `players` map to Provider context
- [x] Fetch room members and map to player metadata
### Phase 2: Spectator Enhancements
- [ ] Spectator banner component
- [ ] Educational mode toggle
- [ ] Stats sidebar (collapsible)
### Phase 3: Collaborative Mode
- [ ] Add `gameMode` to state and config
- [ ] Implement JOIN/LEAVE moves
- [ ] Colored cursor tracking
- [ ] Activity feed notifications
- [ ] Team scoring UI
### Phase 4: Competitive Mode
- [ ] Design multi-player state structure
- [ ] Refactor Provider for per-player boards
- [ ] Split-screen UI layout
- [ ] Live leaderboard
- [ ] Ghost opponent preview
- [ ] Winner determination
### Phase 5: Polish & Testing
- [ ] Mobile responsive layouts
- [ ] Performance optimization (many simultaneous players)
- [ ] Network resilience (handle disconnects)
- [ ] Accessibility (keyboard nav, screen readers)
---
## 7. Technical Considerations
### 7.1 WebSocket Message Frequency
- **Current:** Position updates throttled to 100ms (10Hz)
- **Collaborative:** May need higher frequency for smoothness
- **Recommendation:** 50ms (20Hz) for active drag, 100ms otherwise
### 7.2 State Synchronization
- Use optimistic updates for local player
- Reconcile with server state on conflicts
- Use timestamp-based conflict resolution
### 7.3 Player Disconnection Handling
- Collaborative: Keep their last positions, mark as "disconnected"
- Competitive: Pause their timer, allow rejoin within 60s
- Spectators: Just remove from viewer list
### 7.4 Security & Validation
- Server validates all moves (already done)
- Prevent players from seeing others' moves before they happen
- Rate limit position updates per player
---
## 8. Database Schema Changes
### New Tables
#### `competitive_rounds` (for competitive mode)
```sql
CREATE TABLE competitive_rounds (
id UUID PRIMARY KEY,
room_id UUID REFERENCES arcade_rooms(id),
started_at TIMESTAMP,
ended_at TIMESTAMP,
card_set JSON, -- The shared cards
winners JSON -- Array of player IDs in finish order
);
```
#### `player_round_results` (for competitive mode)
```sql
CREATE TABLE player_round_results (
id UUID PRIMARY KEY,
round_id UUID REFERENCES competitive_rounds(id),
player_id UUID,
score_breakdown JSON,
completed_at TIMESTAMP,
final_placement INTEGER -- 1st, 2nd, 3rd, etc.
);
```
---
## 9. Open Questions / Decisions Needed
1. **Collaborative: Card collision handling?**
- What if two players try to grab the same card simultaneously?
- Option A: First one wins, second gets error toast
- Option B: Allow both, last update wins
- **Recommendation:** Option A for better UX
2. **Competitive: Show opponents' exact positions?**
- Option A: Full transparency (see everything)
- Option B: Only show general progress (X/N cards placed)
- Option C: Ghost view (see positions but semi-transparent)
- **Recommendation:** Option C
3. **Spectator limit?**
- Max 10 spectators per game?
- Performance considerations for broadcasting positions
4. **Replay feature?**
- Record all position updates for playback?
- Storage implications?
- **Recommendation:** Future feature, not in initial scope
---
## 10. Success Metrics
- **Engagement:** % of games played in multiplayer vs. solo
- **Completion rate:** Do multiplayer games finish more/less often?
- **Session duration:** How long do multiplayer games last?
- **Return rate:** Do players come back for multiplayer?
- **Social sharing:** Do players invite friends?
---
## Next Steps
1. Get user approval on overall plan
2. Start with Phase 1 (player emoji on cards)
3. Build spectator UI enhancements (Phase 2)
4. Choose between Collaborative or Competitive for Phase 3/4
5. Iterate based on testing and feedback

View File

@@ -18,13 +18,11 @@ The Card Sorting Challenge correctly implements spectator mode functionally - sp
## Current Behavior
**Functional (Correct)**:
- ✅ Actions check `if (!localPlayerId) return` before sending moves
- ✅ Spectators cannot start game, place cards, or check solution
- ✅ Spectators receive real-time state updates
**Missing (UX Gap)**:
- ❌ No visual indicator that user is spectating
- ❌ Buttons appear clickable but don't respond
- ❌ No context about whose game is being watched
@@ -35,46 +33,40 @@ The Card Sorting Challenge correctly implements spectator mode functionally - sp
## Enhancement 1: Expose `localPlayerId` in Context
### Location
`/src/arcade-games/card-sorting/Provider.tsx`
### Changes
**Add to `CardSortingContextValue` interface** (line 14):
```typescript
interface CardSortingContextValue {
state: CardSortingState;
state: CardSortingState
// Actions
startGame: () => void;
placeCard: (cardId: string, position: number) => void;
insertCard: (cardId: string, insertPosition: number) => void;
removeCard: (position: number) => void;
checkSolution: () => void;
revealNumbers: () => void;
goToSetup: () => void;
resumeGame: () => void;
setConfig: (
field: "cardCount" | "showNumbers" | "timeLimit",
value: unknown,
) => void;
exitSession: () => void;
startGame: () => void
placeCard: (cardId: string, position: number) => void
insertCard: (cardId: string, insertPosition: number) => void
removeCard: (position: number) => void
checkSolution: () => void
revealNumbers: () => void
goToSetup: () => void
resumeGame: () => void
setConfig: (field: 'cardCount' | 'showNumbers' | 'timeLimit', value: unknown) => void
exitSession: () => void
// Computed
canCheckSolution: boolean;
placedCount: number;
elapsedTime: number;
hasConfigChanged: boolean;
canResumeGame: boolean;
canCheckSolution: boolean
placedCount: number
elapsedTime: number
hasConfigChanged: boolean
canResumeGame: boolean
// UI state
selectedCardId: string | null;
selectCard: (cardId: string | null) => void;
localPlayerId: string | undefined; // ✨ NEW: Expose for spectator checks
isSpectating: boolean; // ✨ NEW: Derived flag for convenience
selectedCardId: string | null
selectCard: (cardId: string | null) => void
localPlayerId: string | undefined // ✨ NEW: Expose for spectator checks
isSpectating: boolean // ✨ NEW: Derived flag for convenience
}
```
**Update context value** (line 527):
```typescript
const contextValue: CardSortingContextValue = {
state,
@@ -98,13 +90,12 @@ const contextValue: CardSortingContextValue = {
// UI state
selectedCardId,
selectCard: setSelectedCardId,
localPlayerId, // ✨ NEW
isSpectating: !localPlayerId, // ✨ NEW: Convenience flag
};
localPlayerId, // ✨ NEW
isSpectating: !localPlayerId, // ✨ NEW: Convenience flag
}
```
### Rationale
- Components need `localPlayerId` to determine spectator vs player state
- `isSpectating` is a convenience flag to avoid `!localPlayerId` checks everywhere
- Makes spectator mode a first-class concept in the API
@@ -114,13 +105,11 @@ const contextValue: CardSortingContextValue = {
## Enhancement 2: Spectator Indicator Banner
### Location
`/src/arcade-games/card-sorting/components/GameComponent.tsx`
### Visual Design
**Banner Appearance**:
```
┌─────────────────────────────────────────────────────┐
│ 👀 Spectating Alice 👧's game │
@@ -129,7 +118,6 @@ const contextValue: CardSortingContextValue = {
```
**Styling**:
- Background: `rgba(59, 130, 246, 0.1)` (soft blue, semi-transparent)
- Border: `1px solid rgba(59, 130, 246, 0.3)` (blue.500 with opacity)
- Border radius: `8px`
@@ -143,7 +131,6 @@ const contextValue: CardSortingContextValue = {
### Implementation
**Add banner component**:
```typescript
// Add after existing imports
import { useCardSorting } from '../Provider'
@@ -264,13 +251,11 @@ export function GameComponent() {
### Behavior
**Show Banner When**:
-`isSpectating === true` (no local player)
-`state.gamePhase === 'playing'` OR `state.gamePhase === 'results'`
- ❌ NOT during setup phase (handled separately below)
**Hide Banner When**:
- User has an active local player
- Game is in setup phase (use setup phase spectator prompt instead)
@@ -331,9 +316,7 @@ export function SetupPhase() {
## Enhancement 3: Visual Disabled States
### Location
All interactive components in:
- `/src/arcade-games/card-sorting/components/SetupPhase.tsx`
- `/src/arcade-games/card-sorting/components/PlayingPhase.tsx`
- `/src/arcade-games/card-sorting/components/ResultsPhase.tsx`
@@ -341,23 +324,21 @@ All interactive components in:
### Visual Design
**Disabled Button Styling**:
```typescript
const disabledStyles = {
opacity: 0.5,
cursor: "not-allowed",
pointerEvents: "none", // Prevent all interactions
};
cursor: 'not-allowed',
pointerEvents: 'none', // Prevent all interactions
}
```
**Disabled Card Styling**:
```typescript
const disabledCardStyles = {
opacity: 0.6,
cursor: "default",
pointerEvents: "none",
};
cursor: 'default',
pointerEvents: 'none',
}
```
### Implementation by Phase
@@ -367,7 +348,6 @@ const disabledCardStyles = {
**File**: `/src/arcade-games/card-sorting/components/SetupPhase.tsx`
**Changes**:
```typescript
export function SetupPhase() {
const {
@@ -462,7 +442,6 @@ export function SetupPhase() {
**File**: `/src/arcade-games/card-sorting/components/PlayingPhase.tsx`
**Changes**:
```typescript
export function PlayingPhase() {
const {
@@ -588,7 +567,6 @@ export function PlayingPhase() {
**File**: `/src/arcade-games/card-sorting/components/ResultsPhase.tsx`
**Changes**:
```typescript
export function ResultsPhase() {
const {
@@ -645,7 +623,6 @@ export function ResultsPhase() {
## Enhancement 4: Spectator Mode Tests
### Location
Create new file: `/src/arcade-games/card-sorting/__tests__/spectator-mode.test.tsx`
### Test Suite
@@ -917,27 +894,26 @@ describe('Card Sorting - Spectator Mode', () => {
## Enhancement 5: Player Ownership Tests
### Location
Create new file: `/src/arcade-games/card-sorting/__tests__/player-ownership.test.tsx`
### Test Suite
```typescript
import { describe, it, expect } from "vitest";
import { CardSortingValidator } from "../Validator";
import type { CardSortingState, CardSortingMove } from "../types";
import { describe, it, expect } from 'vitest'
import { CardSortingValidator } from '../Validator'
import type { CardSortingState, CardSortingMove } from '../types'
const validator = new CardSortingValidator();
const validator = new CardSortingValidator()
describe("Card Sorting - Player Ownership Validation", () => {
describe('Card Sorting - Player Ownership Validation', () => {
const createMockState = (): CardSortingState => ({
gamePhase: "playing",
playerId: "player_alice",
gamePhase: 'playing',
playerId: 'player_alice',
playerMetadata: {
id: "player_alice",
name: "Alice",
emoji: "👧",
userId: "user_123",
id: 'player_alice',
name: 'Alice',
emoji: '👧',
userId: 'user_123',
},
cardCount: 8,
showNumbers: true,
@@ -945,147 +921,147 @@ describe("Card Sorting - Player Ownership Validation", () => {
gameStartTime: Date.now(),
gameEndTime: null,
selectedCards: [
{ id: "card_1", number: 1, abacus: null },
{ id: "card_2", number: 2, abacus: null },
{ id: 'card_1', number: 1, abacus: null },
{ id: 'card_2', number: 2, abacus: null },
],
correctOrder: [
{ id: "card_1", number: 1, abacus: null },
{ id: "card_2", number: 2, abacus: null },
{ id: 'card_1', number: 1, abacus: null },
{ id: 'card_2', number: 2, abacus: null },
],
availableCards: [
{ id: "card_1", number: 1, abacus: null },
{ id: "card_2", number: 2, abacus: null },
{ id: 'card_1', number: 1, abacus: null },
{ id: 'card_2', number: 2, abacus: null },
],
placedCards: new Array(8).fill(null),
selectedCardId: null,
numbersRevealed: false,
scoreBreakdown: null,
});
})
describe("Player ID Validation", () => {
it("should accept move from correct player", () => {
const state = createMockState();
describe('Player ID Validation', () => {
it('should accept move from correct player', () => {
const state = createMockState()
const move: CardSortingMove = {
type: "PLACE_CARD",
playerId: "player_alice",
userId: "user_123",
data: { cardId: "card_1", position: 0 },
};
type: 'PLACE_CARD',
playerId: 'player_alice',
userId: 'user_123',
data: { cardId: 'card_1', position: 0 },
}
const result = validator.validateMove(state, move, {
activePlayers: ["player_alice"],
playerOwnership: { player_alice: "user_123" },
});
activePlayers: ['player_alice'],
playerOwnership: { player_alice: 'user_123' },
})
expect(result.valid).toBe(true);
});
expect(result.valid).toBe(true)
})
it("should reject move from player not in active players", () => {
const state = createMockState();
it('should reject move from player not in active players', () => {
const state = createMockState()
const move: CardSortingMove = {
type: "PLACE_CARD",
playerId: "player_bob", // Not in activePlayers
userId: "user_456",
data: { cardId: "card_1", position: 0 },
};
type: 'PLACE_CARD',
playerId: 'player_bob', // Not in activePlayers
userId: 'user_456',
data: { cardId: 'card_1', position: 0 },
}
const result = validator.validateMove(state, move, {
activePlayers: ["player_alice"], // Only Alice is active
activePlayers: ['player_alice'], // Only Alice is active
playerOwnership: {
player_alice: "user_123",
player_bob: "user_456",
player_alice: 'user_123',
player_bob: 'user_456',
},
});
})
expect(result.valid).toBe(false);
expect(result.error).toContain("PLAYER not in game");
});
expect(result.valid).toBe(false)
expect(result.error).toContain('PLAYER not in game')
})
it("should reject move when user does not own player", () => {
const state = createMockState();
it('should reject move when user does not own player', () => {
const state = createMockState()
const move: CardSortingMove = {
type: "PLACE_CARD",
playerId: "player_alice",
userId: "user_456", // Wrong user ID
data: { cardId: "card_1", position: 0 },
};
type: 'PLACE_CARD',
playerId: 'player_alice',
userId: 'user_456', // Wrong user ID
data: { cardId: 'card_1', position: 0 },
}
const result = validator.validateMove(state, move, {
activePlayers: ["player_alice"],
playerOwnership: { player_alice: "user_123" }, // Alice owned by user_123
});
activePlayers: ['player_alice'],
playerOwnership: { player_alice: 'user_123' }, // Alice owned by user_123
})
expect(result.valid).toBe(false);
expect(result.error).toContain("USER does not own this PLAYER");
});
expect(result.valid).toBe(false)
expect(result.error).toContain('USER does not own this PLAYER')
})
it("should reject move from spectator (no player ownership)", () => {
const state = createMockState();
it('should reject move from spectator (no player ownership)', () => {
const state = createMockState()
const move: CardSortingMove = {
type: "START_GAME",
playerId: "player_spectator",
userId: "user_999",
type: 'START_GAME',
playerId: 'player_spectator',
userId: 'user_999',
data: {
playerMetadata: {
id: "player_spectator",
name: "Spectator",
emoji: "👀",
userId: "user_999",
id: 'player_spectator',
name: 'Spectator',
emoji: '👀',
userId: 'user_999',
},
selectedCards: [],
},
};
}
const result = validator.validateMove(state, move, {
activePlayers: ["player_alice"], // Spectator not in active players
activePlayers: ['player_alice'], // Spectator not in active players
playerOwnership: {
player_alice: "user_123",
player_spectator: "user_999",
player_alice: 'user_123',
player_spectator: 'user_999',
},
});
})
expect(result.valid).toBe(false);
expect(result.error).toContain("PLAYER not in game");
});
});
expect(result.valid).toBe(false)
expect(result.error).toContain('PLAYER not in game')
})
})
describe("Single Player Game Constraints", () => {
it("should allow only one active player in the game", () => {
const state = createMockState();
describe('Single Player Game Constraints', () => {
it('should allow only one active player in the game', () => {
const state = createMockState()
// Card Sorting is single-player (maxPlayers: 1)
// If somehow multiple players try to join, validator should reject
const move: CardSortingMove = {
type: "START_GAME",
playerId: "player_bob",
userId: "user_456",
type: 'START_GAME',
playerId: 'player_bob',
userId: 'user_456',
data: {
playerMetadata: {
id: "player_bob",
name: "Bob",
emoji: "👦",
userId: "user_456",
id: 'player_bob',
name: 'Bob',
emoji: '👦',
userId: 'user_456',
},
selectedCards: [],
},
};
}
// State already has player_alice playing
const result = validator.validateMove(state, move, {
activePlayers: ["player_alice", "player_bob"], // Two active players
activePlayers: ['player_alice', 'player_bob'], // Two active players
playerOwnership: {
player_alice: "user_123",
player_bob: "user_456",
player_alice: 'user_123',
player_bob: 'user_456',
},
});
})
// Should reject if game is single-player only
// (This depends on validator implementation)
expect(result.valid).toBe(false);
});
});
});
expect(result.valid).toBe(false)
})
})
})
```
---
@@ -1093,14 +1069,12 @@ describe("Card Sorting - Player Ownership Validation", () => {
## Implementation Checklist
### Phase 1: Context Updates
- [ ] Add `localPlayerId` to `CardSortingContextValue` interface
- [ ] Add `isSpectating` to `CardSortingContextValue` interface
- [ ] Expose both in context value object
- [ ] Verify hook exports work correctly
### Phase 2: Spectator Indicators
- [ ] Add spectator banner to `GameComponent.tsx`
- [ ] Add setup phase spectator prompt to `SetupPhase.tsx`
- [ ] Test banner appears for spectators
@@ -1108,7 +1082,6 @@ describe("Card Sorting - Player Ownership Validation", () => {
- [ ] Test player name/emoji displayed correctly
### Phase 3: Disabled States
- [ ] Update `SetupPhase.tsx` buttons with disabled state
- [ ] Update `PlayingPhase.tsx` cards and buttons with disabled state
- [ ] Update `ResultsPhase.tsx` buttons with disabled state
@@ -1116,7 +1089,6 @@ describe("Card Sorting - Player Ownership Validation", () => {
- [ ] Test interactions actually blocked
### Phase 4: Testing
- [ ] Create spectator mode test file
- [ ] Write spectator indicator tests
- [ ] Write disabled controls tests
@@ -1128,7 +1100,6 @@ describe("Card Sorting - Player Ownership Validation", () => {
- [ ] All tests pass
### Phase 5: Quality & Deploy
- [ ] Run `npm run pre-commit` (format, lint, type-check)
- [ ] Manual testing: Join room as spectator
- [ ] Manual testing: Verify banner appears
@@ -1143,7 +1114,6 @@ describe("Card Sorting - Player Ownership Validation", () => {
## Visual Examples
### Spectating During Playing Phase
```
┌───────────────────────────────────────────────────────┐
│ 👀 Spectating Alice 👧's game │
@@ -1164,7 +1134,6 @@ describe("Card Sorting - Player Ownership Validation", () => {
```
### Spectating During Setup Phase
```
┌───────────────────────────────────────────────────────┐
│ 👤 Add a Player to Start │
@@ -1186,27 +1155,23 @@ describe("Card Sorting - Player Ownership Validation", () => {
## Success Criteria
**User Experience**:
- Spectators immediately know they're watching, not playing
- All interactive controls clearly disabled
- Spectators can see whose game they're watching
- Clear call-to-action to add a player to join
**Functional**:
- No moves sent from spectators (existing behavior maintained)
- Real-time state updates visible to spectators
- Context correctly exposes spectator state
**Code Quality**:
- All tests pass
- TypeScript types correct
- Pre-commit checks pass
- No regressions in player functionality
**Accessibility**:
- Disabled buttons use `disabled` attribute (not just styling)
- Screen readers announce disabled state
- Color contrast meets WCAG AA standards

View File

@@ -1,153 +1,5 @@
# Claude Code Instructions for apps/web
## CRITICAL: Production Dependencies
**NEVER add TypeScript execution tools to production dependencies.**
### Forbidden Production Dependencies
The following packages must ONLY be in `devDependencies`, NEVER in `dependencies`:
-`tsx` - TypeScript execution (only for scripts during development)
-`ts-node` - TypeScript execution
- ❌ Any TypeScript compiler/executor that runs .ts/.tsx files at runtime
### Why This Matters
1. **Docker Image Size**: These tools add 50-100MB+ to production images
2. **Security**: Running TypeScript at runtime is a security risk
3. **Performance**: Production should run compiled JavaScript, not interpret TypeScript
4. **Architecture**: If you need TypeScript at runtime, the code is in the wrong place
### What To Do Instead
**❌ WRONG - Adding tsx to dependencies to run .ts/.tsx at runtime:**
```json
{
"dependencies": {
"tsx": "^4.20.5" // NEVER DO THIS
}
}
```
**✅ CORRECT - Move code to proper location:**
1. **For Next.js API routes**: Move files to `src/` so Next.js bundles them during build
- Example: `scripts/generateCalendar.tsx``src/utils/calendar/generateCalendar.tsx`
- Next.js will compile and bundle these during `npm run build`
2. **For standalone scripts**: Keep in `scripts/` and use `tsx` from devDependencies
- Only run during development/build, never at runtime
- Scripts can use `tsx` because it's available during build
3. **For server-side TypeScript**: Compile to JavaScript during build
- Use `tsc` to compile `src/` to `dist/`
- Production runs the compiled JavaScript from `dist/`
### Historical Context
**We've made this mistake TWICE:**
1. **First time (commit ffae9c1b)**: Added tsx to dependencies for calendar generation scripts
- **Fix**: Moved scripts to `src/utils/calendar/` so Next.js bundles them
2. **Second time (would have happened again)**: Almost added tsx again for same reason
- **Learning**: If you're tempted to add tsx to dependencies, the architecture is wrong
### Red Flags
If you find yourself thinking:
- "I need to add tsx to dependencies to run this .ts file in production"
- "This script needs TypeScript at runtime"
- "Production can't import this .tsx file"
**STOP.** The code is in the wrong place. Move it to `src/` for bundling.
### Enforcement
Before modifying `package.json` dependencies:
1. Check if any TypeScript execution tools are being added
2. Ask yourself: "Could this code be in `src/` instead?"
3. If unsure, ask the user before proceeding
## CRITICAL: Code Factoring - Never Fork, Always Factor
**When told to share code between files, NEVER copy/paste. ALWAYS extract to shared utility.**
### The Mistake (Made Multiple Times)
When implementing addition worksheet preview examples, I was told **THREE TIMES** to factor out the problem rendering code:
- "the example should be closely associated in the codebase semantically with the template"
- "just be sure to factor, not fork"
- "we need to be showing exactly what the worksheet template uses"
**What I did wrong:** Copied the Typst problem rendering code from `typstGenerator.ts` to `example/route.ts`
**Why this is wrong:**
- Changes to worksheet layout won't reflect in preview
- Maintaining two copies guarantees they'll drift apart
- Violates DRY (Don't Repeat Yourself)
- The user explicitly said "factor, not fork"
### What To Do Instead
**✅ CORRECT - Extract to shared function:**
1. Create shared function in `typstHelpers.ts`:
```typescript
export function generateProblemBoxFunction(cellSize: number): string {
// Returns the Typst function definition that both files can use
return `#let problem-box(problem, index) = { ... }`;
}
```
2. Both `typstGenerator.ts` and `example/route.ts` import and use it:
```typescript
import { generateProblemBoxFunction } from './typstHelpers'
// In Typst template:
${generateProblemBoxFunction(cellSize)}
// Then call it:
#problem-box((a: 45, b: 27), 0)
```
**❌ WRONG - Copy/paste the code:**
```typescript
// typstGenerator.ts
const template = `#let problem-box = { ... }`; // ← Original
// example/route.ts
const template = `#let problem-box = { ... }`; // ← Copy/paste = FORKED CODE
```
### Red Flags
If you find yourself:
- Copying large blocks of code between files
- Saying "I'll make it match the other file"
- Maintaining "two versions" of the same logic
**STOP.** Extract to a shared utility function.
### Rule of Thumb
When the user says "factor" or "share code" or "use the same template":
1. Find the common code
2. Extract to shared function in appropriate utility file
3. Import and call that function from both places
4. The shared function should be the SINGLE SOURCE OF TRUTH
## MANDATORY: Quality Checks for ALL Work
**BEFORE declaring ANY work complete, fixed, or working**, you MUST run and pass these checks:
@@ -173,21 +25,6 @@ This single command runs all quality checks in the correct order:
**DO NOT COMMIT** until all checks pass with zero errors and zero warnings.
## Blog Post Examples
**REUSABLE PATTERN: Generating single-problem examples for blog posts**
We have a **single-problem example generator** used for both the UI preview and blog post examples. This ensures blog examples use the **exact same rendering** as the live tool.
See `.claude/BLOG_EXAMPLES_PATTERN.md` for complete documentation.
**Quick reference:**
- UI preview API: `src/app/api/create/worksheets/addition/example/route.ts`
- Blog generators: `scripts/generateTenFrameExamples.ts`, `scripts/generateBlogExamples.ts`
- Shared code: `src/app/create/worksheets/addition/typstHelpers.ts`
**Key benefit**: Blog examples stay in sync with actual worksheet rendering. When rendering changes, just re-run the generator scripts.
## Available Scripts
```bash
@@ -207,26 +44,14 @@ When asked to make ANY changes:
1. Make your code changes
2. Run `npm run pre-commit`
3. If it fails, fix the issues and run again
4. **STOP - Tell user changes are ready for testing**
5. **WAIT for user to manually test and approve**
6. Only commit/push when user explicitly approves or requests it
4. Only after all checks pass can you:
- Say the work is "done" or "complete"
- Mark tasks as finished
- Create commits
- Tell the user it's working
5. Push immediately after committing
**CRITICAL:** Passing `npm run pre-commit` only verifies code quality (TypeScript, linting, formatting). It does NOT verify that features work correctly. Manual testing by the user is REQUIRED before committing.
**Never auto-commit or auto-push after making changes.**
## Dev Server Management
**CRITICAL: The user manages running the dev server, NOT Claude Code.**
- ❌ DO NOT run `pnpm dev`, `npm run dev`, or `npm start`
- ❌ DO NOT attempt to start, stop, or restart the dev server
- ❌ DO NOT kill processes on port 3000
- ❌ DO NOT use background Bash processes for the dev server
- ✅ Make code changes and let the user restart the server when needed
- ✅ You may run other commands like `npm run type-check`, `npm run lint`, etc.
**The user runs the dev server themselves.** The user will manually start/restart the dev server after you make changes.
**Nothing is complete until `npm run pre-commit` passes.**
## Details
@@ -276,78 +101,26 @@ npm run check # Biome check (format + lint + organize imports)
- Token syntax: `color: 'blue.200'`, `borderColor: 'gray.300'`, etc.
**Common Mistakes to Avoid:**
- ❌ Don't reference "Tailwind" in code, comments, or documentation
- ❌ Don't use Tailwind utility classes (e.g., `className="bg-blue-500"`)
- ✅ Use Panda CSS `css()` function for all styling
- ✅ Use Panda's token system (defined in `panda.config.ts`)
**Color Tokens:**
```typescript
// Correct (Panda CSS)
css({
bg: "blue.200",
borderColor: "gray.300",
color: "brand.600",
});
bg: 'blue.200',
borderColor: 'gray.300',
color: 'brand.600'
})
// Incorrect (Tailwind)
className = "bg-blue-200 border-gray-300 text-brand-600";
className="bg-blue-200 border-gray-300 text-brand-600"
```
See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
## Data Attributes for All Elements
**MANDATORY: All new elements MUST have data attributes for easy reference.**
When creating ANY new HTML/JSX element (div, button, section, etc.), add appropriate data attributes:
**Required patterns:**
- `data-component="component-name"` - For top-level component containers
- `data-element="element-name"` - For major UI elements
- `data-section="section-name"` - For page sections
- `data-action="action-name"` - For interactive elements (buttons, links)
- `data-setting="setting-name"` - For game settings/config elements
- `data-status="status-value"` - For status indicators
**Why this matters:**
- Allows easy element selection for testing, debugging, and automation
- Makes it simple to reference elements by name in discussions
- Provides semantic meaning beyond CSS classes
- Enables reliable E2E testing selectors
**Examples:**
```typescript
// Component container
<div data-component="game-board" className={css({...})}>
// Interactive button
<button data-action="start-game" onClick={handleStart}>
// Settings toggle
<div data-setting="sound-enabled">
// Status indicator
<div data-status={isOnline ? 'online' : 'offline'}>
```
**DO NOT:**
- ❌ Skip data attributes on new elements
- ❌ Use generic names like `data-element="div"`
- ❌ Use data attributes for styling (use CSS classes instead)
**DO:**
- ✅ Use descriptive, kebab-case names
- ✅ Add data attributes to ALL significant elements
- ✅ Make names semantic and self-documenting
## Abacus Visualizations
**CRITICAL: This project uses @soroban/abacus-react for all abacus visualizations.**
@@ -359,75 +132,23 @@ When creating ANY new HTML/JSX element (div, button, section, etc.), add appropr
- DO NOT manually draw abacus columns, beads, or bars
**Common Mistakes to Avoid:**
- ❌ Don't create custom abacus components or SVGs
- ❌ Don't manually render abacus beads or columns
- ✅ Always use `AbacusReact` from `@soroban/abacus-react`
- ✅ Use `useAbacusConfig` for abacus configuration
- ✅ Use `useAbacusDisplay` for reading abacus state
**Server-Side Rendering (CRITICAL):**
`AbacusReact` already supports server-side rendering - it detects SSR and disables animations automatically.
**✅ CORRECT - Use in build scripts:**
```typescript
// scripts/generateAbacusIcons.tsx
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { AbacusReact } from '@soroban/abacus-react'
const svg = renderToStaticMarkup(<AbacusReact value={5} columns={2} />)
// This works! Scripts can use react-dom/server
```
**❌ WRONG - Do NOT use in Next.js route handlers:**
```typescript
// src/app/icon/route.tsx - DON'T DO THIS!
import { renderToStaticMarkup } from 'react-dom/server' // ❌ Next.js forbids this!
import { AbacusReact } from '@soroban/abacus-react'
export async function GET() {
const svg = renderToStaticMarkup(<AbacusReact ... />) // ❌ Will fail!
}
```
**✅ CORRECT - Pre-generate and read in route handlers:**
```typescript
// src/app/icon/route.tsx
import { readFileSync } from "fs";
export async function GET() {
// Read pre-generated SVG from scripts/generateAbacusIcons.tsx
const svg = readFileSync("public/icons/day-01.svg", "utf-8");
return new Response(svg, { headers: { "Content-Type": "image/svg+xml" } });
}
```
**Pattern to follow:**
1. Generate static SVGs using `scripts/generateAbacusIcons.tsx` (uses renderToStaticMarkup)
2. Commit generated SVGs to `public/icons/` or `public/`
3. Route handlers read and serve the pre-generated files
4. Regenerate icons when abacus styling changes
**MANDATORY: Read the Docs Before Customizing**
**ALWAYS read the full README documentation before customizing or styling AbacusReact:**
- Location: `packages/abacus-react/README.md`
- Check homepage implementation: `src/app/page.tsx` (MiniAbacus component)
- Check storybook examples: `src/stories/AbacusReact.*.stories.tsx`
**Key Documentation Points:**
1. **Custom Styles**: Use `fill` (not just `stroke`) for columnPosts and reckoningBar
2. **Props**: Use direct props like `value`, `columns`, `scaleFactor` (not config objects)
3. **Example from Homepage:**
```typescript
const darkStyles = {
columnPosts: {
@@ -450,7 +171,6 @@ export async function GET() {
```
**Example Usage:**
```typescript
import { AbacusReact } from '@soroban/abacus-react'
@@ -462,7 +182,6 @@ import { AbacusReact } from '@soroban/abacus-react'
### @soroban/abacus-react TypeScript Module Resolution
**Issue:** TypeScript reports that `AbacusReact`, `useAbacusConfig`, and other exports do not exist from the `@soroban/abacus-react` package, even though:
- The package builds successfully
- The exports are correctly defined in `dist/index.d.ts`
- The imports work at runtime
@@ -471,7 +190,6 @@ import { AbacusReact } from '@soroban/abacus-react'
**Impact:** `npm run type-check` will report errors for any files importing from `@soroban/abacus-react`.
**Workaround:** This is a known pre-existing issue. When running pre-commit checks, TypeScript errors related to `@soroban/abacus-react` imports can be ignored. Focus on:
- New TypeScript errors in your changed files (excluding @soroban/abacus-react imports)
- Format checks
- Lint checks
@@ -500,7 +218,6 @@ When working on arcade room game settings, refer to:
Settings are stored as: `gameConfig[gameName][setting]`
Three places must handle settings correctly:
1. **Provider** (`Room{Game}Provider.tsx`) - Merges saved config with defaults
2. **Socket Server** (`socket-server.ts`) - Creates session from saved config
3. **Validator** (`{Game}Validator.ts`) - `getInitialState()` must accept ALL settings
@@ -522,23 +239,21 @@ When working with z-index values or encountering layering issues, refer to:
**Quick Reference:**
**ALWAYS use the constants file:**
```typescript
import { Z_INDEX } from "@/constants/zIndex";
import { Z_INDEX } from '@/constants/zIndex'
// ✅ Good
zIndex: Z_INDEX.NAV_BAR;
zIndex: Z_INDEX.MODAL;
zIndex: Z_INDEX.TOOLTIP;
zIndex: Z_INDEX.NAV_BAR
zIndex: Z_INDEX.MODAL
zIndex: Z_INDEX.TOOLTIP
// ❌ Bad - magic numbers!
zIndex: 100;
zIndex: 10000;
zIndex: 500;
zIndex: 100
zIndex: 10000
zIndex: 500
```
**Layering hierarchy:**
- Base content: 0-99
- Navigation/UI chrome: 100-999
- Overlays/dropdowns/tooltips: 1000-9999
@@ -550,165 +265,6 @@ zIndex: 500;
Z-index values are only compared within the same stacking context! Elements with `position + zIndex`, `opacity < 1`, `transform`, or `filter` create new stacking contexts where child z-indexes are relative, not global.
Before setting a z-index, always check:
1. What stacking context is this element in?
2. Am I comparing against siblings or global elements?
3. Does my parent create a stacking context?
## Database Access
This project uses SQLite with Drizzle ORM. Database location: `./data/sqlite.db`
**ALWAYS use MCP SQLite tools for database operations:**
- `mcp__sqlite__list_tables` - List all tables
- `mcp__sqlite__describe_table` - Get table schema
- `mcp__sqlite__read_query` - Run SELECT queries
- `mcp__sqlite__write_query` - Run INSERT/UPDATE/DELETE queries
- `mcp__sqlite__create_table` - Create new tables
- **DO NOT use bash `sqlite3` commands** - use the MCP tools instead
**Database Schema:**
- Schema definitions: `src/db/schema/`
- Drizzle config: `drizzle.config.ts`
- Migrations: `drizzle/` directory
### Creating Database Migrations
**CRITICAL: NEVER manually create migration SQL files or edit the journal.**
When adding/modifying database schema:
1. **Update the schema file** in `src/db/schema/`:
```typescript
// Example: Add new column to existing table
export const abacusSettings = sqliteTable("abacus_settings", {
userId: text("user_id").primaryKey(),
// ... existing columns ...
newField: integer("new_field", { mode: "boolean" })
.notNull()
.default(false),
});
```
2. **Generate migration using drizzle-kit**:
```bash
npx drizzle-kit generate --custom
```
This creates:
- A new SQL file in `drizzle/####_name.sql`
- Updates `drizzle/meta/_journal.json`
- Creates a snapshot in `drizzle/meta/####_snapshot.json`
3. **Edit the generated SQL file** (it will be empty):
```sql
-- Custom SQL migration file, put your code below! --
ALTER TABLE `abacus_settings` ADD `new_field` integer DEFAULT 0 NOT NULL;
```
4. **Test the migration** on your local database:
```bash
npm run db:migrate
```
5. **Verify** the column was added:
```bash
mcp__sqlite__describe_table table_name
```
**What NOT to do:**
- ❌ DO NOT manually create SQL files in `drizzle/` without using `drizzle-kit generate`
- ❌ DO NOT manually edit `drizzle/meta/_journal.json`
- ❌ DO NOT run SQL directly with `sqlite3` command
- ❌ DO NOT use `drizzle-kit generate` without `--custom` flag (it requires interactive prompts)
**Why this matters:**
- Drizzle tracks applied migrations in `__drizzle_migrations` table
- Manual SQL files won't be tracked properly
- Production deployments run `npm run db:migrate` automatically
- Improperly created migrations will fail in production
## Deployment Verification
**CRITICAL: Never assume deployment is complete just because the website is accessible.**
**Deployment System:** The NAS uses `compose-updater` (NOT Watchtower) for automatic deployments. See `.claude/DEPLOYMENT.md` for complete documentation.
When monitoring deployments to production (NAS at abaci.one):
1. **GitHub Actions Success ≠ NAS Deployment**
- GitHub Actions builds and pushes Docker images to GHCR
- compose-updater checks for new images every 5 minutes and auto-deploys
- There is a 5-7 minute delay between GitHub Actions completing and NAS deployment
2. **Always verify the deployed commit:**
```bash
# Check what's actually running on production
ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format="{{index .Config.Labels \"org.opencontainers.image.revision\"}}"'
# Or check the deployment info modal in the app UI
# Look for the "Commit" field and compare to current HEAD
```
3. **Compare commits explicitly:**
```bash
# Current HEAD
git rev-parse HEAD
# If NAS deployed commit doesn't match HEAD, deployment is INCOMPLETE
```
4. **Never report "deployed successfully" unless:**
- ✅ GitHub Actions completed
- ✅ NAS commit SHA matches origin/main HEAD
- ✅ Website is accessible AND serving the new code
5. **If commits don't match:**
- Report the gap clearly: "NAS is X commits behind origin/main"
- List what features are NOT yet deployed
- Note that compose-updater should pick it up within 5 minutes
**Force immediate deployment:**
```bash
# Restart compose-updater to trigger immediate check (instead of waiting up to 5 minutes)
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose -f docker-compose.updater.yaml restart"
```
**Common mistake:** Seeing https://abaci.one is online and assuming the new code is deployed. Always verify the commit SHA.
## Rithmomachia Game
When working on the Rithmomachia arcade game, refer to:
- **`src/arcade-games/rithmomachia/SPEC.md`** - Complete game specification
- Official implementation spec v1
- Board dimensions (8×16), piece types, movement rules
- Mathematical capture relations (equality, sum, difference, multiple, divisor, product, ratio)
- Harmony (progression) victory conditions
- Data models, server protocol, validation logic
- Test cases and UI/UX suggestions
**Quick Reference:**
- **Board**: 8 rows × 16 columns (A-P, 1-8)
- **Pieces per side**: 25 total (12 Circles, 6 Triangles, 6 Squares, 1 Pyramid)
- **Movement**: Geometric (C=diagonal, T=orthogonal, S=queen, P=king)
- **Captures**: Mathematical relations between piece values
- **Victory**: Harmony (3+ pieces in enemy half forming arithmetic/geometric/harmonic progression), exhaustion, or optional point threshold
**Critical Rules**:
- All piece values are positive integers (use `number`, not `bigint` for game state serialization)
- No jumping - pieces must have clear paths
- Captures require valid mathematical relations (use helper pieces for sum/diff/product/ratio)
- Pyramid pieces have 4 faces - face value must be chosen during relation checks

View File

@@ -12,7 +12,6 @@ I used the **correct modular game pattern** (useArcadeSession) but **threw away
### The Correct Pattern (Used by ALL Modular Games)
**Pattern: useArcadeSession** (from GAME_MIGRATION_PLAYBOOK.md)
```typescript
// Uses useArcadeSession with action creators
export function YourGameProvider({ children }) {
@@ -44,7 +43,6 @@ export function YourGameProvider({ children }) {
```
**Used by**:
- Number Guesser ✅
- Matching ✅
- Memory Quiz ✅
@@ -66,7 +64,6 @@ export function YourGameProvider({ children }) {
**Game.tsx** - Created a simple quiz UI from scratch instead of using existing components:
**What I created (WRONG)**:
```typescript
// Simple number pad quiz
{currentQuestion && (
@@ -80,7 +77,6 @@ export function YourGameProvider({ children }) {
```
**What I should have used (CORRECT)**:
```typescript
// Existing sophisticated UI from src/app/arcade/complement-race/components/
- ComplementRaceGame.tsx // Main game container
@@ -110,7 +106,6 @@ The Complement Race Migration Plan Phase 4 mentioned `useSocketSync` and preserv
## What I Actually Did (Wrong)
**CORRECT**:
- Created `Validator.ts` (~700 lines of server-side game logic)
- Created `types.ts` with proper TypeScript types
- Registered in `validators.ts` and `game-registry.ts`
@@ -119,7 +114,6 @@ The Complement Race Migration Plan Phase 4 mentioned `useSocketSync` and preserv
- Disabled debug logging
**COMPLETELY WRONG**:
- Created `Provider.tsx` using Pattern A (useArcadeSession)
- Threw away existing reducer with 30+ action types
- Created `Game.tsx` with simple quiz UI
@@ -136,7 +130,6 @@ The Complement Race Migration Plan Phase 4 mentioned `useSocketSync` and preserv
## What Needs to Happen
### KEEP (Correct Implementation) ✅
1. `src/arcade-games/complement-race/Provider.tsx` ✅ (Actually correct!)
2. `src/arcade-games/complement-race/Validator.ts`
3. `src/arcade-games/complement-race/types.ts`
@@ -145,11 +138,9 @@ The Complement Race Migration Plan Phase 4 mentioned `useSocketSync` and preserv
6. Test file fixes ✅
### DELETE (Wrong Implementation) ❌
1. `src/arcade-games/complement-race/Game.tsx` ❌ (Simple quiz UI)
### UPDATE (Use Existing Components) ✏️
1. `src/arcade-games/complement-race/index.tsx`:
- Change `GameComponent` from new `Game.tsx` to existing `ComplementRaceGame`
- Import from `@/app/arcade/complement-race/components/ComplementRaceGame`
@@ -186,7 +177,6 @@ export const complementRaceGame = defineGame<...>({
**Challenge**: Existing UI components use `dispatch({ type: 'ACTION' })` but Provider exposes `startGame()`, `submitAnswer()`, etc.
**Solutions**:
1. Update components to use action creators (preferred)
2. Add compatibility layer in Provider that exposes `dispatch`
3. Create wrapper components

View File

@@ -23,7 +23,6 @@ Speed Complement Race is currently a sophisticated single-player game with three
## Current State Analysis
### What We Have
- ✅ Complex single-player game with 3 modes
- ✅ Advanced adaptive difficulty system
- ✅ AI opponent system with personalities
@@ -33,7 +32,6 @@ Speed Complement Race is currently a sophisticated single-player game with three
- ✅ Sound effects and visual feedback
### What's Missing
- ❌ Multiplayer support (max players: 1)
- ❌ Socket integration
- ❌ Validator registration in modular system
@@ -61,7 +59,6 @@ Game Components (existing UI)
```
### Key Principles
1. **Preserve existing gameplay** - Keep all three modes working
2. **Maintain UI/UX quality** - All animations, sounds, visuals stay intact
3. **Support both single and multiplayer** - AI opponents + human players
@@ -75,27 +72,19 @@ Game Components (existing UI)
## Phase 1: Configuration & Type System ✓
### 1.1 Define ComplementRaceGameConfig
**File**: `src/lib/game-configs.ts`
```typescript
export interface ComplementRaceGameConfig {
// Game Style (which mode)
style: "practice" | "sprint" | "survival";
style: 'practice' | 'sprint' | 'survival';
// Question Settings
mode: "friends5" | "friends10" | "mixed";
complementDisplay: "number" | "abacus" | "random";
mode: 'friends5' | 'friends10' | 'mixed';
complementDisplay: 'number' | 'abacus' | 'random';
// Difficulty
timeoutSetting:
| "preschool"
| "kindergarten"
| "relaxed"
| "slow"
| "normal"
| "fast"
| "expert";
timeoutSetting: 'preschool' | 'kindergarten' | 'relaxed' | 'slow' | 'normal' | 'fast' | 'expert';
// AI Settings
enableAI: boolean;
@@ -114,10 +103,10 @@ export interface ComplementRaceGameConfig {
}
export const DEFAULT_COMPLEMENT_RACE_CONFIG: ComplementRaceGameConfig = {
style: "practice",
mode: "mixed",
complementDisplay: "random",
timeoutSetting: "normal",
style: 'practice',
mode: 'mixed',
complementDisplay: 'random',
timeoutSetting: 'normal',
enableAI: true,
aiOpponentCount: 2,
maxPlayers: 1,
@@ -129,11 +118,9 @@ export const DEFAULT_COMPLEMENT_RACE_CONFIG: ComplementRaceGameConfig = {
```
### 1.2 Disable Debug Logging
**File**: `src/app/arcade/complement-race/hooks/useSteamJourney.ts`
Change:
```typescript
const DEBUG_PASSENGER_BOARDING = false; // was true
```
@@ -143,11 +130,9 @@ const DEBUG_PASSENGER_BOARDING = false; // was true
## Phase 2: Validator Implementation ✓
### 2.1 Create ComplementRaceValidator
**File**: `src/lib/validators/ComplementRaceValidator.ts`
**Responsibilities**:
- Validate player answers
- Generate questions
- Manage game state
@@ -155,33 +140,20 @@ const DEBUG_PASSENGER_BOARDING = false; // was true
- Synchronize multiplayer state
**Key Methods**:
```typescript
class ComplementRaceValidator {
getInitialState(config: ComplementRaceGameConfig): GameState;
getNewQuestion(state: GameState): ComplementQuestion;
validateAnswer(
state: GameState,
playerId: string,
answer: number,
): ValidationResult;
updatePlayerProgress(
state: GameState,
playerId: string,
correct: boolean,
): GameState;
checkWinCondition(state: GameState): {
winner: string | null;
gameOver: boolean;
};
updateAIPositions(state: GameState, deltaTime: number): GameState;
serializeState(state: GameState): SerializedState;
deserializeState(serialized: SerializedState): GameState;
getInitialState(config: ComplementRaceGameConfig): GameState
getNewQuestion(state: GameState): ComplementQuestion
validateAnswer(state: GameState, playerId: string, answer: number): ValidationResult
updatePlayerProgress(state: GameState, playerId: string, correct: boolean): GameState
checkWinCondition(state: GameState): { winner: string | null, gameOver: boolean }
updateAIPositions(state: GameState, deltaTime: number): GameState
serializeState(state: GameState): SerializedState
deserializeState(serialized: SerializedState): GameState
}
```
**State Structure**:
```typescript
interface MultiplayerGameState {
// Configuration
@@ -209,7 +181,7 @@ interface MultiplayerGameState {
progress: Map<playerId, number>; // 0-100% or lap count
// Game Status
phase: "waiting" | "countdown" | "playing" | "finished";
phase: 'waiting' | 'countdown' | 'playing' | 'finished';
winner: string | null;
startTime: number | null;
@@ -235,11 +207,9 @@ interface PlayerState {
## Phase 3: Socket Server Integration ✓
### 3.1 Register Game Handler
**File**: `src/services/socket-server.ts`
Add to game session management:
```typescript
case 'complement-race':
validator = new ComplementRaceValidator();
@@ -249,13 +219,11 @@ case 'complement-race':
### 3.2 Socket Events
**Client → Server**:
- `game:answer` - Submit answer for current question
- `game:ready` - Player ready to start
- `game:settings-change` - Update game config (host only)
**Server → Client**:
- `game:state-update` - Full state sync
- `game:question-new` - New question generated
- `game:answer-result` - Answer validation result
@@ -266,13 +234,11 @@ case 'complement-race':
### 3.3 Real-time Synchronization Strategy
**State Updates**:
- Full state broadcast every 200ms (AI updates)
- Instant broadcasts on player actions (answers, ready status)
- Delta compression for large states (sprint mode passengers)
**Race Condition Handling**:
- Server is source of truth
- Client predictions for smooth animations
- Rollback on server correction
@@ -282,11 +248,9 @@ case 'complement-race':
## Phase 4: Room Provider & Configuration ✓
### 4.1 Create RoomComplementRaceProvider
**File**: `src/app/arcade/complement-race/context/RoomComplementRaceProvider.tsx`
Similar to existing `ComplementRaceProvider` but:
- Accepts `roomCode` prop
- Loads saved config from arcade room state
- Merges saved config with defaults
@@ -327,11 +291,9 @@ export function RoomComplementRaceProvider({
```
### 4.2 Update Arcade Room Store
**File**: `src/app/arcade/stores/arcade-room-store.ts`
Ensure complement-race config is saved:
```typescript
updateGameConfig: (gameName: string, config: Partial<GameConfig>) => {
set((state) => {
@@ -352,14 +314,12 @@ updateGameConfig: (gameName: string, config: Partial<GameConfig>) => {
**Core Concept**: ONE railroad with ONE set of passengers. Players compete to pick them up and deliver them first.
#### Shared Game Board
- All players see the SAME track with SAME stations
- 6-8 passengers spawn per route at various stations
- Once a player picks up a passenger, it disappears for EVERYONE
- Real competition for limited resources
#### Visual Design: Ghost Trains
```
Your train: 🚂🟦 Full opacity (100%), prominent
Other players: 🚂🟢🟡🟣 Low opacity (30-40%), "ghost" effect
@@ -374,14 +334,12 @@ Benefits:
#### Gameplay Mechanics
**Movement**:
- Answer complement questions to build momentum
- Correct answer → +15 momentum → train speed increases
- Each player has independent momentum/speed
- Trains can pass through each other (no collision)
**Pickup Rules**:
```typescript
When train reaches station (within 5% position):
IF passenger waiting at station:
@@ -393,7 +351,6 @@ When train reaches station (within 5% position):
```
**Delivery Rules**:
```typescript
When train with passenger reaches destination station:
Auto-deliver
@@ -403,13 +360,11 @@ When train with passenger reaches destination station:
```
**Capacity**:
- Each train: 3 passenger cars = max 3 concurrent passengers
- Must deliver before picking up more
- Strategic choice: quick nearby delivery vs. valuable long-distance
**Resource Competition**:
- 6-8 passengers per route
- 4 players competing
- Not enough for everyone to get all passengers
@@ -418,25 +373,21 @@ When train with passenger reaches destination station:
#### Win Conditions (Host Configurable)
**Route-based** (default):
- Play 3 routes (3 minutes)
- Most passengers delivered wins
- Tiebreaker: total points
**Score-based**:
- First to 100 points
- Urgent passengers (20pts) are strategic targets
**Time-based**:
- 5-minute session
- Most deliveries at time limit
### 5.2 Practice/Survival Mode Multiplayer
**Practice Mode**: Linear race track with multiple lanes
- 2-4 horizontal lanes stacked vertically
- Each player in their own lane
- AI opponents fill remaining lanes (optional)
@@ -445,7 +396,6 @@ When train with passenger reaches destination station:
- First to 20 questions wins
**Survival Mode**: Circular track with lap counting
- Players race on shared circular track
- Lap counter instead of finish line
- Infinite laps, timed rounds
@@ -454,7 +404,6 @@ When train with passenger reaches destination station:
### 5.3 Practice Mode: Simultaneous Questions
**Question Flow**:
```
1. Same question appears for all players: "7 + ? = 10"
2. Players race to answer (optional: show "🤔" indicator)
@@ -471,7 +420,6 @@ When train with passenger reaches destination station:
```
**Strategic Tension**:
- Rush to be first (more reward) vs. take time to be accurate
- See opponents' progress in real-time
- Dramatic overtaking moments
@@ -479,10 +427,7 @@ When train with passenger reaches destination station:
### 5.4 AI Opponent Scaling
```typescript
function getAICount(
config: ComplementRaceGameConfig,
humanPlayers: number,
): number {
function getAICount(config: ComplementRaceGameConfig, humanPlayers: number): number {
if (!config.enableAI) return 0;
const totalRacers = humanPlayers + config.aiOpponentCount;
@@ -493,7 +438,6 @@ function getAICount(
```
**AI Behavior in Multiplayer**:
- Optional (host configurable)
- Fill empty lanes in practice/survival modes
- Act as ghost trains in sprint mode
@@ -503,7 +447,6 @@ function getAICount(
### 5.5 Live Updates & Broadcasts
**Event Feed** (shown to all players):
```
• 🟦 Player 1 delivered 👨‍💼 Bob! +10 pts
• 🟢 Player 2 picked up 👩‍🎓 Alice at Hillside
@@ -512,7 +455,6 @@ function getAICount(
```
**Tension Moments** (sprint mode):
```
When 2+ players approach same station:
"🚨 Race for passenger at Riverside!"
@@ -524,7 +466,6 @@ Result:
```
**Scoreboard** (always visible):
```
🏆 LEADERBOARD:
1. 🟣 Player 4: 4 delivered (50 pts)
@@ -540,14 +481,12 @@ Result:
### 6.1 Track Visualization Updates
**Practice/Survival Mode**:
- Stack up to 4 player tracks vertically
- Show player names/avatars
- Color-code each player's lane
- Show AI opponents in separate lanes
**Sprint Mode**:
- Show multiple trains on same track OR
- Picture-in-picture mini views OR
- Leaderboard overlay with positions
@@ -555,7 +494,6 @@ Result:
### 6.2 Settings UI
**Add to GameControls.tsx**:
- Max Players selector (1-4)
- Enable AI toggle
- AI Opponent Count (0-2)
@@ -564,7 +502,6 @@ Result:
### 6.3 Lobby/Waiting Room
**Add GameLobby.tsx phase**:
- Show connected players
- Ready check system
- Host can change settings
@@ -573,7 +510,6 @@ Result:
### 6.4 Results Screen Updates
**Show multiplayer results**:
- Leaderboard with all player scores
- Individual stats per player
- Replay button (returns to lobby)
@@ -584,21 +520,19 @@ Result:
## Phase 7: Registry & Routing ✓
### 7.1 Update Game Registry
**File**: `src/lib/validators/index.ts`
```typescript
import { ComplementRaceValidator } from "./ComplementRaceValidator";
import { ComplementRaceValidator } from './ComplementRaceValidator';
export const GAME_VALIDATORS = {
matching: MatchingGameValidator,
"number-guesser": NumberGuesserValidator,
"complement-race": ComplementRaceValidator, // ADD THIS
'matching': MatchingGameValidator,
'number-guesser': NumberGuesserValidator,
'complement-race': ComplementRaceValidator, // ADD THIS
} as const;
```
### 7.2 Update Game Config
**File**: `src/lib/game-configs.ts`
```typescript
@@ -609,34 +543,26 @@ export type GameConfig =
```
### 7.3 Update GameSelector
**File**: `src/components/GameSelector.tsx`
```typescript
GAMES_CONFIG = {
"complement-race": {
name: "Speed Complement Race",
fullName: "Speed Complement Race 🏁",
'complement-race': {
name: 'Speed Complement Race',
fullName: 'Speed Complement Race 🏁',
maxPlayers: 4, // CHANGE FROM 1
url: "/arcade/complement-race",
chips: [
"🤖 AI Opponents",
"🔥 Speed Challenge",
"🏆 Three Game Modes",
"👥 Multiplayer",
],
difficulty: "Intermediate",
url: '/arcade/complement-race',
chips: ['🤖 AI Opponents', '🔥 Speed Challenge', '🏆 Three Game Modes', '👥 Multiplayer'],
difficulty: 'Intermediate',
available: true,
},
};
}
}
```
### 7.4 Update Routing
**File**: `src/app/arcade/complement-race/page.tsx`
Add room-based routing:
```typescript
// Support both standalone and room-based play
export default function ComplementRacePage({
@@ -667,7 +593,6 @@ export default function ComplementRacePage({
## Phase 8: Testing & Validation ⚠️ PENDING
### 8.1 Unit Tests
- [ ] ComplementRaceValidator logic
- [ ] Question generation
- [ ] Answer validation
@@ -675,14 +600,12 @@ export default function ComplementRacePage({
- [ ] AI position updates
### 8.2 Integration Tests
- [ ] Socket event flow
- [ ] State synchronization
- [ ] Room configuration persistence
- [ ] Multi-player race logic
### 8.3 E2E Tests
- [ ] Single-player mode (backward compatibility)
- [ ] Multiplayer with 2 players
- [ ] Multiplayer with 4 players
@@ -691,7 +614,6 @@ export default function ComplementRacePage({
- [ ] Settings persistence across sessions
### 8.4 Manual Testing Checklist
- [ ] Create room with complement-race
- [ ] Join with multiple clients
- [ ] Change settings (host only)
@@ -808,14 +730,12 @@ export function GhostTrain({ position, color, opacity, name, passengerCount }: G
```
**Visual Design**:
- Local player: Full opacity (100%), vibrant colors, clear
- Other players: 30-40% opacity, subtle blur, labeled with name
- Show passenger count on ghost trains
- No collision detection needed (trains pass through each other)
**Checklist**:
- [ ] Create GhostTrain component
- [ ] Update SteamTrainJourney to render all players
- [ ] Test with 2 players (local + 1 ghost)
@@ -929,7 +849,6 @@ export function Lane({ yOffset, isLocalPlayer, children }: LaneProps) {
```
**Features**:
- Each lane is color-coded per player
- Local player's lane has brighter background
- Progress bars show position clearly
@@ -937,7 +856,6 @@ export function Lane({ yOffset, isLocalPlayer, children }: LaneProps) {
- Smooth position interpolation for animations
**Checklist**:
- [ ] Create Lane component
- [ ] Create Racer component (or update existing)
- [ ] Update LinearTrack to render multiple lanes
@@ -1053,7 +971,6 @@ export function LeaderboardRow({ rank, player, isLocalPlayer }: LeaderboardRowPr
```
**Checklist**:
- [ ] Update GameResults.tsx to show leaderboard
- [ ] Create LeaderboardRow component
- [ ] Add winner announcement
@@ -1199,7 +1116,6 @@ return (
```
**Checklist**:
- [ ] Create GameLobby.tsx component
- [ ] Create PlayerCard component
- [ ] Add setReady to Provider context
@@ -1216,7 +1132,6 @@ return (
**Current State**: AI opponents defined in types but not populated
**Files to Update**:
1. `src/arcade-games/complement-race/Validator.ts` - AI logic
2. Track components (LinearTrack, SteamTrainJourney) - AI rendering
@@ -1341,7 +1256,6 @@ private shouldAIAnswerCorrectly(personality: string): boolean {
**Already handled by 9.1 and 9.2** - Since AI opponents are in `state.players`, they'll render automatically as ghost trains/lanes!
**Checklist**:
- [ ] Implement AI population in validateStartGame
- [ ] Implement updateAIPositions logic
- [ ] Add AI answer timing system
@@ -1395,7 +1309,6 @@ export function EventFeed() {
```
**Checklist**:
- [ ] Create EventFeed component
- [ ] Update Validator to emit events
- [ ] Add event types (claim, deliver, overtake)
@@ -1410,13 +1323,11 @@ export function EventFeed() {
**Total Estimated Time**: 15-20 hours
**Priority Breakdown**:
- 🚨 **HIGH** (8-9 hours): Ghost trains, multi-lane track, results screen
- ⚠️ **MEDIUM** (8-12 hours): Lobby system, AI opponents
-**LOW** (3-4 hours): Event feed
**Completion Criteria**:
- [ ] Can see all players' trains/positions in real-time
- [ ] Multiplayer leaderboard shows all players
- [ ] Lobby shows player list with ready indicators
@@ -1425,7 +1336,6 @@ export function EventFeed() {
- [ ] Zero visual glitches with 4 players
**Once Phase 9 is complete**:
- Multiplayer will be FULLY functional
- Overall implementation: 100% complete
- Ready for Phase 8 (Testing & Validation)
@@ -1435,28 +1345,24 @@ export function EventFeed() {
## Implementation Order
### ✅ Priority 1: Foundation (COMPLETE)
1. ✓ Define ComplementRaceGameConfig
2. ✓ Disable debug logging
3. ✓ Create ComplementRaceValidator skeleton
4. ✓ Register in modular system
### ✅ Priority 2: Core Multiplayer (COMPLETE)
5. ✓ Implement validator methods
6. ✓ Socket server integration
7. ✓ Create RoomComplementRaceProvider (State Adapter Pattern)
8. ✓ Update arcade room store
### ✅ Priority 3: Basic UI Integration (COMPLETE)
9. ✓ Add navigation bar (PageWithNav)
10. ✓ Update settings UI
11. ✓ Config persistence
12. ✓ Registry integration
### 🚨 Priority 4: Multiplayer Visuals (CRITICAL - NEXT)
13. [ ] Ghost trains (Sprint Mode)
14. [ ] Multi-lane track (Practice Mode)
15. [ ] Multiplayer results screen
@@ -1464,7 +1370,6 @@ export function EventFeed() {
17. [ ] AI opponent display
### Priority 5: Testing & Polish (FINAL)
18. [ ] Write tests (unit, integration, E2E)
19. [ ] Manual testing with 2-4 players
20. [ ] Bug fixes
@@ -1476,19 +1381,15 @@ export function EventFeed() {
## Risk Mitigation
### Risk 1: Breaking Existing Single-Player
**Mitigation**: Keep existing Provider, add new RoomProvider, support both paths
### Risk 2: Complex Sprint Mode State Sync
**Mitigation**: Start with Practice mode, add Sprint later, use delta compression
### Risk 3: Performance with 4 Players
**Mitigation**: Optimize rendering, use React.memo, throttle updates, profile early
### Risk 4: AI + Multiplayer Complexity
**Mitigation**: Make AI optional, test with AI disabled first, add AI last
---
@@ -1496,7 +1397,6 @@ export function EventFeed() {
## Reference Games
Use these as architectural reference:
- **Matching Game** (`src/lib/validators/MatchingGameValidator.ts`) - Room config, socket integration
- **Number Guesser** (`src/lib/validators/NumberGuesserValidator.ts`) - Turn-based logic
- **Game Settings Docs** (`.claude/GAME_SETTINGS_PERSISTENCE.md`) - Config patterns
@@ -1506,7 +1406,6 @@ Use these as architectural reference:
## Success Criteria
### ✅ Backend & Infrastructure (COMPLETE)
- [x] Complement Race appears in arcade room game selector
- [x] Can create room with complement-race
- [x] Settings persist across page refreshes
@@ -1516,7 +1415,6 @@ Use these as architectural reference:
- [x] Pre-commit checks pass
### ⚠️ Multiplayer Visuals (IN PROGRESS - Phase 9)
- [ ] **Sprint Mode**: Can see other players' trains (ghost effect)
- [ ] **Practice Mode**: Multi-lane track shows all players
- [ ] **Survival Mode**: Circular track with multiple players
@@ -1526,7 +1424,6 @@ Use these as architectural reference:
- [ ] AI opponents visible in all game modes
### Testing & Polish (PENDING)
- [ ] 2-player multiplayer test (all 3 modes)
- [ ] 4-player multiplayer test (all 3 modes)
- [ ] AI + human players test
@@ -1537,7 +1434,6 @@ Use these as architectural reference:
- [ ] Event feed for competitive tension (optional)
### Current Status: 70% Complete
**What Works**: Backend, state management, config persistence, navigation
**What's Missing**: Multiplayer visualization (ghost trains, multi-lane tracks, lobby UI)
@@ -1548,18 +1444,15 @@ Use these as architectural reference:
**Immediate Priority**: Phase 9 - Multiplayer Visual Features
### Quick Wins (Do These First)
1. **Ghost Trains** (2-3 hours) - Make Sprint mode multiplayer visible
2. **Multi-Lane Track** (3-4 hours) - Make Practice mode multiplayer visible
3. **Results Screen** (1-2 hours) - Show full leaderboard
### After Quick Wins
4. **Visual Lobby** (2-3 hours) - Add ready check system
5. **AI Opponents** (4-6 hours) - Populate and display AI players
### Then Testing
6. Manual testing with 2+ players
7. Bug fixes and polish
8. Unit/integration tests

View File

@@ -22,13 +22,11 @@
### Phase 1: Configuration & Type System ✅ COMPLETE
**Plan Requirements**:
- Define ComplementRaceGameConfig
- Disable debug logging
- Set up type system
**Actual Implementation**:
```typescript
// ✅ CORRECT: Full config interface in types.ts
export interface ComplementRaceConfig {
@@ -62,7 +60,6 @@ export interface ComplementRaceConfig {
### Phase 2: Validator Implementation ✅ COMPLETE
**Plan Requirements**:
- Create ComplementRaceValidator class
- Implement all move validation methods
- Handle scoring, questions, and game state
@@ -70,7 +67,6 @@ export interface ComplementRaceConfig {
**Actual Implementation**:
**✅ All Required Methods Implemented**:
- `validateStartGame` - Initialize multiplayer game
- `validateSubmitAnswer` - Validate answers, update scores
- `validateClaimPassenger` - Sprint mode passenger pickup
@@ -83,7 +79,6 @@ export interface ComplementRaceConfig {
- `validatePlayAgain` - Restart
**✅ Helper Methods**:
- `generateQuestion` - Random question generation
- `calculateAnswerScore` - Scoring with speed/streak bonuses
- `generatePassengers` - Sprint mode passenger spawning
@@ -91,7 +86,6 @@ export interface ComplementRaceConfig {
- `calculateLeaderboard` - Sort players by score
**✅ State Structure** matches plan:
```typescript
interface ComplementRaceState {
config: ComplementRaceConfig
@@ -113,7 +107,6 @@ interface ComplementRaceState {
### Phase 3: Socket Server Integration ✅ COMPLETE
**Plan Requirements**:
- Register in validators.ts
- Socket event handling
- Real-time synchronization
@@ -121,27 +114,25 @@ interface ComplementRaceState {
**Actual Implementation**:
**Registered in validators.ts**:
```typescript
import { complementRaceValidator } from "@/arcade-games/complement-race/Validator";
import { complementRaceValidator } from '@/arcade-games/complement-race/Validator'
export const VALIDATORS = {
matching: matchingGameValidator,
"number-guesser": numberGuesserValidator,
"complement-race": complementRaceValidator, // ✅ CORRECT
};
'number-guesser': numberGuesserValidator,
'complement-race': complementRaceValidator, // ✅ CORRECT
}
```
**Registered in game-registry.ts**:
```typescript
import { complementRaceGame } from "@/arcade-games/complement-race";
import { complementRaceGame } from '@/arcade-games/complement-race'
const GAME_REGISTRY = {
matching: matchingGame,
"number-guesser": numberGuesserGame,
"complement-race": complementRaceGame, // ✅ CORRECT
};
'number-guesser': numberGuesserGame,
'complement-race': complementRaceGame, // ✅ CORRECT
}
```
**Uses standard useArcadeSession pattern** - Socket integration automatic via SDK
@@ -157,20 +148,16 @@ const GAME_REGISTRY = {
**Actual Implementation**: **State Adapter Pattern** (Better Solution!)
Instead of creating a separate RoomProvider, we:
1. ✅ Used standard **useArcadeSession** pattern in Provider.tsx
2. ✅ Created **state transformation layer** to bridge multiplayer ↔ single-player UI
3. ✅ Preserved ALL existing UI components without changes
4. ✅ Config merging from roomData works correctly
**Key Innovation**:
```typescript
// Transform multiplayer state to look like single-player state
const compatibleState = useMemo((): CompatibleGameState => {
const localPlayer = localPlayerId
? multiplayerState.players[localPlayerId]
: null;
const localPlayer = localPlayerId ? multiplayerState.players[localPlayerId] : null
return {
// Extract local player's data
@@ -178,12 +165,11 @@ const compatibleState = useMemo((): CompatibleGameState => {
score: localPlayer?.score || 0,
streak: localPlayer?.streak || 0,
// ... etc
};
}, [multiplayerState, localPlayerId]);
}
}, [multiplayerState, localPlayerId])
```
This is **better than the plan** because:
- No code duplication
- Reuses existing components
- Clean separation of concerns
@@ -198,7 +184,6 @@ This is **better than the plan** because:
**Plan Requirements** vs **Implementation**:
#### 5.1 Sprint Mode: Passenger Rush ✅ IMPLEMENTED
- ✅ Shared passenger pool (all players see same passengers)
- ✅ First-come-first-served claiming (`claimedBy` field)
- ✅ Delivery points (10 regular, 20 urgent)
@@ -209,7 +194,6 @@ This is **better than the plan** because:
**Status**: **Server logic complete, visual features missing**
#### 5.2 Practice Mode: Simultaneous Questions ⚠️ NEEDS WORK
- ✅ Question generation per player works
- ✅ Answer validation works
- ✅ Position tracking works
@@ -220,7 +204,6 @@ This is **better than the plan** because:
**Status**: **Backend works, frontend needs multiplayer UI**
#### 5.3 Survival Mode ⚠️ NEEDS WORK
- ✅ Position/lap tracking logic exists
-**MISSING**: Circular track with multiple players
-**MISSING**: Lap counter display
@@ -229,7 +212,6 @@ This is **better than the plan** because:
**Status**: **Basic structure, needs multiplayer visuals**
#### 5.4 AI Opponent Scaling ❌ NOT IMPLEMENTED
- ❌ AI opponents defined in types but not populated
- ❌ No AI update logic in validator
-`aiOpponents` array stays empty
@@ -237,7 +219,6 @@ This is **better than the plan** because:
**Status**: **Needs implementation**
#### 5.5 Live Updates & Broadcasts ❌ NOT IMPLEMENTED
- ❌ No event feed component
- ❌ No "race for passenger" alerts
- ❌ No live leaderboard overlay
@@ -254,7 +235,6 @@ This is **better than the plan** because:
**Plan Requirements** vs **Implementation**:
#### 6.1 Track Visualization ❌ NOT UPDATED
- ❌ Practice: No multi-lane track (still shows single player)
- ❌ Sprint: No ghost trains (only local train visible)
- ❌ Survival: No multi-player circular track
@@ -262,13 +242,11 @@ This is **better than the plan** because:
**Current State**: UI still shows **single-player view only**
#### 6.2 Settings UI ✅ COMPLETE
- ✅ GameControls.tsx has all settings
- ✅ Max players, AI settings, game mode all configurable
- ✅ Settings persist via arcade room store
#### 6.3 Lobby/Waiting Room ⚠️ PARTIAL
- ⚠️ Uses "controls" phase as lobby (functional but not ideal)
- ❌ No visual "ready check" system
- ❌ No player list with ready indicators
@@ -277,7 +255,6 @@ This is **better than the plan** because:
**Should Add**: Proper lobby phase with visual ready checks
#### 6.4 Results Screen ⚠️ PARTIAL
- ✅ GameResults.tsx exists
- ❌ No multiplayer leaderboard (still shows single-player stats)
- ❌ No per-player breakdown
@@ -290,13 +267,11 @@ This is **better than the plan** because:
### Phase 7: Registry & Routing ✅ COMPLETE
**Plan Requirements**:
- Update game registry
- Update validators
- Update routing
**Actual Implementation**:
- ✅ Registered in validators.ts
- ✅ Registered in game-registry.ts
- ✅ Registered in game-configs.ts
@@ -311,7 +286,6 @@ This is **better than the plan** because:
### Phase 8: Testing & Validation ❌ NOT DONE
All testing checkboxes remain unchecked:
- [ ] Unit tests
- [ ] Integration tests
- [ ] E2E tests
@@ -433,7 +407,6 @@ From migration plan's "Success Criteria":
### Immediate Next Steps (To Complete Multiplayer)
1. **Implement Ghost Trains** (2-3 hours)
```typescript
// In SteamTrainJourney.tsx
{Object.entries(state.players).map(([playerId, player]) => {
@@ -451,7 +424,6 @@ From migration plan's "Success Criteria":
```
2. **Add Multi-Lane Track** (3-4 hours)
```typescript
// In LinearTrack.tsx
const lanes = Object.values(state.players)
@@ -496,14 +468,12 @@ From migration plan's "Success Criteria":
### Overall Grade: **B (70%)**
**Strengths**:
-**Excellent architecture** - State adapter is ingenious
-**Complete backend logic** - Validator fully functional
-**Proper integration** - Follows all patterns correctly
-**Type safety** - Zero TypeScript errors
**Weaknesses**:
-**Missing multiplayer visuals** - Can't see other players
-**No AI opponents** - Can't test solo
-**Minimal lobby** - Auto-starts instead of ready check
@@ -519,14 +489,12 @@ From migration plan's "Success Criteria":
### What Would Make This Complete?
**Minimum Viable Multiplayer** (8-10 hours of work):
1. Ghost trains in sprint mode
2. Multi-lane tracks in practice mode
3. Multiplayer leaderboard in results
4. Lobby with ready checks
**Full Polish** (20-25 hours total):
- Above + AI opponents
- Above + event feed
- Above + comprehensive testing

View File

@@ -11,14 +11,12 @@
### ✅ Phase 1: Foundation & Architecture (COMPLETE)
**1. Comprehensive Migration Plan**
- File: `.claude/COMPLEMENT_RACE_MIGRATION_PLAN.md`
- Detailed multiplayer game design with ghost train visualization
- Shared universe passenger competition mechanics
- Complete 8-phase implementation roadmap
**2. Type System** (`src/arcade-games/complement-race/types.ts`)
- `ComplementRaceConfig` - Full game configuration with all settings
- `ComplementRaceState` - Multiplayer game state management
- `ComplementRaceMove` - Player action types
@@ -26,7 +24,6 @@
- All types fully documented and exported
**3. Validator** (`src/arcade-games/complement-race/Validator.ts`) - **~700 lines**
- ✅ Question generation (friends of 5, 10, mixed)
- ✅ Answer validation with scoring
- ✅ Player progress tracking
@@ -38,7 +35,6 @@
- Fully implements `GameValidator<ComplementRaceState, ComplementRaceMove>`
**4. Game Definition** (`src/arcade-games/complement-race/index.tsx`)
- Manifest with game metadata
- Default configuration
- Config validation function
@@ -47,7 +43,6 @@
- Properly typed with generics
**5. Registry Integration**
- ✅ Registered in `src/lib/arcade/validators.ts`
- ✅ Registered in `src/lib/arcade/game-registry.ts`
- ✅ Added types to `src/lib/arcade/validation/types.ts`
@@ -55,7 +50,6 @@
- ✅ Added types to `src/lib/arcade/game-configs.ts`
**6. Configuration System**
-`ComplementRaceGameConfig` defined with all settings:
- Game style (practice, sprint, survival)
- Question settings (mode, display type)
@@ -68,7 +62,6 @@
- ✅ Room-based config persistence supported
**7. Code Quality**
- ✅ Debug logging disabled (`DEBUG_PASSENGER_BOARDING = false`)
- ✅ New modular code compiles (only 1 minor type warning)
- ✅ Backward compatible Station type (icon + emoji fields)
@@ -81,20 +74,17 @@
### Core Mechanics
**Shared Universe**:
- ONE track with ONE set of passengers
- Real competition for limited resources
- First to station claims passenger
- Ghost train visualization (opponents at 30-40% opacity)
**Player Capacity**:
- 1-4 players per game
- 3 passenger cars per train
- Strategic delivery choices
**Win Conditions** (Host Configurable):
1. **Route-based**: Complete N routes, highest score wins
2. **Score-based**: First to target score
3. **Time-based**: Most deliveries in time limit
@@ -102,20 +92,17 @@
### Game Modes
**Practice Mode**: Linear race
- First to 20 questions wins
- Optional AI opponents
- Simultaneous question answering
**Sprint Mode**: Train journey with passengers
- 60-second routes
- Passenger pickup/delivery competition
- Momentum system
- Time-of-day cycles
**Survival Mode**: Infinite laps
- Circular track
- Lap counting
- Endurance challenge
@@ -165,34 +152,30 @@ src/lib/arcade/
```typescript
// Create: src/arcade-games/complement-race/__tests__/Validator.test.ts
import { complementRaceValidator } from "../Validator";
import { DEFAULT_COMPLEMENT_RACE_CONFIG } from "@/lib/arcade/game-configs";
import { complementRaceValidator } from '../Validator'
import { DEFAULT_COMPLEMENT_RACE_CONFIG } from '@/lib/arcade/game-configs'
test("generates initial state", () => {
const state = complementRaceValidator.getInitialState(
DEFAULT_COMPLEMENT_RACE_CONFIG,
);
expect(state.gamePhase).toBe("setup");
expect(state.stations).toHaveLength(6);
});
test('generates initial state', () => {
const state = complementRaceValidator.getInitialState(DEFAULT_COMPLEMENT_RACE_CONFIG)
expect(state.gamePhase).toBe('setup')
expect(state.stations).toHaveLength(6)
})
test("validates starting game", () => {
const state = complementRaceValidator.getInitialState(
DEFAULT_COMPLEMENT_RACE_CONFIG,
);
test('validates starting game', () => {
const state = complementRaceValidator.getInitialState(DEFAULT_COMPLEMENT_RACE_CONFIG)
const result = complementRaceValidator.validateMove(state, {
type: "START_GAME",
playerId: "p1",
userId: "u1",
type: 'START_GAME',
playerId: 'p1',
userId: 'u1',
timestamp: Date.now(),
data: {
activePlayers: ["p1", "p2"],
playerMetadata: { p1: { name: "Alice" }, p2: { name: "Bob" } },
},
});
expect(result.valid).toBe(true);
expect(result.newState?.activePlayers).toHaveLength(2);
});
activePlayers: ['p1', 'p2'],
playerMetadata: { p1: { name: 'Alice' }, p2: { name: 'Bob' } }
}
})
expect(result.valid).toBe(true)
expect(result.newState?.activePlayers).toHaveLength(2)
})
```
### 2. Game Appears in Selector
@@ -226,12 +209,10 @@ npm run type-check
## ✅ What's Been Implemented (Update)
### Provider Component
**Status**: ✅ Complete
**Location**: `src/arcade-games/complement-race/Provider.tsx`
**Implemented**:
- ✅ Socket connection via useArcadeSession
- ✅ Real-time state synchronization
- ✅ Config loading from room (with persistence)
@@ -240,12 +221,10 @@ npm run type-check
- ✅ Optimistic update handling
### Game UI Component
**Status**: ✅ MVP Complete
**Location**: `src/arcade-games/complement-race/Game.tsx`
**Implemented**:
- ✅ Setup phase with game settings display
- ✅ Lobby/countdown phase UI
- ✅ Playing phase with:
@@ -260,7 +239,6 @@ npm run type-check
### What's Still Pending
**Multiplayer-Specific Features** (can be added later):
- Ghost train visualization (opacity-based rendering)
- Shared passenger board (sprint mode)
- Advanced race track visualization
@@ -274,14 +252,12 @@ npm run type-check
### Immediate (Can Test Multiplayer)
**1. Create RoomComplementRaceProvider** (~2-3 hours)
- Connect to socket
- Load room config
- Sync state with server
- Handle moves
**2. Create Basic Multiplayer UI** (~3-4 hours)
- Show all player positions
- Render ghost trains
- Display shared passenger board
@@ -290,19 +266,16 @@ npm run type-check
### Polish (Make it Great)
**3. Sprint Mode Multiplayer** (~4-6 hours)
- Multiple trains on same track
- Passenger competition visualization
- Route celebration for all players
**4. Practice/Survival Modes** (~2-3 hours)
- Multi-lane racing
- Lap tracking (survival)
- Finish line detection
**5. Testing & Bug Fixes** (~2-3 hours)
- End-to-end multiplayer testing
- Handle edge cases
- Performance optimization
@@ -340,17 +313,14 @@ npm run type-check
## 🔗 Important Files to Reference
**For Provider Implementation**:
- `src/arcade-games/number-guesser/Provider.tsx` - Socket integration pattern
- `src/arcade-games/matching/Provider.tsx` - Room config loading
**For UI Implementation**:
- `src/app/arcade/complement-race/components/` - Existing UI components
- `src/arcade-games/number-guesser/components/` - Multiplayer UI patterns
**For Testing**:
- `src/arcade-games/number-guesser/__tests__/` - Validator test patterns
- `.claude/GAME_SETTINGS_PERSISTENCE.md` - Config testing guide
@@ -380,7 +350,6 @@ npm run type-check
### What Was Wrong
I initially created a **simple quiz UI** (`Game.tsx`) from scratch, throwing away ALL the existing beautiful components:
- ❌ No RailroadTrackPath
- ❌ No SteamTrainJourney
- ❌ No PassengerCard
@@ -396,14 +365,13 @@ The user rightfully said: **"what the fuck is this game?"**
**Updated** `index.tsx` to use existing `ComplementRaceGame` from `src/app/arcade/complement-race/components/`
**Added** `dispatch` compatibility layer to Provider to bridge action creators with existing UI expectations
**Preserved** ALL existing beautiful UI components:
- Train animations
- Track visualization
- Passenger mechanics ✅
- Route celebrations
- HUD with pressure gauge
- Adaptive difficulty
- AI opponents ✅
- Train animations ✅
- Track visualization ✅
- Passenger mechanics
- Route celebrations ✅
- HUD with pressure gauge
- Adaptive difficulty
- AI opponents
### What Works Now

View File

@@ -5,14 +5,12 @@
The existing single-player UI components were deeply coupled to a specific state shape that differed from the new multiplayer state structure:
**Old Single-Player State**:
- `currentQuestion` - single question object at root level
- `correctAnswers`, `streak`, `score` - at root level
- `gamePhase: 'intro' | 'controls' | 'countdown' | 'playing' | 'results'`
- Config fields at root: `mode`, `style`, `complementDisplay`
**New Multiplayer State**:
- `currentQuestions: Record<playerId, question>` - per player
- `players: Record<playerId, PlayerState>` - stats nested in player objects
- `gamePhase: 'setup' | 'lobby' | 'countdown' | 'playing' | 'results'`
@@ -35,7 +33,6 @@ Defined an interface that matches the old single-player `GameState` shape, allow
#### 2. Local UI State
Uses `useState` to track local UI state that doesn't need server synchronization:
- `currentInput` - what user is typing
- `previousQuestion` - for animations
- `isPaused` - local pause state
@@ -50,14 +47,12 @@ Transforms multiplayer state into compatible single-player shape:
```typescript
const compatibleState = useMemo((): CompatibleGameState => {
const localPlayer = localPlayerId
? multiplayerState.players[localPlayerId]
: null;
const localPlayer = localPlayerId ? multiplayerState.players[localPlayerId] : null
// Map gamePhase: setup/lobby -> controls
let gamePhase = multiplayerState.gamePhase;
if (gamePhase === "setup" || gamePhase === "lobby") {
gamePhase = "controls";
let gamePhase = multiplayerState.gamePhase
if (gamePhase === 'setup' || gamePhase === 'lobby') {
gamePhase = 'controls'
}
return {
@@ -75,7 +70,7 @@ const compatibleState = useMemo((): CompatibleGameState => {
streak: localPlayer?.streak || 0,
// Map AI opponents to old aiRacers format
aiRacers: multiplayerState.aiOpponents.map((ai) => ({
aiRacers: multiplayerState.aiOpponents.map(ai => ({
id: ai.id,
name: ai.name,
position: ai.position,
@@ -86,8 +81,8 @@ const compatibleState = useMemo((): CompatibleGameState => {
currentInput: localUIState.currentInput,
adaptiveFeedback: localUIState.adaptiveFeedback,
// ... etc
};
}, [multiplayerState, localPlayerId, localUIState]);
}
}, [multiplayerState, localPlayerId, localUIState])
```
#### 4. Compatibility Dispatch
@@ -95,29 +90,26 @@ const compatibleState = useMemo((): CompatibleGameState => {
Maps old reducer action types to new action creators:
```typescript
const dispatch = useCallback(
(action: { type: string; [key: string]: any }) => {
switch (action.type) {
case "START_COUNTDOWN":
case "BEGIN_GAME":
startGame();
break;
const dispatch = useCallback((action: { type: string; [key: string]: any }) => {
switch (action.type) {
case 'START_COUNTDOWN':
case 'BEGIN_GAME':
startGame()
break
case "SUBMIT_ANSWER":
const responseTime = Date.now() - multiplayerState.questionStartTime;
submitAnswer(action.answer, responseTime);
break;
case 'SUBMIT_ANSWER':
const responseTime = Date.now() - multiplayerState.questionStartTime
submitAnswer(action.answer, responseTime)
break
// Local UI state actions
case "UPDATE_INPUT":
setLocalUIState((prev) => ({ ...prev, currentInput: action.input }));
break;
// Local UI state actions
case 'UPDATE_INPUT':
setLocalUIState(prev => ({ ...prev, currentInput: action.input }))
break
// ... etc
}
},
[startGame, submitAnswer, multiplayerState.questionStartTime],
);
// ... etc
}
}, [startGame, submitAnswer, multiplayerState.questionStartTime])
```
## Benefits
@@ -140,13 +132,11 @@ const dispatch = useCallback(
## Testing
### Type Checking
- ✅ No TypeScript errors in new code
- ✅ All component files compile successfully
- ✅ Only pre-existing errors remain (known @soroban/abacus-react issue)
### Format & Lint
- ✅ Code formatted with Biome
- ✅ No new lint warnings
- ✅ All style guidelines followed

View File

@@ -5,7 +5,6 @@ This document describes the production deployment infrastructure and procedures
## Infrastructure Overview
### Production Server
- **Host**: `nas.home.network` (Synology NAS DS923+)
- **Access**: SSH access required
- Must be connected to network at **730 N. Oak Park Ave**
@@ -13,52 +12,24 @@ This document describes the production deployment infrastructure and procedures
- **Project Directory**: `/volume1/homes/antialias/projects/abaci.one`
### Docker Configuration
- **Docker binary**: `/usr/local/bin/docker`
- **Docker Compose binary**: `/usr/local/bin/docker-compose`
- **Container name**: `soroban-abacus-flashcards`
- **Image**: `ghcr.io/antialias/soroban-abacus-flashcards:latest`
This deployment uses **two separate Docker Compose projects**:
1. **soroban-app** (`docker-compose.yaml`)
- Main web application
- Container: `soroban-abacus-flashcards`
- Image: `ghcr.io/antialias/soroban-abacus-flashcards:main`
- Port: 3000 (internal to Docker network)
2. **soroban-updater** (`docker-compose.updater.yaml`)
- Automatic update service
- Container: `compose-updater`
- Image: `virtualzone/compose-updater:latest`
- Checks for new images every 5 minutes
**Why separate projects?** If compose-updater was in the same project as the app, running `docker-compose down` would kill itself mid-update. Separate projects prevent this.
### Auto-Deployment with compose-updater
- **compose-updater** monitors and auto-updates containers
- **Update frequency**: Every **5 minutes** (configurable via `INTERVAL=5`)
- Works WITH docker-compose files (respects configuration, volumes, environment variables)
- Automatically cleans up old images (`CLEANUP=1`)
### Auto-Deployment
- **Watchtower** monitors and auto-updates containers
- **Update frequency**: Every **5 minutes**
- Watchtower pulls latest images and restarts containers automatically
- No manual intervention required for deployments after pushing to main
**Key advantages over Watchtower:**
- Respects docker-compose.yaml configuration
- Re-reads `.env` file on every update
- Can manage multiple docker-compose projects
- Container labels control which containers to watch:
```yaml
labels:
- "docker-compose-watcher.watch=1"
- "docker-compose-watcher.dir=/volume1/homes/antialias/projects/abaci.one"
- "com.centurylinklabs.watchtower.enable=false" # Disables Watchtower for this container
```
## Database Management
### Location
- **Database path**: `data/sqlite.db` (relative to project directory)
- **WAL files**: `data/sqlite.db-shm` and `data/sqlite.db-wal`
### Migrations
- **Automatic**: Migrations run on server startup via `server.js`
- **Migration folder**: `./drizzle`
- **Process**:
@@ -69,7 +40,6 @@ This deployment uses **two separate Docker Compose projects**:
5. Logs: `❌ Migration failed: [error]` (on failure, process exits)
### Nuke and Rebuild Database
If you need to completely reset the production database:
```bash
@@ -95,7 +65,6 @@ rm -f data/sqlite.db data/sqlite.db-shm data/sqlite.db-wal
## CI/CD Pipeline
### GitHub Actions
When code is pushed to `main` branch:
1. **Workflows triggered**:
@@ -105,157 +74,92 @@ When code is pushed to `main` branch:
- `Deploy Storybooks to GitHub Pages` - Publishes Storybook
2. **Image build**:
- Built image is tagged as `main` (also `latest` for compatibility)
- Built image is tagged as `latest`
- Pushed to GitHub Container Registry (ghcr.io)
- Typically completes within 1-2 minutes
3. **Deployment**:
- compose-updater detects new image (within 5 minutes)
- Pulls new image
- Runs `docker-compose down && docker-compose up -d`
- Cleans up old images
- Total deployment time: ~5-7 minutes from push to production (15-30 seconds downtime during restart)
- Watchtower detects new image (within 5 minutes)
- Pulls latest image
- Recreates and restarts container
- Total deployment time: ~5-7 minutes from push to production
## Manual Deployment Procedures
### Force Pull Latest Image
If you need to immediately deploy without waiting for compose-updater's next check cycle:
If you need to immediately deploy without waiting for Watchtower:
```bash
# Option 1: Restart compose-updater (triggers immediate check)
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose -f docker-compose.updater.yaml restart"
# Option 2: Manual pull and restart
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose pull && docker-compose up -d"
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && /usr/local/bin/docker-compose pull && /usr/local/bin/docker-compose up -d"
```
### Check Container Status
```bash
# Check both app and compose-updater
ssh nas.home.network "docker ps | grep -E '(soroban|compose)'"
# Check just the app
ssh nas.home.network "docker ps | grep soroban-abacus-flashcards"
ssh nas.home.network "/usr/local/bin/docker ps | grep -E '(soroban|abaci)'"
```
### View Logs
```bash
# Application logs - recent
ssh nas.home.network "docker logs --tail 100 soroban-abacus-flashcards"
# Recent logs
ssh nas.home.network "/usr/local/bin/docker logs --tail 100 soroban-abacus-flashcards"
# Application logs - follow in real-time
ssh nas.home.network "docker logs -f soroban-abacus-flashcards"
# compose-updater logs - see update activity
ssh nas.home.network "docker logs --tail 50 compose-updater"
# compose-updater logs - follow to watch for updates
ssh nas.home.network "docker logs -f compose-updater"
# Follow logs in real-time
ssh nas.home.network "/usr/local/bin/docker logs -f soroban-abacus-flashcards"
# Search for specific patterns
ssh nas.home.network "docker logs soroban-abacus-flashcards" | grep -i "error"
ssh nas.home.network "/usr/local/bin/docker logs soroban-abacus-flashcards" | grep -i "error"
```
### Restart Container
```bash
# Restart just the app (quick, minimal downtime)
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose restart"
# Full restart (down then up, recreates container)
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose down && docker-compose up -d"
# Restart compose-updater (triggers immediate update check)
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose -f docker-compose.updater.yaml restart"
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && /usr/local/bin/docker-compose restart"
```
## Checking Deployed Version
## Deployment Script
Always verify what's actually running in production:
```bash
# Get commit SHA of running container
ssh nas.home.network 'docker inspect soroban-abacus-flashcards --format="{{index .Config.Labels \"org.opencontainers.image.revision\"}}"'
# Compare with current HEAD
git rev-parse HEAD
# Or check via the deployment info modal in the app UI
```
The project includes a deployment script at `nas-deployment/deploy.sh` for manual deployments.
## Troubleshooting
### Common Issues
#### 1. Migration Failures
**Symptom**: Container keeps restarting, logs show migration errors
**Solution**:
1. Check migration files in `drizzle/` directory
2. Verify `drizzle/meta/_journal.json` is up to date
3. If migrations are corrupted, may need to nuke database (see above)
#### 2. Container Not Updating
**Symptom**: Changes pushed but production still shows old code
**Possible causes**:
- GitHub Actions build failed - check workflow status with `gh run list`
- compose-updater not running - check with `docker ps | grep compose-updater`
- compose-updater labels incorrect - check container labels
- Watchtower not running - check with `docker ps | grep watchtower`
- Image not pulled - manually pull with `docker-compose pull`
**Debugging**:
```bash
# Check compose-updater is running
ssh nas.home.network "docker ps | grep compose-updater"
# Check compose-updater logs for errors
ssh nas.home.network "docker logs --tail 50 compose-updater"
# Check container labels are correct
ssh nas.home.network "docker inspect soroban-abacus-flashcards" | grep -A3 "docker-compose-watcher"
# Should show:
# "docker-compose-watcher.watch": "1"
# "docker-compose-watcher.dir": "/volume1/homes/antialias/projects/abaci.one"
```
**Solution**:
```bash
# Option 1: Restart compose-updater to force immediate check
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose -f docker-compose.updater.yaml restart"
# Option 2: Manual pull and restart
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose pull && docker-compose up -d"
# Force pull and restart
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && /usr/local/bin/docker-compose pull && /usr/local/bin/docker-compose up -d"
```
#### 3. Missing Database Columns
**Symptom**: Errors like `SqliteError: no such column: "column_name"`
**Cause**: Migration not registered or not run
**Solution**:
1. Verify migration exists in `drizzle/` directory
2. Check migration is registered in `drizzle/meta/_journal.json`
3. If migration is new, restart container to run migrations
4. If migration is malformed, fix it and nuke database
#### 4. API Returns Unexpected Response
**Symptom**: Client shows errors but API appears to work
**Debugging**:
1. Test API directly with curl: `curl -X POST 'https://abaci.one/api/arcade/rooms' -H 'Content-Type: application/json' -d '...'`
2. Check production logs for errors
3. Verify container is running latest image:
@@ -266,26 +170,11 @@ ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-c
## Environment Variables
Production environment variables are stored in `.env` file on the server and loaded via `env_file:` in docker-compose.yaml.
Production environment variables are configured in the docker-compose.yml file on the server. Common variables:
**Critical advantage**: compose-updater re-reads the `.env` file on every update, so environment variable changes are automatically picked up without manual intervention.
Common variables:
- `AUTH_URL` - Base URL (https://abaci.one)
- `AUTH_SECRET` - Random secret for sessions (NEVER share!)
- `AUTH_TRUST_HOST=true` - Required for NextAuth v5
- `DATABASE_URL` - SQLite database path (optional, defaults to `./data/sqlite.db`)
To update environment variables:
```bash
# Edit .env file on NAS
ssh nas.home.network "vi /volume1/homes/antialias/projects/abaci.one/.env"
# Restart compose-updater (will pick up new .env on next cycle)
ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-compose -f docker-compose.updater.yaml restart"
```
- `NEXT_PUBLIC_URL` - Base URL for the application
- `DATABASE_URL` - SQLite database path
- Additional variables may be set in `.env.production` or docker-compose.yml
## Network Configuration

View File

@@ -5,13 +5,11 @@
**Mission:** Fill the gap in the USA school system by providing a complete, self-directed abacus curriculum that trains students from beginner to mastery using the Japanese kyu/dan ranking system.
**Target Users:**
- Primary: Elementary school students (ages 6-12)
- Secondary: Middle school students and adult learners
- Teachers/Parents: Dashboard for monitoring progress
**Core Experience Principles:**
1. **Integrated Learning Loop:** Tutorial → Practice → Play → Assessment → Progress
2. **Self-Directed:** Simple enough for kids to fire up and start learning independently
3. **Gamified Progression:** Games reinforce lessons, feel like play but teach skills
@@ -25,7 +23,6 @@
### ✅ What We Have (Well-Built)
**1. Interactive Abacus Component (AbacusReact)**
- Highly polished, production-ready
- Excellent pedagogical features (bead highlighting, direction arrows, tooltips)
- Multiple color schemes and accessibility options
@@ -33,7 +30,6 @@
- **Rating: 95% Complete**
**2. Game System (4 Games)**
- Memory Lightning (memorization skills)
- Matching Pairs Battle (pattern recognition, complements)
- Card Sorting (visual literacy, ordering)
@@ -43,7 +39,6 @@
- **Rating: 80% Complete** (games exist but need curriculum integration)
**3. Tutorial Infrastructure**
- Tutorial player with step-based guidance
- Tutorial editor for content creation
- Bead highlighting system for instruction
@@ -51,14 +46,12 @@
- **Rating: 70% Complete** (infrastructure exists but lacks content)
**4. Real-time Multiplayer**
- Socket.IO integration
- Room-based architecture
- State synchronization
- **Rating: 90% Complete**
**5. Flashcard Generator**
- PDF/PNG/SVG export
- Customizable layouts and themes
- **Rating: 100% Complete**
@@ -66,7 +59,6 @@
### ⚠️ What We Have (Partially Built)
**1. Progress Tracking**
- Basic user stats (games played, wins, accuracy)
- No skill-level tracking
- No tutorial completion tracking
@@ -74,14 +66,12 @@
- **Rating: 30% Complete**
**2. Tutorial Content**
- One example tutorial (GuidedAdditionTutorial)
- Type system for tutorials defined
- No comprehensive curriculum
- **Rating: 15% Complete**
**3. Assessment System**
- Per-game scoring exists
- Achievement system exists
- No formal tests or certification
@@ -106,7 +96,6 @@
### Beginner Levels (Kyu)
**10 Kyu - "First Steps"**
- Age: 6-7 years
- Skills: Basic bead manipulation, numbers 1-10
- Curriculum: Recognize and set numbers on abacus, understand place value
@@ -114,60 +103,51 @@
- Games: Card Sorting (visual recognition), Memory Lightning (basic)
**9 Kyu - "Number Explorer"**
- Skills: Addition/subtraction with no carry (1-9)
- Curriculum: Friends of 5 concept introduction
- Assessment: 20 problems, 2-digit addition/subtraction, no carry, 80% accuracy
- Games: Complement Race (practice mode), Matching Pairs (numerals)
**8 Kyu - "Complement Apprentice"**
- Skills: Friends of 5 mastery, introduction to friends of 10
- Curriculum: All combinations that make 5, carry concepts
- Assessment: 30 problems including carries using friends of 5, 85% accuracy
- Games: Complement Race (friends-5 sprint), Matching Pairs (complement pairs)
**7 Kyu - "Addition Warrior"**
- Skills: Friends of 10 mastery, 2-digit addition/subtraction with carries
- Curriculum: All combinations that make 10, mixed complement strategies
- Assessment: 40 problems, 2-3 digit calculations, mixed operations, 85% accuracy
- Games: Complement Race (friends-10 sprint), All games at medium difficulty
**6 Kyu - "Speed Calculator"**
- Skills: Multi-digit addition/subtraction (3-4 digits), speed emphasis
- Curriculum: Chain calculations, mental imagery beginning
- Assessment: 50 problems, 3-4 digits, 3 minutes time limit, 90% accuracy
- Games: Complement Race (survival mode), Memory Lightning (medium)
**5 Kyu - "Multiplication Initiate"**
- Skills: Single-digit multiplication (1-5)
- Curriculum: Multiplication tables 1-5, abacus multiplication method
- Assessment: 30 multiplication problems, 40 add/subtract problems, 90% accuracy
- Games: All games at hard difficulty
**4 Kyu - "Multiplication Master"**
- Skills: Full multiplication tables (1-9), 2-digit × 1-digit
- Curriculum: All multiplication patterns, division introduction
- Assessment: 40 multiplication, 20 division, 40 add/subtract, 90% accuracy
**3 Kyu - "Division Explorer"**
- Skills: Division mastery (2-digit ÷ 1-digit), mixed operations
- Curriculum: Division algorithm, remainders, mixed problem solving
- Assessment: 100 mixed problems in 10 minutes, 92% accuracy
**2 Kyu - "Advanced Operator"**
- Skills: Multi-digit multiplication/division, decimals introduction
- Curriculum: 3-digit × 2-digit, decimals, percentages
- Assessment: 120 mixed problems including decimals, 10 minutes, 93% accuracy
**1 Kyu - "Pre-Mastery"**
- Skills: Decimal operations, fractions, complex multi-step problems
- Curriculum: Real-world applications, word problems
- Assessment: 150 mixed problems, 10 minutes, 95% accuracy
@@ -176,18 +156,15 @@
### Master Levels (Dan)
**1 Dan - "Shodan" (First Degree)**
- Skills: Mental imagery without abacus, complex calculations
- Assessment: 200 mixed problems, 10 minutes, 96% accuracy
- Mental arithmetic certification
**2 Dan - "Nidan"**
- Skills: Advanced mental calculation, speed competitions
- Assessment: 250 problems, 10 minutes, 97% accuracy
**3 Dan - "Sandan"**
- Skills: Championship-level speed and accuracy
- Assessment: 300 problems, 10 minutes, 98% accuracy
@@ -211,12 +188,10 @@
### Example: Teaching "Friends of 5"
**1. Assessment (Placement)**
- Quick quiz: "Can you add 3 + 4 using the abacus?"
- Result: Student struggles → Assign Friends of 5 tutorial
**2. Learn (Tutorial)**
- Interactive tutorial: "Friends of 5"
- Steps:
1. Show that 5 = 1+4, 2+3, 3+2, 4+1
@@ -226,27 +201,23 @@
5. Practice all combinations
**3. Practice (Structured Exercises)**
- 20 problems: Set number, add its friend
- Real-time feedback on bead movements
- Hints available: "Use the heaven bead!"
- Must achieve 90% accuracy to proceed
**4. Play (Game Reinforcement)**
- Complement Race: Friends-5 mode
- Matching Pairs: Match numbers that make 5
- Makes practice feel like play
**5. Test (Formal Assessment)**
- 30 problems mixing friends-5 with previous skills
- Timed: 5 minutes
- Must achieve 85% to certify skill
- Can retake after reviewing mistakes
**6. Advance (Progress Update)**
- Friends of 5 skill marked as "Mastered"
- Unlock: Friends of 10 tutorial
- Update skill matrix
@@ -261,76 +232,76 @@
```typescript
// Skill taxonomy
enum SkillCategory {
NUMBER_SENSE = "number-sense",
ADDITION = "addition",
SUBTRACTION = "subtraction",
MULTIPLICATION = "multiplication",
DIVISION = "division",
MENTAL_CALC = "mental-calculation",
COMPLEMENTS = "complements",
SPEED = "speed",
ACCURACY = "accuracy",
NUMBER_SENSE = 'number-sense',
ADDITION = 'addition',
SUBTRACTION = 'subtraction',
MULTIPLICATION = 'multiplication',
DIVISION = 'division',
MENTAL_CALC = 'mental-calculation',
COMPLEMENTS = 'complements',
SPEED = 'speed',
ACCURACY = 'accuracy'
}
// Individual skill (atomic unit)
interface Skill {
id: string;
name: string;
category: SkillCategory;
kyuLevel: number; // Which kyu level this skill belongs to
prerequisiteSkills: string[]; // Must master these first
description: string;
estimatedPracticeTime: number; // minutes
id: string
name: string
category: SkillCategory
kyuLevel: number // Which kyu level this skill belongs to
prerequisiteSkills: string[] // Must master these first
description: string
estimatedPracticeTime: number // minutes
}
// Learning module (collection of related skills)
interface Module {
id: string;
title: string;
kyuLevel: number;
description: string;
skills: string[]; // Skill IDs
estimatedCompletionTime: number; // hours
sequence: number; // Order within kyu level
id: string
title: string
kyuLevel: number
description: string
skills: string[] // Skill IDs
estimatedCompletionTime: number // hours
sequence: number // Order within kyu level
}
// Tutorial (teaches one or more skills)
interface Tutorial {
id: string;
skillIds: string[];
moduleId: string;
type: "interactive" | "video" | "reading";
content: TutorialStep[];
estimatedDuration: number;
id: string
skillIds: string[]
moduleId: string
type: 'interactive' | 'video' | 'reading'
content: TutorialStep[]
estimatedDuration: number
}
// Practice set (reinforces skills)
interface PracticeSet {
id: string;
skillIds: string[];
problemCount: number;
timeLimit?: number;
passingAccuracy: number;
difficulty: "easy" | "medium" | "hard";
id: string
skillIds: string[]
problemCount: number
timeLimit?: number
passingAccuracy: number
difficulty: 'easy' | 'medium' | 'hard'
}
// Game mapping (which games teach which skills)
interface GameSkillMapping {
gameId: string;
skillIds: string[];
difficulty: string;
recommendedKyuRange: [number, number];
gameId: string
skillIds: string[]
difficulty: string
recommendedKyuRange: [number, number]
}
// Assessment (formal test)
interface Assessment {
id: string;
type: "placement" | "skill-check" | "kyu-certification";
kyuLevel?: number;
skillIds: string[];
problemCount: number;
timeLimit: number;
passingAccuracy: number;
id: string
type: 'placement' | 'skill-check' | 'kyu-certification'
kyuLevel?: number
skillIds: string[]
problemCount: number
timeLimit: number
passingAccuracy: number
}
```
@@ -387,7 +358,6 @@ interface Assessment {
**Goal:** Students can learn and certify 10 Kyu and 9 Kyu levels
**Database Schema Updates:**
- [ ] Create `skills` table
- [ ] Create `modules` table
- [ ] Create `curriculum_tutorials` table (links tutorials to skills)
@@ -402,7 +372,6 @@ interface Assessment {
- [ ] Extend `user_stats` table: add `currentKyuLevel`, `currentDanLevel`, `skillsMastered`
**Tutorial Content Creation:**
- [ ] 10 Kyu tutorials (5 tutorials):
1. Introduction to Abacus
2. Understanding Place Value
@@ -415,14 +384,12 @@ interface Assessment {
3. Friends of 5 - Subtraction
**Practice Sets:**
- [ ] Build practice set generator for each skill
- [ ] Implement immediate feedback system
- [ ] Add hint system for common mistakes
- [ ] Track accuracy and time per problem
**Assessment System:**
- [ ] Build placement test component (determines starting level)
- [ ] Build skill-check test component (practice test before certification)
- [ ] Build kyu certification test component (formal test)
@@ -431,7 +398,6 @@ interface Assessment {
- [ ] Allow test retakes with review of mistakes
**Game Integration:**
- [ ] Map existing games to skills
- Memory Lightning → Number recognition, memory
- Card Sorting → Visual pattern recognition, ordering
@@ -441,7 +407,6 @@ interface Assessment {
- [ ] Track game performance per skill
**Student Dashboard:**
- [ ] Create dashboard showing:
- Current kyu level
- Skills mastered / in progress / locked
@@ -452,7 +417,6 @@ interface Assessment {
- [ ] Add celebratory animations for milestones
**Core User Flow:**
- [ ] Onboarding: Placement test → Assign kyu level
- [ ] Home: Dashboard shows next recommended activity
- [ ] Click "Start Learning" → Next tutorial
@@ -463,7 +427,6 @@ interface Assessment {
- [ ] Celebration and badge award
**Deliverables:**
- Students can complete 10 Kyu and 9 Kyu
- ~8 tutorials
- ~10 skills defined
@@ -478,7 +441,6 @@ interface Assessment {
**Goal:** Complete beginner curriculum through multiplication introduction
**Content Creation:**
- [ ] 8 Kyu: Friends of 10 tutorials and practice (4 weeks)
- [ ] 7 Kyu: Mixed complements, 2-digit operations (4 weeks)
- [ ] 6 Kyu: Multi-digit, speed training (6 weeks)
@@ -486,7 +448,6 @@ interface Assessment {
- Total: ~40 tutorials, ~30 skills
**Enhanced Features:**
- [ ] Adaptive difficulty in practice sets (adjusts based on performance)
- [ ] Spaced repetition system (review mastered skills periodically)
- [ ] Daily recommended practice (10-15 min sessions)
@@ -494,13 +455,11 @@ interface Assessment {
- [ ] Peer comparison (anonymous, optional)
**New Games:**
- [ ] Multiplication tables game
- [ ] Speed drill game (flash calculation)
- [ ] Mental math game (visualization without physical abacus)
**Parent/Teacher Dashboard:**
- [ ] View student progress
- [ ] See time spent learning
- [ ] Review test results
@@ -508,7 +467,6 @@ interface Assessment {
- [ ] Generate progress reports
**Gamification Enhancements:**
- [ ] Achievement badges for milestones
- [ ] Experience points (XP) system
- [ ] Level-up animations
@@ -516,7 +474,6 @@ interface Assessment {
- [ ] Virtual rewards (stickers, themes)
**Deliverables:**
- Complete 8-5 Kyu curriculum
- ~50 total tutorials (cumulative)
- ~40 total skills (cumulative)
@@ -531,7 +488,6 @@ interface Assessment {
**Goal:** Advanced operations, real-world applications, mental calculation
**Content Creation:**
- [ ] 4 Kyu: Full multiplication, division introduction (8 weeks)
- [ ] 3 Kyu: Division mastery, mixed operations (8 weeks)
- [ ] 2 Kyu: Decimals, percentages (10 weeks)
@@ -539,35 +495,30 @@ interface Assessment {
- Total: ~60 additional tutorials, ~40 additional skills
**Mental Calculation Training:**
- [ ] Visualization exercises (see abacus in mind)
- [ ] Flash anzan (rapid mental calculation)
- [ ] Mental calculation games
- [ ] Transition from physical to mental abacus
**Real-World Applications:**
- [ ] Shopping math (money, change, discounts)
- [ ] Measurement conversions
- [ ] Time calculations
- [ ] Real-world word problems
**Competition Features:**
- [ ] Speed competitions (leaderboards)
- [ ] Accuracy challenges
- [ ] Weekly tournaments
- [ ] Regional/global rankings (optional)
**AI Tutor Assistant:**
- [ ] Smart hints during practice
- [ ] Personalized learning paths
- [ ] Concept explanations on demand
- [ ] Answer specific questions ("Why do I use friends of 5 here?")
**Deliverables:**
- Complete 4-1 Kyu curriculum
- ~110 total tutorials (cumulative)
- ~80 total skills (cumulative)
@@ -582,14 +533,12 @@ interface Assessment {
**Goal:** Championship-level speed and accuracy, mental calculation mastery
**Content Creation:**
- [ ] Dan level certification tests
- [ ] Advanced mental calculation curriculum
- [ ] Championship preparation materials
- [ ] Expert-level problem sets
**Advanced Features:**
- [ ] Customized training plans for dan levels
- [ ] Video lessons from expert abacus users
- [ ] Community forum for advanced learners
@@ -597,13 +546,11 @@ interface Assessment {
- [ ] Certification/diploma generation (printable)
**Integration with Standards:**
- [ ] Align with League of Soroban of Americas standards
- [ ] Japan Abacus Committee certification mapping
- [ ] International competition preparation
**Deliverables:**
- 1-10 Dan curriculum
- Certification system
- Community features
@@ -614,14 +561,12 @@ interface Assessment {
### Phase 5: Ecosystem (Months 18+) - "Complete Platform"
**Content Management System:**
- [ ] Tutorial builder UI (create without code)
- [ ] Content versioning
- [ ] Community-contributed content (vetted)
- [ ] Multilingual support (Spanish, Japanese, Hindi)
**Classroom Features:**
- [ ] Teacher creates classes
- [ ] Bulk student enrollment
- [ ] Class-wide assignments
@@ -629,7 +574,6 @@ interface Assessment {
- [ ] Live teaching mode (project for class)
**Analytics & Insights:**
- [ ] Student learning velocity
- [ ] Skill gap analysis
- [ ] Predictive success modeling
@@ -637,20 +581,17 @@ interface Assessment {
- [ ] Export data for research
**Mobile App:**
- [ ] iOS and React Native apps
- [ ] Offline mode
- [ ] Sync across devices
**Integrations:**
- [ ] Google Classroom
- [ ] Canvas LMS
- [ ] Schoology
- [ ] Export to SIS systems
**Advanced Gamification:**
- [ ] Story mode (learning quest)
- [ ] Cooperative challenges
- [ ] Guild/team system
@@ -661,7 +602,6 @@ interface Assessment {
## Success Metrics
### Student Engagement
- **Daily Active Users (DAU):** Target 40% of registered students
- **Weekly Active Users (WAU):** Target 70% of registered students
- **Average session time:** 20-30 minutes
@@ -670,27 +610,23 @@ interface Assessment {
- **Streak length:** Average 7+ days
### Learning Outcomes
- **Certification pass rate:** >70% on first attempt per kyu level
- **Skill mastery rate:** >85% accuracy on mastered skills after 30 days
- **Time to mastery:** Track average time per kyu level
- **Progression velocity:** Students advance 1 kyu level per 4-8 weeks (varies by level)
### Content Quality
- **Tutorial completion rate:** >90%
- **Practice set completion rate:** >85%
- **Game play rate:** >60% of students play games weekly
- **Assessment completion rate:** >75%
### Platform Health
- **System uptime:** >99.5%
- **Load time:** <2 seconds
- **Error rate:** <0.1%
### Business/Growth
- **Monthly signups:** Track growth month-over-month
- **Paid conversion** (if applicable): Target 10-20%
- **Teacher/school adoption:** Track institutional users
@@ -703,7 +639,6 @@ interface Assessment {
### Database Changes Priority
**Immediate (Phase 1):**
```sql
-- Skills and curriculum structure
CREATE TABLE skills (...)
@@ -726,13 +661,11 @@ CREATE TABLE game_skill_mappings (...)
```
**Phase 2:**
- Add spaced repetition tables
- Achievement tracking enhancements
- Peer comparison data
**Phase 3:**
- Mental calculation tracking
- Competition results
- AI tutor interaction logs
@@ -740,21 +673,18 @@ CREATE TABLE game_skill_mappings (...)
### API Endpoints Needed
**Progress & Skills:**
- `GET /api/student/progress` - Current kyu level, skills, next steps
- `GET /api/student/skills/:skillId` - Skill details and progress
- `POST /api/student/skills/:skillId/practice` - Record practice attempt
- `GET /api/student/dashboard` - Dashboard data
**Curriculum:**
- `GET /api/curriculum/kyu/:level` - All modules for kyu level
- `GET /api/curriculum/modules/:moduleId` - Module details
- `GET /api/curriculum/tutorials/:tutorialId` - Tutorial content
- `GET /api/curriculum/next` - Next recommended activity
**Assessments:**
- `POST /api/assessments/placement` - Take placement test
- `POST /api/assessments/skill-check/:skillId` - Practice test
- `POST /api/assessments/certification/:kyuLevel` - Certification test
@@ -762,13 +692,11 @@ CREATE TABLE game_skill_mappings (...)
- `GET /api/assessments/:assessmentId/results` - Get results
**Games:**
- `GET /api/games/recommended` - Games for current skills
- `POST /api/games/:gameId/result` - Log game completion
- `GET /api/games/:gameId/skills` - Which skills this game teaches
**Teacher/Parent:**
- `GET /api/teacher/students` - List of students
- `GET /api/teacher/students/:studentId/progress` - Student progress
- `POST /api/teacher/assignments` - Create assignment
@@ -932,28 +860,24 @@ CREATE TABLE game_skill_mappings (...)
## Next Immediate Steps
### Week 1: Database Schema Design
- [ ] Design complete schema for Phase 1
- [ ] Write migration scripts
- [ ] Document schema decisions
- [ ] Review with stakeholders
### Week 2-3: Content Planning
- [ ] Write detailed 10 Kyu curriculum outline
- [ ] Write detailed 9 Kyu curriculum outline
- [ ] Define all skills for 10-9 Kyu
- [ ] Map skills to existing games
### Week 4-5: Tutorial Content Creation
- [ ] Write 5 tutorials for 10 Kyu
- [ ] Write 3 tutorials for 9 Kyu
- [ ] Create interactive steps with highlighting
- [ ] Add kid-friendly explanations
### Week 6-7: Assessment System Build
- [ ] Build assessment component UI
- [ ] Implement grading engine
- [ ] Create placement test (20 problems)
@@ -961,21 +885,18 @@ CREATE TABLE game_skill_mappings (...)
- [ ] Create 9 Kyu certification test (40 problems)
### Week 8-9: Practice System
- [ ] Build practice session component
- [ ] Implement problem generator for each skill
- [ ] Add immediate feedback system
- [ ] Create hint system
### Week 10-11: Student Dashboard
- [ ] Design dashboard UI (kid-friendly)
- [ ] Build progress visualization
- [ ] Implement "next recommended activity" logic
- [ ] Add achievement display
### Week 12: Integration & Testing
- [ ] Connect all pieces: tutorials → practice → games → assessment
- [ ] Test complete user flow
- [ ] User testing with kids
@@ -1003,7 +924,6 @@ CREATE TABLE game_skill_mappings (...)
This roadmap provides a clear path from current state (scattered features) to target state (complete educational platform). The phased approach allows incremental delivery while maintaining focus on core learning experience.
**Estimated Timeline:**
- Phase 1 (10-9 Kyu MVP): 3 months
- Phase 2 (8-5 Kyu): 5 months
- Phase 3 (4-1 Kyu): 6 months

View File

@@ -21,7 +21,6 @@ CREATE TABLE room_game_configs (
```
**Benefits:**
- ✅ Type-safe config access with shared types
- ✅ Smaller rows (only configs for games that have been used)
- ✅ Easier updates (single row vs entire JSON blob)
@@ -30,7 +29,6 @@ CREATE TABLE room_game_configs (
- ✅ Can query/index individual game settings
**Example Row:**
```json
{
"id": "clxyz123",
@@ -54,35 +52,34 @@ All game configs are defined in `src/lib/arcade/game-configs.ts`:
```typescript
// Shared config types (single source of truth)
export interface MatchingGameConfig {
gameType: "abacus-numeral" | "complement-pairs";
difficulty: number;
turnTimer: number;
gameType: 'abacus-numeral' | 'complement-pairs'
difficulty: number
turnTimer: number
}
export interface MemoryQuizGameConfig {
selectedCount: 2 | 5 | 8 | 12 | 15;
displayTime: number;
selectedDifficulty: DifficultyLevel;
playMode: "cooperative" | "competitive";
selectedCount: 2 | 5 | 8 | 12 | 15
displayTime: number
selectedDifficulty: DifficultyLevel
playMode: 'cooperative' | 'competitive'
}
// Default configs
export const DEFAULT_MATCHING_CONFIG: MatchingGameConfig = {
gameType: "abacus-numeral",
gameType: 'abacus-numeral',
difficulty: 6,
turnTimer: 30,
};
}
export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
selectedCount: 5,
displayTime: 2.0,
selectedDifficulty: "easy",
playMode: "cooperative",
};
selectedDifficulty: 'easy',
playMode: 'cooperative',
}
```
**Why This Matters:**
- TypeScript enforces that validators, helpers, and API routes all use the same types
- Adding a new setting requires changes in only ONE place (the type definition)
- Impossible to forget a setting or use wrong type
@@ -92,50 +89,43 @@ export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
Settings persistence requires coordination between FOUR systems:
### 1. Helper Functions
**Location:** `src/lib/arcade/game-config-helpers.ts`
**Responsibilities:**
- Read/write game configs from `room_game_configs` table
- Provide type-safe access with automatic defaults
- Validate configs at runtime
**Key Functions:**
```typescript
// Get config with defaults (type-safe)
const config = await getGameConfig(roomId, "memory-quiz");
const config = await getGameConfig(roomId, 'memory-quiz')
// Returns: MemoryQuizGameConfig
// Set/update config (upsert)
await setGameConfig(roomId, "memory-quiz", {
playMode: "competitive",
await setGameConfig(roomId, 'memory-quiz', {
playMode: 'competitive',
selectedCount: 8,
});
})
// Get all game configs for a room
const allConfigs = await getAllGameConfigs(roomId);
const allConfigs = await getAllGameConfigs(roomId)
// Returns: { matching?: MatchingGameConfig, 'memory-quiz'?: MemoryQuizGameConfig }
```
### 2. API Routes
**Location:**
- `src/app/api/arcade/rooms/current/route.ts` (read)
- `src/app/api/arcade/rooms/[roomId]/settings/route.ts` (write)
**Responsibilities:**
- Aggregate game configs from database
- Return them to client in `room.gameConfig`
- Write config updates to `room_game_configs` table
**Read Example:** `GET /api/arcade/rooms/current`
```typescript
const gameConfig = await getAllGameConfigs(roomId);
const gameConfig = await getAllGameConfigs(roomId)
return NextResponse.json({
room: {
@@ -144,61 +134,54 @@ return NextResponse.json({
},
members,
memberPlayers,
});
})
```
**Write Example:** `PATCH /api/arcade/rooms/[roomId]/settings`
```typescript
if (body.gameConfig !== undefined) {
// body.gameConfig: { matching: {...}, memory-quiz: {...} }
for (const [gameName, config] of Object.entries(body.gameConfig)) {
await setGameConfig(roomId, gameName, config);
await setGameConfig(roomId, gameName, config)
}
}
```
### 3. Socket Server (Session Creation)
**Location:** `src/socket-server.ts:70-90`
**Responsibilities:**
- Create initial arcade session when user joins room
- Read saved settings using `getGameConfig()` helper
- Pass settings to validator's `getInitialState()`
**Example:**
```typescript
const room = await getRoomById(roomId);
const validator = getValidator(room.gameName as GameName);
const room = await getRoomById(roomId)
const validator = getValidator(room.gameName as GameName)
// Get config from database (type-safe, includes defaults)
const gameConfig = await getGameConfig(roomId, room.gameName as GameName);
const gameConfig = await getGameConfig(roomId, room.gameName as GameName)
// Pass to validator (types match automatically)
const initialState = validator.getInitialState(gameConfig);
const initialState = validator.getInitialState(gameConfig)
await createArcadeSession({ userId, gameName, initialState, roomId });
await createArcadeSession({ userId, gameName, initialState, roomId })
```
**Key Point:** No more manual config extraction or default fallbacks!
### 4. Game Validators
**Location:** `src/lib/arcade/validation/*Validator.ts`
**Responsibilities:**
- Define `getInitialState()` method with shared config type
- Create initial game state from config
- TypeScript enforces all settings are handled
**Example:** `MemoryQuizGameValidator.ts`
```typescript
import type { MemoryQuizGameConfig } from "@/lib/arcade/game-configs";
import type { MemoryQuizGameConfig } from '@/lib/arcade/game-configs'
class MemoryQuizGameValidator {
getInitialState(config: MemoryQuizGameConfig): SorobanQuizState {
@@ -206,59 +189,52 @@ class MemoryQuizGameValidator {
selectedCount: config.selectedCount,
displayTime: config.displayTime,
selectedDifficulty: config.selectedDifficulty,
playMode: config.playMode, // TypeScript ensures this field exists!
playMode: config.playMode, // TypeScript ensures this field exists!
// ...other state
};
}
}
}
```
### 5. Client Providers (Unchanged)
**Location:** `src/app/arcade/{game}/context/Room{Game}Provider.tsx`
**Responsibilities:**
- Read settings from `roomData.gameConfig[gameName]`
- Merge with `initialState` defaults
- Works transparently with new backend structure
**Example:** `RoomMemoryQuizProvider.tsx:211-233`
```typescript
const mergedInitialState = useMemo(() => {
const gameConfig = roomData?.gameConfig as Record<string, any>;
const savedConfig = gameConfig?.["memory-quiz"];
const gameConfig = roomData?.gameConfig as Record<string, any>
const savedConfig = gameConfig?.['memory-quiz']
if (!savedConfig) {
return initialState;
return initialState
}
return {
...initialState,
selectedCount: savedConfig.selectedCount ?? initialState.selectedCount,
displayTime: savedConfig.displayTime ?? initialState.displayTime,
selectedDifficulty:
savedConfig.selectedDifficulty ?? initialState.selectedDifficulty,
selectedDifficulty: savedConfig.selectedDifficulty ?? initialState.selectedDifficulty,
playMode: savedConfig.playMode ?? initialState.playMode,
};
}, [roomData?.gameConfig]);
}
}, [roomData?.gameConfig])
```
## Common Bugs and Solutions
### Bug #1: Settings Not Persisting
**Symptom:** Settings reset to defaults after game switch
**Root Cause:** One of the following:
1. API route not writing to `room_game_configs` table
2. Helper function not being used correctly
3. Validator not using shared config type
**Solution:** Verify the data flow:
```bash
# 1. Check database write
SELECT * FROM room_game_configs WHERE room_id = '...';
@@ -274,13 +250,11 @@ SELECT * FROM room_game_configs WHERE room_id = '...';
```
### Bug #2: TypeScript Errors About Missing Fields
**Symptom:** `Property '{field}' is missing in type ...`
**Root Cause:** Validator's `getInitialState()` signature doesn't match shared config type
**Solution:** Import and use the shared config type:
```typescript
// ❌ WRONG
getInitialState(config: {
@@ -296,19 +270,17 @@ getInitialState(config: MemoryQuizGameConfig): SorobanQuizState
```
### Bug #3: Settings Wiped When Returning to Game Selection
**Symptom:** Settings reset when going back to game selection
**Root Cause:** Sending `gameConfig: null` in PATCH request
**Solution:** Only send `gameName: null`, don't touch gameConfig:
```typescript
// ❌ WRONG
body: JSON.stringify({ gameName: null, gameConfig: null });
body: JSON.stringify({ gameName: null, gameConfig: null })
// ✅ CORRECT
body: JSON.stringify({ gameName: null });
body: JSON.stringify({ gameName: null })
```
## Debugging Checklist
@@ -345,23 +317,22 @@ When a setting doesn't persist:
To add a new setting to an existing game:
1. **Update the shared config type** (`game-configs.ts`):
```typescript
export interface MemoryQuizGameConfig {
selectedCount: 2 | 5 | 8 | 12 | 15;
displayTime: number;
selectedDifficulty: DifficultyLevel;
playMode: "cooperative" | "competitive";
newSetting: string; // ← Add here
selectedCount: 2 | 5 | 8 | 12 | 15
displayTime: number
selectedDifficulty: DifficultyLevel
playMode: 'cooperative' | 'competitive'
newSetting: string // ← Add here
}
export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
selectedCount: 5,
displayTime: 2.0,
selectedDifficulty: "easy",
playMode: "cooperative",
newSetting: "default", // ← Add default
};
selectedDifficulty: 'easy',
playMode: 'cooperative',
newSetting: 'default', // ← Add default
}
```
2. **TypeScript will now enforce:**
@@ -370,7 +341,6 @@ export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
- ✅ Client providers will need to handle it
3. **Update the validator** (`*Validator.ts`):
```typescript
getInitialState(config: MemoryQuizGameConfig): SorobanQuizState {
return {
@@ -399,13 +369,11 @@ Manual test procedure:
## Migration Notes
**Old Schema:**
- Settings stored in `arcade_rooms.game_config` JSON column
- Config stored directly for currently selected game only
- Config lost when switching games
**New Schema:**
- Settings stored in `room_game_configs` table
- One row per game per room
- Unique constraint on (room_id, game_name)
@@ -414,14 +382,12 @@ Manual test procedure:
**Migration:** See `.claude/MANUAL_MIGRATION_0011.md` for complete details
**Summary:**
- Manual migration applied on 2025-10-15
- Created `room_game_configs` table via sqlite3 CLI
- Migrated 6000 existing configs (5991 matching, 9 memory-quiz)
- Table created directly instead of through drizzle migration system
**Rollback Plan:**
- Old `game_config` column still exists in `arcade_rooms` table
- Old data preserved (was only read, not deleted)
- Can revert to reading from old column if needed
@@ -430,31 +396,26 @@ Manual test procedure:
## Architecture Benefits
**Type Safety:**
- Single source of truth for config types
- TypeScript enforces consistency everywhere
- Impossible to forget a setting
**DRY (Don't Repeat Yourself):**
- No duplicated default values
- No manual config extraction
- No manual merging with defaults
**Maintainability:**
- Adding a setting touches fewer places
- Clear separation of concerns
- Easier to trace data flow
**Performance:**
- Smaller database rows
- Better query performance
- Less network payload
**Correctness:**
- Runtime validation available
- Database constraints (unique index)
- Impossible to create duplicate configs

View File

@@ -19,16 +19,16 @@
// src/lib/arcade/game-configs.ts
export interface MatchingGameConfig {
gameType: "abacus-numeral" | "complement-pairs";
difficulty: number;
turnTimer: number;
gameType: 'abacus-numeral' | 'complement-pairs'
difficulty: number
turnTimer: number
}
export interface MemoryQuizGameConfig {
selectedCount: 2 | 5 | 8 | 12 | 15;
displayTime: number;
selectedDifficulty: DifficultyLevel;
playMode: "cooperative" | "competitive";
selectedCount: 2 | 5 | 8 | 12 | 15
displayTime: number
selectedDifficulty: DifficultyLevel
playMode: 'cooperative' | 'competitive'
}
export interface ComplementRaceGameConfig {
@@ -36,28 +36,27 @@ export interface ComplementRaceGameConfig {
}
export interface RoomGameConfig {
matching?: MatchingGameConfig;
"memory-quiz"?: MemoryQuizGameConfig;
"complement-race"?: ComplementRaceGameConfig;
matching?: MatchingGameConfig
'memory-quiz'?: MemoryQuizGameConfig
'complement-race'?: ComplementRaceGameConfig
}
// Default configs
export const DEFAULT_MATCHING_CONFIG: MatchingGameConfig = {
gameType: "abacus-numeral",
gameType: 'abacus-numeral',
difficulty: 6,
turnTimer: 30,
};
}
export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
selectedCount: 5,
displayTime: 2.0,
selectedDifficulty: "easy",
playMode: "cooperative",
};
selectedDifficulty: 'easy',
playMode: 'cooperative',
}
```
**Benefits:**
- Single source of truth for each game's settings
- TypeScript enforces consistency across codebase
- Easy to see what settings each game has
@@ -71,53 +70,47 @@ export const DEFAULT_MEMORY_QUIZ_CONFIG: MemoryQuizGameConfig = {
```typescript
// src/lib/arcade/game-config-helpers.ts
import type { GameName } from "./validation";
import type {
RoomGameConfig,
MatchingGameConfig,
MemoryQuizGameConfig,
} from "./game-configs";
import {
DEFAULT_MATCHING_CONFIG,
DEFAULT_MEMORY_QUIZ_CONFIG,
} from "./game-configs";
import type { GameName } from './validation'
import type { RoomGameConfig, MatchingGameConfig, MemoryQuizGameConfig } from './game-configs'
import { DEFAULT_MATCHING_CONFIG, DEFAULT_MEMORY_QUIZ_CONFIG } from './game-configs'
/**
* Get game-specific config from room's gameConfig with defaults
*/
export function getGameConfig<T extends GameName>(
roomGameConfig: RoomGameConfig | null | undefined,
gameName: T,
): T extends "matching"
gameName: T
): T extends 'matching'
? MatchingGameConfig
: T extends "memory-quiz"
? MemoryQuizGameConfig
: never {
: T extends 'memory-quiz'
? MemoryQuizGameConfig
: never {
if (!roomGameConfig) {
return getDefaultGameConfig(gameName) as any;
return getDefaultGameConfig(gameName) as any
}
const savedConfig = roomGameConfig[gameName];
const savedConfig = roomGameConfig[gameName]
if (!savedConfig) {
return getDefaultGameConfig(gameName) as any;
return getDefaultGameConfig(gameName) as any
}
// Merge saved config with defaults to handle missing fields
const defaults = getDefaultGameConfig(gameName);
return { ...defaults, ...savedConfig } as any;
const defaults = getDefaultGameConfig(gameName)
return { ...defaults, ...savedConfig } as any
}
function getDefaultGameConfig(gameName: GameName) {
switch (gameName) {
case "matching":
return DEFAULT_MATCHING_CONFIG;
case "memory-quiz":
return DEFAULT_MEMORY_QUIZ_CONFIG;
case "complement-race":
case 'matching':
return DEFAULT_MATCHING_CONFIG
case 'memory-quiz':
return DEFAULT_MEMORY_QUIZ_CONFIG
case 'complement-race':
// return DEFAULT_COMPLEMENT_RACE_CONFIG
throw new Error("complement-race config not implemented");
throw new Error('complement-race config not implemented')
default:
throw new Error(`Unknown game: ${gameName}`);
throw new Error(`Unknown game: ${gameName}`)
}
}
@@ -127,16 +120,10 @@ function getDefaultGameConfig(gameName: GameName) {
export function updateGameConfig<T extends GameName>(
currentRoomConfig: RoomGameConfig | null | undefined,
gameName: T,
updates: Partial<
T extends "matching"
? MatchingGameConfig
: T extends "memory-quiz"
? MemoryQuizGameConfig
: never
>,
updates: Partial<T extends 'matching' ? MatchingGameConfig : T extends 'memory-quiz' ? MemoryQuizGameConfig : never>
): RoomGameConfig {
const current = currentRoomConfig || {};
const gameConfig = current[gameName] || getDefaultGameConfig(gameName);
const current = currentRoomConfig || {}
const gameConfig = current[gameName] || getDefaultGameConfig(gameName)
return {
...current,
@@ -144,57 +131,53 @@ export function updateGameConfig<T extends GameName>(
...gameConfig,
...updates,
},
};
}
}
```
**Usage in socket-server.ts:**
```typescript
// BEFORE (error-prone, duplicated)
const memoryQuizConfig = (room.gameConfig as any)?.["memory-quiz"] || {};
const memoryQuizConfig = (room.gameConfig as any)?.['memory-quiz'] || {}
initialState = validator.getInitialState({
selectedCount: memoryQuizConfig.selectedCount || 5,
displayTime: memoryQuizConfig.displayTime || 2.0,
selectedDifficulty: memoryQuizConfig.selectedDifficulty || "easy",
playMode: memoryQuizConfig.playMode || "cooperative",
});
selectedDifficulty: memoryQuizConfig.selectedDifficulty || 'easy',
playMode: memoryQuizConfig.playMode || 'cooperative',
})
// AFTER (type-safe, concise)
const config = getGameConfig(room.gameConfig, "memory-quiz");
initialState = validator.getInitialState(config);
const config = getGameConfig(room.gameConfig, 'memory-quiz')
initialState = validator.getInitialState(config)
```
**Usage in RoomMemoryQuizProvider.tsx:**
```typescript
// BEFORE (verbose, error-prone)
const mergedInitialState = useMemo(() => {
const gameConfig = roomData?.gameConfig as Record<string, any>;
const savedConfig = gameConfig?.["memory-quiz"];
const gameConfig = roomData?.gameConfig as Record<string, any>
const savedConfig = gameConfig?.['memory-quiz']
return {
...initialState,
selectedCount: savedConfig?.selectedCount ?? initialState.selectedCount,
displayTime: savedConfig?.displayTime ?? initialState.displayTime,
selectedDifficulty:
savedConfig?.selectedDifficulty ?? initialState.selectedDifficulty,
selectedDifficulty: savedConfig?.selectedDifficulty ?? initialState.selectedDifficulty,
playMode: savedConfig?.playMode ?? initialState.playMode,
};
}, [roomData?.gameConfig]);
}
}, [roomData?.gameConfig])
// AFTER (type-safe, concise)
const mergedInitialState = useMemo(() => {
const config = getGameConfig(roomData?.gameConfig, "memory-quiz");
const config = getGameConfig(roomData?.gameConfig, 'memory-quiz')
return {
...initialState,
...config, // Spread config directly - all settings included
};
}, [roomData?.gameConfig]);
...config, // Spread config directly - all settings included
}
}, [roomData?.gameConfig])
```
**Benefits:**
- No more manual property-by-property merging
- Type-safe
- Defaults handled automatically
@@ -209,7 +192,7 @@ const mergedInitialState = useMemo(() => {
```typescript
// src/lib/arcade/validation/MemoryQuizGameValidator.ts
import type { MemoryQuizGameConfig } from "@/lib/arcade/game-configs";
import type { MemoryQuizGameConfig } from '@/lib/arcade/game-configs'
export class MemoryQuizGameValidator {
// BEFORE: Manual type definition
@@ -227,15 +210,14 @@ export class MemoryQuizGameValidator {
selectedCount: config.selectedCount,
displayTime: config.displayTime,
selectedDifficulty: config.selectedDifficulty,
playMode: config.playMode, // TypeScript ensures all fields are handled
playMode: config.playMode, // TypeScript ensures all fields are handled
// ...
};
}
}
}
```
**Benefits:**
- If you add a setting to `MemoryQuizGameConfig`, TypeScript forces you to handle it
- Impossible to forget a setting
- Impossible to use wrong type
@@ -281,46 +263,43 @@ If you add a new field to `MemoryQuizGameConfig`, TypeScript will error on `_exh
export function validateGameConfig(
gameName: GameName,
config: any,
config: any
): config is MatchingGameConfig | MemoryQuizGameConfig {
switch (gameName) {
case "matching":
case 'matching':
return (
typeof config.gameType === "string" &&
["abacus-numeral", "complement-pairs"].includes(config.gameType) &&
typeof config.difficulty === "number" &&
typeof config.gameType === 'string' &&
['abacus-numeral', 'complement-pairs'].includes(config.gameType) &&
typeof config.difficulty === 'number' &&
config.difficulty > 0 &&
typeof config.turnTimer === "number" &&
typeof config.turnTimer === 'number' &&
config.turnTimer > 0
);
)
case "memory-quiz":
case 'memory-quiz':
return (
[2, 5, 8, 12, 15].includes(config.selectedCount) &&
typeof config.displayTime === "number" &&
typeof config.displayTime === 'number' &&
config.displayTime > 0 &&
["beginner", "easy", "medium", "hard", "expert"].includes(
config.selectedDifficulty,
) &&
["cooperative", "competitive"].includes(config.playMode)
);
['beginner', 'easy', 'medium', 'hard', 'expert'].includes(config.selectedDifficulty) &&
['cooperative', 'competitive'].includes(config.playMode)
)
default:
return false;
return false
}
}
```
Use in settings API:
```typescript
// src/app/api/arcade/rooms/[roomId]/settings/route.ts
if (body.gameConfig !== undefined) {
if (!validateGameConfig(room.gameName, body.gameConfig[room.gameName])) {
return NextResponse.json({ error: "Invalid game config" }, { status: 400 });
return NextResponse.json({ error: 'Invalid game config' }, { status: 400 })
}
updateData.gameConfig = body.gameConfig;
updateData.gameConfig = body.gameConfig
}
```
@@ -338,7 +317,6 @@ All game configs are stored in a single JSON column in `arcade_rooms.gameConfig`
```
**Issues:**
- No schema validation
- Inefficient updates (read/parse/modify/serialize entire blob)
- Grows without bounds as more games added
@@ -353,37 +331,27 @@ Create `room_game_configs` table with one row per game per room:
```typescript
// src/db/schema/room-game-configs.ts
export const roomGameConfigs = sqliteTable(
"room_game_configs",
{
id: text("id")
.primaryKey()
.$defaultFn(() => createId()),
roomId: text("room_id")
.notNull()
.references(() => arcadeRooms.id, { onDelete: "cascade" }),
gameName: text("game_name", {
enum: ["matching", "memory-quiz", "complement-race"],
}).notNull(),
config: text("config", { mode: "json" }).notNull(), // Game-specific JSON
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
},
(table) => ({
uniqueRoomGame: uniqueIndex("room_game_idx").on(
table.roomId,
table.gameName,
),
}),
);
export const roomGameConfigs = sqliteTable('room_game_configs', {
id: text('id').primaryKey().$defaultFn(() => createId()),
roomId: text('room_id')
.notNull()
.references(() => arcadeRooms.id, { onDelete: 'cascade' }),
gameName: text('game_name', {
enum: ['matching', 'memory-quiz', 'complement-race'],
}).notNull(),
config: text('config', { mode: 'json' }).notNull(), // Game-specific JSON
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
}, (table) => ({
uniqueRoomGame: uniqueIndex('room_game_idx').on(table.roomId, table.gameName),
}))
```
**Benefits:**
- ✅ Smaller rows (only configs for games that have been used)
- ✅ Easier updates (single row, not entire JSON blob)
- ✅ Can track updatedAt per game
@@ -391,7 +359,6 @@ export const roomGameConfigs = sqliteTable(
- ✅ Foundation for future audit trail
**Migration Strategy:**
1. Create new table
2. Migrate existing data from `arcade_rooms.gameConfig`
3. Update all config read/write code
@@ -403,7 +370,6 @@ See migration SQL below.
## Implementation Priority
### Phase 1: Schema Migration (HIGHEST PRIORITY)
1. **Create new table** - Add `room_game_configs` schema
2. **Create migration** - SQL to migrate existing data
3. **Update helper functions** - Adapt to new table structure
@@ -412,18 +378,15 @@ See migration SQL below.
6. **Drop old column** - Remove `gameConfig` from `arcade_rooms`
### Phase 2: Type Safety (HIGH)
1. **Create shared config types** (`game-configs.ts`) - Prevents type mismatches
2. **Create helper functions** (`game-config-helpers.ts`) - Now queries new table
3. **Update validators** to use shared types - Enforces consistency
### Phase 3: Compile-Time Safety (MEDIUM)
1. **Add exhaustiveness checking** - Catches missing fields at compile time
2. **Enforce validator config types** - Use shared types
### Phase 4: Runtime Safety (LOW)
1. **Add runtime validation** - Prevents invalid data from being saved
## Detailed Migration SQL
@@ -476,38 +439,32 @@ WHERE json_extract(game_config, '$."memory-quiz"') IS NOT NULL;
### Step-by-Step with Checkpoints
**Checkpoint 1: Schema & Migration**
1. Create `src/db/schema/room-game-configs.ts`
2. Export from `src/db/schema/index.ts`
3. Generate and apply migration
4. Verify data migrated correctly
**Checkpoint 2: Helper Functions**
1. Create shared config types in `src/lib/arcade/game-configs.ts`
2. Create helper functions in `src/lib/arcade/game-config-helpers.ts`
3. Add unit tests for helpers
**Checkpoint 3: Update Config Reads**
1. Update socket-server.ts to read from new table
2. Update RoomMemoryQuizProvider to read from new table
3. Update RoomMemoryPairsProvider to read from new table
4. Test: Load room and verify settings appear
**Checkpoint 4: Update Config Writes**
1. Update useRoomData.ts updateGameConfig to write to new table
2. Update settings API to write to new table
3. Test: Change settings and verify they persist
**Checkpoint 5: Update Validators**
1. Update validators to use shared config types
2. Test: All games work correctly
**Checkpoint 6: Cleanup**
1. Remove old gameConfig column references
2. Drop gameConfig column from arcade_rooms table
3. Final testing of all games

View File

@@ -1,623 +0,0 @@
# Cross-Game Stats Analysis & Universal Data Model
## Overview
This document analyzes ALL arcade games to ensure our `GameResult` type works universally.
## Games Analyzed
1.**Matching** (Memory Pairs)
2.**Complement Race** (Math race game)
3.**Memory Quiz** (Number memory game)
4.**Card Sorting** (Sort abacus cards)
5.**Rithmomachia** (Strategic board game)
6. 🔍 **YJS Demo** (Multiplayer demo - skipping for now)
---
## Per-Game Analysis
### 1. Matching (Memory Pairs)
**Game Type**: Memory/Pattern Matching
**Players**: 1-N (competitive multiplayer)
**How to Win**: Most pairs matched (multiplayer) OR complete all pairs (solo)
**Data Tracked**:
```typescript
{
scores: { [playerId]: matchCount }
moves: number
matchedPairs: number
totalPairs: number
gameTime: milliseconds
accuracy: percentage (matchedPairs / moves * 100)
grade: 'A+' | 'A' | 'B+' | ...
starRating: 1-5
}
```
**Winner Determination**:
- Solo: completed = won
- Multiplayer: highest score wins
**Fits GameResult?**
```typescript
{
gameType: 'matching',
duration: gameTime,
playerResults: [{
playerId,
won: isWinner,
score: matchCount,
accuracy: 0.0-1.0,
metrics: { moves, matchedPairs, difficulty }
}]
}
```
---
### 2. Complement Race
**Game Type**: Racing/Quiz hybrid
**Players**: 1-N (competitive race)
**How to Win**: Highest score OR reach finish line first (depending on mode)
**Data Tracked**:
```typescript
{
players: {
[playerId]: {
score: number
streak: number
bestStreak: number
correctAnswers: number
totalQuestions: number
position: 0-100% (for practice/survival)
deliveredPassengers: number (sprint mode)
}
}
gameTime: milliseconds
winner: playerId | null
leaderboard: [{ playerId, score, rank }]
}
```
**Winner Determination**:
- Practice/Survival: reach 100% position
- Sprint: highest score (delivered passengers)
**Fits GameResult?**
```typescript
{
gameType: 'complement-race',
duration: gameTime,
playerResults: [{
playerId,
won: winnerId === playerId,
score: player.score,
accuracy: player.correctAnswers / player.totalQuestions,
placement: leaderboard rank,
metrics: {
streak: player.bestStreak,
correctAnswers: player.correctAnswers,
totalQuestions: player.totalQuestions
}
}]
}
```
---
### 3. Memory Quiz
**Game Type**: Memory/Recall
**Players**: 1-N (cooperative OR competitive)
**How to Win**:
- Cooperative: team finds all numbers
- Competitive: most correct answers
**Data Tracked**:
```typescript
{
playerScores: {
[playerId]: { correct: number, incorrect: number }
}
foundNumbers: number[]
correctAnswers: number[]
selectedCount: 2 | 5 | 8 | 12 | 15
playMode: 'cooperative' | 'competitive'
gameTime: milliseconds
}
```
**Winner Determination**:
- Cooperative: ALL found = team wins
- Competitive: highest correct count wins
**Fits GameResult?****BUT needs special handling for cooperative**
```typescript
{
gameType: 'memory-quiz',
duration: gameTime,
playerResults: [{
playerId,
won: playMode === 'cooperative'
? foundAll // All players win or lose together
: hasHighestScore, // Individual winner
score: playerScores[playerId].correct,
accuracy: correct / (correct + incorrect),
metrics: {
correct: playerScores[playerId].correct,
incorrect: playerScores[playerId].incorrect,
difficulty: selectedCount
}
}],
metadata: {
playMode: 'cooperative' | 'competitive',
isTeamVictory: boolean // ← IMPORTANT for cooperative games
}
}
```
**NEW INSIGHT**: Cooperative games need special handling - all players share win/loss!
---
### 4. Card Sorting
**Game Type**: Sorting/Puzzle
**Players**: 1-N (solo, collaborative, competitive, relay)
**How to Win**:
- Solo: achieve high score (0-100)
- Collaborative: team achieves score
- Competitive: highest individual score
- Relay: TBD (not fully implemented)
**Data Tracked**:
```typescript
{
scoreBreakdown: {
finalScore: 0 - 100;
exactMatches: number;
lcsLength: number; // Longest common subsequence
inversions: number; // Out-of-order pairs
relativeOrderScore: 0 - 100;
exactPositionScore: 0 - 100;
inversionScore: 0 - 100;
elapsedTime: seconds;
}
gameMode: "solo" | "collaborative" | "competitive" | "relay";
}
```
**Winner Determination**:
- Solo/Collaborative: score > threshold (e.g., 70+)
- Competitive: highest score
**Fits GameResult?****Similar to Memory Quiz**
```typescript
{
gameType: 'card-sorting',
duration: elapsedTime * 1000,
playerResults: [{
playerId,
won: gameMode === 'collaborative'
? scoreBreakdown.finalScore >= 70 // Team threshold
: hasHighestScore,
score: scoreBreakdown.finalScore,
accuracy: scoreBreakdown.exactMatches / cardCount,
metrics: {
exactMatches: scoreBreakdown.exactMatches,
inversions: scoreBreakdown.inversions,
lcsLength: scoreBreakdown.lcsLength
}
}],
metadata: {
gameMode,
isTeamVictory: gameMode === 'collaborative'
}
}
```
---
### 5. Rithmomachia
**Game Type**: Strategic board game (2-player only)
**Players**: Exactly 2 (White vs Black)
**How to Win**: Multiple victory conditions (harmony, points, exhaustion, resignation)
**Data Tracked**:
```typescript
{
winner: 'W' | 'B' | null
winCondition: 'HARMONY' | 'EXHAUSTION' | 'RESIGNATION' | 'POINTS' | ...
capturedPieces: { W: Piece[], B: Piece[] }
pointsCaptured: { W: number, B: number }
history: MoveRecord[]
gameTime: milliseconds (computed from history)
}
```
**Winner Determination**:
- Specific win condition triggered
- No draws (or rare)
**Fits GameResult?****Needs win condition metadata**
```typescript
{
gameType: 'rithmomachia',
duration: gameTime,
playerResults: [
{
playerId: whitePlayerId,
won: winner === 'W',
score: capturedPieces.W.length, // or pointsCaptured.W
metrics: {
capturedPieces: capturedPieces.W.length,
points: pointsCaptured?.W || 0,
moves: history.filter(m => m.color === 'W').length
}
},
{
playerId: blackPlayerId,
won: winner === 'B',
score: capturedPieces.B.length,
metrics: {
capturedPieces: capturedPieces.B.length,
points: pointsCaptured?.B || 0,
moves: history.filter(m => m.color === 'B').length
}
}
],
metadata: {
winCondition: 'HARMONY' | 'POINTS' | ...
}
}
```
---
## Cross-Game Patterns Identified
### Pattern 1: Competitive (Most Common)
**Games**: Matching (multiplayer), Complement Race, Memory Quiz (competitive), Card Sorting (competitive)
**Characteristics**:
- Each player has their own score
- Winner = highest score
- Players track individually
**Stats to track per player**:
- games_played ++
- wins ++ (if winner)
- losses ++ (if not winner)
- best_time (if faster)
- highest_accuracy (if better)
---
### Pattern 2: Cooperative (Team-Based)
**Games**: Memory Quiz (cooperative), Card Sorting (collaborative)
**Characteristics**:
- All players share outcome
- Team wins or loses together
- Individual contributions still tracked
**Stats to track per player**:
- games_played ++
- wins ++ (if TEAM won) ← Key difference
- losses ++ (if TEAM lost)
- Individual metrics still tracked (correct answers, etc.)
**CRITICAL**: Check `metadata.isTeamVictory` to determine if all players get same win/loss!
---
### Pattern 3: Head-to-Head (Exactly 2 Players)
**Games**: Rithmomachia
**Characteristics**:
- Always 2 players
- One wins, one loses (rare draws)
- Different win conditions
**Stats to track per player**:
- games_played ++
- wins ++ (winner only)
- losses ++ (loser only)
- Game-specific metrics (captures, harmonies)
---
### Pattern 4: Solo Completion
**Games**: Matching (solo), Complement Race (practice), Memory Quiz (solo), Card Sorting (solo)
**Characteristics**:
- Single player
- Win = completion or threshold
- Compete against self/time
**Stats to track**:
- games_played ++
- wins ++ (if completed/threshold met)
- losses ++ (if failed/gave up)
- best_time, highest_accuracy
---
## Refined Universal Data Model
### GameResult Type (UPDATED)
```typescript
export interface GameResult {
// Game identification
gameType: string; // e.g., "matching", "complement-race", etc.
// Player results (supports 1-N players)
playerResults: PlayerGameResult[];
// Timing
completedAt: number; // timestamp
duration: number; // milliseconds
// Optional game-specific data
metadata?: {
// For cooperative games
isTeamVictory?: boolean; // ← NEW: all players share win/loss
// For specific win conditions
winCondition?: string; // e.g., "HARMONY", "POINTS", "TIMEOUT"
// For game modes
gameMode?: string; // e.g., "solo", "competitive", "cooperative"
// Any other game-specific info
[key: string]: unknown;
};
}
export interface PlayerGameResult {
playerId: string;
// Outcome
won: boolean; // For cooperative: all players same value
placement?: number; // 1st, 2nd, 3rd (for competitive with >2 players)
// Performance
score?: number;
accuracy?: number; // 0.0 - 1.0
completionTime?: number; // milliseconds (player-specific time)
// Game-specific metrics (optional, stored as JSON in DB)
metrics?: {
// Matching
moves?: number;
matchedPairs?: number;
difficulty?: number;
// Complement Race
streak?: number;
correctAnswers?: number;
totalQuestions?: number;
// Memory Quiz
correct?: number;
incorrect?: number;
// Card Sorting
exactMatches?: number;
inversions?: number;
lcsLength?: number;
// Rithmomachia
capturedPieces?: number;
points?: number;
// Extensible for future games
[key: string]: unknown;
};
}
```
---
## Stats Recording Logic (UPDATED)
### For Each Player in GameResult
```typescript
// Fetch player stats
const stats = await getPlayerStats(playerId);
// Always increment
stats.gamesPlayed++;
// Handle wins/losses based on game type
if (gameResult.metadata?.isTeamVictory !== undefined) {
// COOPERATIVE: All players share outcome
if (playerResult.won) {
stats.totalWins++;
} else {
stats.totalLosses++;
}
} else {
// COMPETITIVE/SOLO: Individual outcome
if (playerResult.won) {
stats.totalWins++;
} else {
stats.totalLosses++;
}
}
// Update performance metrics
if (
playerResult.completionTime &&
(!stats.bestTime || playerResult.completionTime < stats.bestTime)
) {
stats.bestTime = playerResult.completionTime;
}
if (playerResult.accuracy && playerResult.accuracy > stats.highestAccuracy) {
stats.highestAccuracy = playerResult.accuracy;
}
// Update per-game stats (JSON)
stats.gameStats[gameResult.gameType] = {
gamesPlayed: (stats.gameStats[gameResult.gameType]?.gamesPlayed || 0) + 1,
wins:
(stats.gameStats[gameResult.gameType]?.wins || 0) +
(playerResult.won ? 1 : 0),
// ... other game-specific aggregates
};
// Update favorite game type (most played)
stats.favoriteGameType = getMostPlayedGame(stats.gameStats);
// Update timestamps
stats.lastPlayedAt = gameResult.completedAt;
stats.updatedAt = Date.now();
```
---
## Database Schema (CONFIRMED)
No changes needed from original design! The `metrics` JSON field handles game-specific data perfectly.
```sql
CREATE TABLE player_stats (
player_id TEXT PRIMARY KEY,
-- Aggregates
games_played INTEGER NOT NULL DEFAULT 0,
total_wins INTEGER NOT NULL DEFAULT 0,
total_losses INTEGER NOT NULL DEFAULT 0,
-- Performance
best_time INTEGER,
highest_accuracy REAL NOT NULL DEFAULT 0,
-- Per-game breakdown (JSON)
game_stats TEXT NOT NULL DEFAULT '{}',
-- Meta
favorite_game_type TEXT,
last_played_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
---
## Key Insights & Design Decisions
### 1. Cooperative Games Need Special Flag
**Problem**: Memory Quiz (cooperative) and Card Sorting (collaborative) - all players share win/loss.
**Solution**: Add `metadata.isTeamVictory: boolean` to `GameResult`. When `true`, recording logic gives ALL players the same win/loss.
### 2. Flexible Metrics Field
**Problem**: Each game tracks different metrics (moves, streak, inversions, etc.).
**Solution**: `PlayerGameResult.metrics` is an open object. Store game-specific data here, saved as JSON in DB.
### 3. Placement for Tournaments
**Problem**: 3+ player games need to track ranking (1st, 2nd, 3rd).
**Solution**: `PlayerGameResult.placement` field. Useful for leaderboards.
### 4. Win Conditions Matter
**Problem**: Rithmomachia has multiple win conditions (harmony, points, etc.).
**Solution**: `metadata.winCondition` stores how the game was won. Useful for achievements/stats breakdown.
### 5. Score is Optional
**Problem**: Not all games have scores (e.g., Rithmomachia can win by harmony without points enabled).
**Solution**: Make `score` optional. Use `won` as primary outcome indicator.
---
## Testing Matrix
### Scenarios to Test
| Game | Mode | Players | Expected Outcome |
| --------------- | ------------- | ------- | ----------------------------------- |
| Matching | Solo | 1 | Player wins if completed |
| Matching | Competitive | 2+ | Winner = highest score, others lose |
| Complement Race | Sprint | 2+ | Winner = highest score |
| Memory Quiz | Cooperative | 2+ | ALL win or ALL lose (team) |
| Memory Quiz | Competitive | 2+ | Winner = most correct |
| Card Sorting | Solo | 1 | Win if score >= 70 |
| Card Sorting | Collaborative | 2+ | ALL win or ALL lose (team) |
| Card Sorting | Competitive | 2+ | Winner = highest score |
| Rithmomachia | PvP | 2 | One wins (by condition), one loses |
---
## Conclusion
**Universal `GameResult` type CONFIRMED to work for all games**
**Key Requirements**:
1. Support 1-N players (flexible array)
2. Support cooperative games (isTeamVictory flag)
3. Support game-specific metrics (open metrics object)
4. Support multiple win conditions (winCondition metadata)
5. Track both individual AND team performance
**Next Steps**:
1. Update `.claude/PER_PLAYER_STATS_ARCHITECTURE.md` with refined types
2. Implement database schema
3. Build API endpoints
4. Create React hooks
5. Integrate with each game (starting with Matching)
---
**Status**: ✅ Complete cross-game analysis
**Result**: GameResult type is universal and robust
**Date**: 2025-01-03

View File

@@ -3,7 +3,6 @@
## Problem
Previously, each game manually specified `color`, `gradient`, and `borderColor` in their manifest. This led to:
- Inconsistent appearance across game cards
- No guidance on what colors/gradients to use
- Easy to choose saturated colors that don't match the pastel style
@@ -20,29 +19,28 @@ All games now use predefined color themes that ensure consistent, professional a
### 1. Import from the Game SDK
```typescript
import { defineGame, getGameTheme } from "@/lib/arcade/game-sdk";
import type { GameManifest } from "@/lib/arcade/game-sdk";
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
```
### 2. Use the Theme Spread Operator
```typescript
const manifest: GameManifest = {
name: "my-game",
displayName: "My Awesome Game",
icon: "🎮",
description: "A fun game",
longDescription: "More details...",
name: 'my-game',
displayName: 'My Awesome Game',
icon: '🎮',
description: 'A fun game',
longDescription: 'More details...',
maxPlayers: 4,
difficulty: "Intermediate",
chips: ["🎯 Feature 1", "⚡ Feature 2"],
...getGameTheme("blue"), // ← Just add this!
difficulty: 'Intermediate',
chips: ['🎯 Feature 1', '⚡ Feature 2'],
...getGameTheme('blue'), // ← Just add this!
available: true,
};
}
```
That's it! The theme automatically provides:
- `color: 'blue'`
- `gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)'`
- `borderColor: 'blue.200'`
@@ -51,18 +49,18 @@ That's it! The theme automatically provides:
All themes use Panda CSS's 100-200 color range for soft pastel appearance:
| Theme | Color Range | Use Case |
| -------- | ------------------------ | ------------------------- |
| `blue` | blue-100 to blue-200 | Memory, puzzle games |
| `purple` | purple-100 to purple-200 | Strategic, battle games |
| `green` | green-100 to green-200 | Growth, achievement games |
| `teal` | teal-100 to teal-200 | Creative, sorting games |
| `indigo` | indigo-100 to indigo-200 | Deep thinking games |
| `pink` | pink-100 to pink-200 | Fun, casual games |
| `orange` | orange-100 to orange-200 | Speed, energy games |
| `yellow` | yellow-100 to yellow-200 | Bright, happy games |
| `red` | red-100 to red-200 | Competition, challenge |
| `gray` | gray-100 to gray-200 | Neutral games |
| Theme | Color Range | Use Case |
|-------|-------------|----------|
| `blue` | blue-100 to blue-200 | Memory, puzzle games |
| `purple` | purple-100 to purple-200 | Strategic, battle games |
| `green` | green-100 to green-200 | Growth, achievement games |
| `teal` | teal-100 to teal-200 | Creative, sorting games |
| `indigo` | indigo-100 to indigo-200 | Deep thinking games |
| `pink` | pink-100 to pink-200 | Fun, casual games |
| `orange` | orange-100 to orange-200 | Speed, energy games |
| `yellow` | yellow-100 to yellow-200 | Bright, happy games |
| `red` | red-100 to red-200 | Competition, challenge |
| `gray` | gray-100 to gray-200 | Neutral games |
## Examples
@@ -95,11 +93,11 @@ All themes use Panda CSS's 100-200 color range for soft pastel appearance:
If you need to inspect or customize a theme:
```typescript
import { GAME_THEMES } from "@/lib/arcade/game-sdk";
import type { GameTheme } from "@/lib/arcade/game-sdk";
import { GAME_THEMES } from '@/lib/arcade/game-sdk'
import type { GameTheme } from '@/lib/arcade/game-sdk'
// Access a specific theme
const blueTheme: GameTheme = GAME_THEMES.blue;
const blueTheme: GameTheme = GAME_THEMES.blue
// Use it
const manifest: GameManifest = {
@@ -107,9 +105,9 @@ const manifest: GameManifest = {
...blueTheme,
// Or customize:
color: blueTheme.color,
gradient: "linear-gradient(135deg, #custom, #values)", // override
gradient: 'linear-gradient(135deg, #custom, #values)', // override
borderColor: blueTheme.borderColor,
};
}
```
## Adding New Themes
@@ -120,17 +118,16 @@ To add a new theme, edit `/src/lib/arcade/game-themes.ts`:
export const GAME_THEMES = {
// ... existing themes
mycolor: {
color: "mycolor",
gradient: "linear-gradient(135deg, #lighter, #darker)", // Use Panda CSS 100-200 range
borderColor: "mycolor.200",
color: 'mycolor',
gradient: 'linear-gradient(135deg, #lighter, #darker)', // Use Panda CSS 100-200 range
borderColor: 'mycolor.200',
},
} as const satisfies Record<string, GameTheme>;
} as const satisfies Record<string, GameTheme>
```
Then update the TypeScript type:
```typescript
export type GameThemeName = keyof typeof GAME_THEMES;
export type GameThemeName = keyof typeof GAME_THEMES
```
## Migration Checklist
@@ -145,7 +142,6 @@ When creating a new game:
## Summary
**Old way** (error-prone, inconsistent):
```typescript
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)', // Too saturated!
@@ -153,7 +149,6 @@ borderColor: 'teal.200',
```
**New way** (simple, consistent):
```typescript
...getGameTheme('teal')
```

View File

@@ -1,504 +0,0 @@
# Google Classroom Integration Setup Guide
**Goal:** Set up Google Classroom API integration using mostly CLI commands, minimizing web console interaction.
**Time Required:** 15-20 minutes
**Cost:** $0 (free for educational use)
---
## Prerequisites
**gcloud CLI installed** (already installed at `/opt/homebrew/bin/gcloud`)
**Valid Google account**
- **Billing account** (required by Google, but FREE for Classroom API)
---
## Quick Start (TL;DR)
```bash
# Run the automated setup script
./scripts/setup-google-classroom.sh
```
The script will:
1. Authenticate with your Google account
2. Create a GCP project
3. Enable Classroom & People APIs
4. Guide you through OAuth setup (2 web console steps)
5. Configure your `.env.local` file
**Note:** Steps 6 & 7 still require web console (Google doesn't provide CLI for OAuth consent screen), but the script opens the pages for you and provides exact instructions.
---
## What the Script Does (Step by Step)
### 1. Authentication ✅ Fully Automated
```bash
gcloud auth login
```
Opens browser, you log in with Google, done.
### 2. Create GCP Project ✅ Fully Automated
```bash
PROJECT_ID="soroban-abacus-$(date +%s)" # Unique ID with timestamp
gcloud projects create "$PROJECT_ID" --name="Soroban Abacus Flashcards"
gcloud config set project "$PROJECT_ID"
```
### 3. Link Billing Account ✅ Mostly Automated
```bash
# List your billing accounts
gcloud billing accounts list
# Link to project
gcloud billing projects link "$PROJECT_ID" --billing-account="BILLING_ACCOUNT_ID"
```
**Why billing is required:**
- Google requires billing for API access (even free APIs!)
- Classroom API is **FREE** with no usage charges
- You won't be charged unless you enable paid services
**If you don't have a billing account:**
- Script will prompt you to create one at: https://console.cloud.google.com/billing
- It's quick: just add payment method (won't be charged)
- Press Enter in terminal after creation
### 4. Enable APIs ✅ Fully Automated
```bash
gcloud services enable classroom.googleapis.com
gcloud services enable people.googleapis.com
```
Takes 1-2 minutes to propagate.
### 5. Create OAuth Credentials ⚠️ Requires Web Console
**Why CLI doesn't work:**
Google doesn't provide `gcloud` commands for creating OAuth clients. You need the web console.
**What the script does:**
- Opens: https://console.cloud.google.com/apis/credentials?project=YOUR_PROJECT
- Provides exact instructions (copy-paste ready)
**Manual steps (takes 2 minutes):**
1. Click "**Create Credentials**" → "**OAuth client ID**"
2. Application type: **"Web application"**
3. Name: **"Soroban Abacus Web"**
4. **Authorized JavaScript origins:**
```
http://localhost:3000
https://abaci.one
```
5. **Authorized redirect URIs:**
```
http://localhost:3000/api/auth/callback/google
https://abaci.one/api/auth/callback/google
```
6. Click **"Create"**
7. **Copy** the Client ID and Client Secret (you'll paste into terminal)
### 6. Configure OAuth Consent Screen ⚠️ Requires Web Console
**Why CLI doesn't work:**
OAuth consent screen configuration is web-only.
**What the script does:**
- Opens: https://console.cloud.google.com/apis/credentials/consent?project=YOUR_PROJECT
- Provides step-by-step instructions
**Manual steps (takes 3 minutes):**
**Screen 1: OAuth consent screen**
- User Type: **"External"** (unless you have Google Workspace)
- Click "**Create**"
**Screen 2: App information**
- App name: **"Soroban Abacus Flashcards"**
- User support email: **Your email**
- App logo: (optional)
- App domain: (optional, can add later)
- Developer contact: **Your email**
- Click "**Save and Continue**"
**Screen 3: Scopes**
- Click "**Add or Remove Scopes**"
- Filter/search for these scopes and check them:
- ✅ `.../auth/userinfo.email` (See your primary Google Account email)
- ✅ `.../auth/userinfo.profile` (See your personal info)
- ✅ `.../auth/classroom.courses.readonly` (View courses)
- ✅ `.../auth/classroom.rosters.readonly` (View class rosters)
- Click "**Update**"
- Click "**Save and Continue**"
**Screen 4: Test users**
- Click "**Add Users**"
- Add your email address (for testing)
- Click "**Save and Continue**"
**Screen 5: Summary**
- Review and click "**Back to Dashboard**"
Done! ✅
### 7. Save Credentials to .env.local ✅ Fully Automated
Script prompts you for:
- Client ID (paste from step 5)
- Client Secret (paste from step 5)
Then automatically adds to `.env.local`:
```bash
# Google OAuth (Generated by setup-google-classroom.sh)
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="GOCSPX-your-secret"
```
---
## After Running the Script
### Verify Setup
```bash
# Check project configuration
gcloud config get-value project
# List enabled APIs
gcloud services list --enabled
# Check Classroom API is enabled
gcloud services list --enabled | grep classroom
```
Expected output:
```
classroom.googleapis.com Google Classroom API
```
### Test API Access
```bash
# Get an access token
gcloud auth application-default login
gcloud auth application-default print-access-token
# Test Classroom API (replace TOKEN)
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://classroom.googleapis.com/v1/courses
```
Expected response (if you have no courses yet):
```json
{}
```
---
## NextAuth Configuration
Now that you have credentials, add Google provider to NextAuth:
### 1. Check Current NextAuth Config
```bash
cat src/app/api/auth/[...nextauth]/route.ts
```
### 2. Add Google Provider
Add to your NextAuth providers array:
```typescript
import GoogleProvider from "next-auth/providers/google";
export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
scope: [
"openid",
"email",
"profile",
"https://www.googleapis.com/auth/classroom.courses.readonly",
"https://www.googleapis.com/auth/classroom.rosters.readonly",
].join(" "),
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
// ... your existing providers
],
// ... rest of config
};
```
### 3. Test Login
```bash
# Start dev server
npm run dev
# Open browser
open http://localhost:3000
```
Click "Sign in with Google" and verify:
- ✅ OAuth consent screen appears
- ✅ Shows requested permissions
- ✅ Successfully logs in
- ✅ User profile is created
---
## Troubleshooting
### "Billing account required"
**Problem:** Can't enable APIs without billing
**Solution:** Create billing account at https://console.cloud.google.com/billing
- Won't be charged for Classroom API (it's free)
- Just need payment method on file
### "Error 401: deleted_client"
**Problem:** OAuth client was deleted or not created properly
**Solution:** Re-run OAuth client creation (step 5)
```bash
open "https://console.cloud.google.com/apis/credentials?project=$(gcloud config get-value project)"
```
### "Error 403: Access Not Configured"
**Problem:** APIs not enabled yet (takes 1-2 min to propagate)
**Solution:** Wait 2 minutes, then verify:
```bash
gcloud services list --enabled | grep classroom
```
### "Invalid redirect URI"
**Problem:** Redirect URI doesn't match OAuth client config
**Solution:** Check that these URIs are in your OAuth client:
- http://localhost:3000/api/auth/callback/google
- https://abaci.one/api/auth/callback/google
### "App is not verified"
**Problem:** OAuth consent screen in "Testing" mode
**Solution:** This is **normal** for development!
- Click "Advanced" → "Go to [app name] (unsafe)"
- Only affects external test users
- For production, submit for verification (takes 1-2 weeks)
---
## CLI Reference
### Project Management
```bash
# List all your projects
gcloud projects list
# Switch project
gcloud config set project PROJECT_ID
# Delete project (if needed)
gcloud projects delete PROJECT_ID
```
### API Management
```bash
# List enabled APIs
gcloud services list --enabled
# Enable an API
gcloud services enable APINAME.googleapis.com
# Disable an API
gcloud services disable APINAME.googleapis.com
# Check quota
gcloud services quota describe classroom.googleapis.com
```
### OAuth Management
```bash
# List OAuth clients (requires REST API)
PROJECT_ID=$(gcloud config get-value project)
ACCESS_TOKEN=$(gcloud auth application-default print-access-token)
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://oauth2.googleapis.com/v1/projects/$PROJECT_ID/oauthClients"
```
### Billing
```bash
# List billing accounts
gcloud billing accounts list
# Link billing to project
gcloud billing projects link PROJECT_ID --billing-account=ACCOUNT_ID
# Check project billing status
gcloud billing projects describe PROJECT_ID
```
---
## What You Can Do From CLI (Summary)
✅ **Fully Automated:**
- Authenticate with Google
- Create GCP project
- Enable APIs
- Link billing account
- Configure environment variables
⚠️ **Requires Web Console (2-5 minutes):**
- Create OAuth client (2 min)
- Configure OAuth consent screen (3 min)
**Why web console required:**
Google doesn't provide CLI for these security-sensitive operations. But the script:
- Opens the exact pages for you
- Provides step-by-step instructions
- Makes it as painless as possible
---
## Cost Breakdown
| Item | Cost |
| --------------------------- | ----------------- |
| GCP project | $0 |
| Google Classroom API | $0 (free forever) |
| Google People API | $0 (free forever) |
| Billing account requirement | $0 (no charges) |
| **Total** | **$0** |
**Note:** You need to add a payment method for billing account, but Google Classroom API is completely free with no usage limits.
---
## Next Steps After Setup
1. ✅ Run the setup script: `./scripts/setup-google-classroom.sh`
2. ✅ Add Google provider to NextAuth
3. ✅ Test "Sign in with Google"
4. 📝 Implement class import feature (Phase 2 of roadmap)
5. 📝 Build teacher dashboard
6. 📝 Add assignment integration
Refer to `.claude/PLATFORM_INTEGRATION_ROADMAP.md` for full implementation timeline.
---
## Security Best Practices
### Protect Your Secrets
```bash
# Check .env.local is in .gitignore
cat .gitignore | grep .env.local
```
Should see:
```
.env*.local
```
### Rotate Credentials Periodically
```bash
# Open credentials page
PROJECT_ID=$(gcloud config get-value project)
open "https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID"
# Delete old client, create new one
# Update .env.local with new credentials
```
### Use Different Credentials for Dev/Prod
**Development:**
- OAuth client: `http://localhost:3000/api/auth/callback/google`
- Test users only
**Production:**
- OAuth client: `https://abaci.one/api/auth/callback/google`
- Verified app (submit for review)
---
## Resources
**Official Documentation:**
- GCP CLI: https://cloud.google.com/sdk/gcloud
- Classroom API: https://developers.google.com/classroom
- OAuth 2.0: https://developers.google.com/identity/protocols/oauth2
**Script Location:**
- `scripts/setup-google-classroom.sh`
**Configuration Files:**
- `.env.local` (credentials)
- `src/app/api/auth/[...nextauth]/route.ts` (NextAuth config)
---
**Ready to run?**
```bash
./scripts/setup-google-classroom.sh
```
Good luck! 🚀

View File

@@ -48,7 +48,6 @@ WHERE game_config IS NOT NULL
```
**Results:**
- 5991 matching game configs migrated
- 9 memory-quiz game configs migrated
- Total: 6000 configs
@@ -56,12 +55,10 @@ WHERE game_config IS NOT NULL
## Old vs New Schema
**Old Schema:**
- `arcade_rooms.game_config` (TEXT/JSON) - stored config for currently selected game only
- Config was lost when switching games
**New Schema:**
- `room_game_configs` table - one row per game per room
- Unique constraint on (room_id, game_name)
- Configs persist when switching between games
@@ -87,7 +84,6 @@ sqlite3 data/sqlite.db "SELECT game_name, COUNT(*) FROM room_game_configs GROUP
## Related Files
This migration supports the refactoring documented in:
- `.claude/GAME_SETTINGS_PERSISTENCE.md` - Architecture documentation
- `src/lib/arcade/game-configs.ts` - Shared config types
- `src/lib/arcade/game-config-helpers.ts` - Database access helpers

View File

@@ -1,299 +0,0 @@
# Matching Game Stats Integration Guide
## Quick Reference
**Files to modify**: `src/arcade-games/matching/components/ResultsPhase.tsx`
**What we're adding**: Call `useRecordGameResult()` when game completes to save per-player stats.
## Current State Analysis
### ResultsPhase.tsx (lines 9-29)
Already has all the data we need:
```typescript
const { state, resetGame, activePlayers, gameMode, exitSession } =
useMatching();
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode();
const gameTime =
state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0;
const analysis = getPerformanceAnalysis(state);
const multiplayerResult =
gameMode === "multiplayer"
? getMultiplayerWinner(state, activePlayers)
: null;
```
**Available data:**
-`state.scores` - scores by player ID
-`state.gameStartTime`, `state.gameEndTime` - timing
-`state.matchedPairs`, `state.totalPairs` - completion
-`state.moves` - total moves
-`activePlayers` - array of player IDs
-`multiplayerResult.winners` - who won
-`analysis.statistics.accuracy` - accuracy percentage
## Implementation Steps
### Step 1: Add state flag to prevent duplicate recording
Add `recorded: boolean` to `MatchingState` type:
```typescript
// src/arcade-games/matching/types.ts (add to MatchingState interface)
export interface MatchingState extends GameState {
// ... existing fields ...
// Stats recording
recorded?: boolean; // ← ADD THIS
}
```
### Step 2: Import the hook in ResultsPhase.tsx
```typescript
// At top of src/arcade-games/matching/components/ResultsPhase.tsx
import { useEffect } from "react"; // ← ADD if not present
import { useRecordGameResult } from "@/hooks/useRecordGameResult";
import type { GameResult } from "@/lib/arcade/stats/types";
```
### Step 3: Call the hook
```typescript
// Inside ResultsPhase component, after existing hooks
export function ResultsPhase() {
const router = useRouter()
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
// ← ADD THIS
const { mutate: recordGame, isPending: isRecording } = useRecordGameResult()
// ... existing code ...
```
### Step 4: Record game result on mount
Add this useEffect after the hook declarations:
```typescript
// Record game result once when entering results phase
useEffect(() => {
// Only record if we haven't already
if (state.phase === "results" && !state.recorded && !isRecording) {
const gameTime =
state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0;
const analysis = getPerformanceAnalysis(state);
const multiplayerResult =
gameMode === "multiplayer"
? getMultiplayerWinner(state, activePlayers)
: null;
// Build GameResult
const gameResult: GameResult = {
gameType:
state.gameType === "abacus-numeral"
? "matching-abacus"
: "matching-complements",
completedAt: state.gameEndTime || Date.now(),
duration: gameTime,
playerResults: activePlayers.map((playerId) => {
const score = state.scores[playerId] || 0;
const won = multiplayerResult
? multiplayerResult.winners.includes(playerId)
: state.matchedPairs === state.totalPairs; // Solo = completed
// In multiplayer, calculate per-player accuracy from their score
// In single player, use overall accuracy
const playerAccuracy =
gameMode === "multiplayer"
? score / state.totalPairs // Their score as fraction of total pairs
: analysis.statistics.accuracy / 100; // Convert percentage to 0-1
return {
playerId,
won,
score,
accuracy: playerAccuracy,
completionTime: gameTime,
metrics: {
moves: state.moves,
matchedPairs: state.matchedPairs,
difficulty: state.difficulty,
},
};
}),
metadata: {
gameType: state.gameType,
difficulty: state.difficulty,
grade: analysis.grade,
starRating: analysis.starRating,
},
};
// Record to database
recordGame(gameResult, {
onSuccess: (updates) => {
console.log("✅ Stats recorded:", updates);
// Mark as recorded to prevent duplicate saves
// Note: This assumes Provider has a way to update state.recorded
// We'll need to add an action for this
},
onError: (error) => {
console.error("❌ Failed to record stats:", error);
},
});
}
}, [state.phase, state.recorded, isRecording /* ... deps */]);
```
### Step 5: Add loading state UI (optional)
Show a subtle loading indicator while recording:
```typescript
// At the top of the return statement in ResultsPhase
if (isRecording) {
return (
<div className={css({
textAlign: 'center',
padding: '20px',
})}>
<p>Saving results...</p>
</div>
)
}
```
Or keep it subtle and just disable buttons:
```typescript
// On the "Play Again" button
<button
disabled={isRecording}
className={css({
// ... styles ...
opacity: isRecording ? 0.5 : 1,
cursor: isRecording ? 'not-allowed' : 'pointer',
})}
onClick={resetGame}
>
{isRecording ? '💾 Saving...' : '🎮 Play Again'}
</button>
```
## Provider Changes Needed
The Provider needs an action to mark the game as recorded:
```typescript
// src/arcade-games/matching/Provider.tsx
// Add to the context type
export interface MatchingContextType {
// ... existing ...
markAsRecorded: () => void; // ← ADD THIS
}
// Add to the reducer or state update logic
const markAsRecorded = useCallback(() => {
setState((prev) => ({ ...prev, recorded: true }));
}, []);
// Add to the context value
const contextValue: MatchingContextType = {
// ... existing ...
markAsRecorded,
};
```
Then in ResultsPhase useEffect:
```typescript
onSuccess: (updates) => {
console.log("✅ Stats recorded:", updates);
markAsRecorded(); // ← Use this instead
};
```
## Testing Checklist
### Solo Game
- [ ] Play a game to completion
- [ ] Check console for "✅ Stats recorded"
- [ ] Refresh page
- [ ] Go to `/games` page
- [ ] Verify player's gamesPlayed incremented
- [ ] Verify player's totalWins incremented (if completed)
### Multiplayer Game
- [ ] Activate 2+ players
- [ ] Play a game to completion
- [ ] Check console for stats for ALL players
- [ ] Go to `/games` page
- [ ] Verify each player's stats updated independently
- [ ] Winner should have +1 win
- [ ] All players should have +1 games played
### Edge Cases
- [ ] Incomplete game (exit early) - should NOT record
- [ ] Play again from results - should NOT duplicate record
- [ ] Network error during save - should show error, not mark as recorded
## Common Issues
### Issue: Stats recorded multiple times
**Cause**: useEffect dependency array missing or incorrect
**Fix**: Ensure `state.recorded` is in deps and checked in condition
### Issue: Can't read property 'id' of undefined
**Cause**: Player not found in playerMap
**Fix**: Add null checks when mapping activePlayers
### Issue: Accuracy is always 100% or 0%
**Cause**: Wrong calculation or unit (percentage vs decimal)
**Fix**: Ensure accuracy is 0.0 - 1.0, not 0-100
### Issue: Single player never "wins"
**Cause**: Wrong win condition for solo mode
**Fix**: Solo player wins if they complete all pairs (`state.matchedPairs === state.totalPairs`)
## Next Steps After Integration
1. ✅ Verify stats save correctly
2. ✅ Update `/games` page to fetch and display per-player stats
3. ✅ Test with different game modes and difficulties
4. 🔄 Repeat this pattern for other arcade games
5. 📊 Add stats visualization/charts (future)
---
**Status**: Ready for implementation
**Blocked by**:
- Database schema (player_stats table)
- API endpoints (/api/player-stats/record-game)
- React hooks (useRecordGameResult)

View File

@@ -10,8 +10,8 @@ Panda CSS's `css()` function requires **static values at build time**. It cannot
```typescript
// ❌ This doesn't work
const color = "blue.400";
css({ color: color }); // Panda can't resolve this at build time
const color = 'blue.400'
css({ color: color }) // Panda can't resolve this at build time
```
The `css()` function performs static analysis during the build process to generate CSS classes. It cannot handle runtime-dynamic token paths.
@@ -40,7 +40,6 @@ const stages = [
1. **Use `as const`**: TypeScript needs the array marked as `const` so the token strings are treated as literal types, not generic strings. The `token()` function expects the `Token` literal type.
2. **Use inline styles**: When using `token()`, apply colors via the `style` prop, not through the `css()` function:
```typescript
// ✅ Correct
<div style={{ color: token(stage.color) }}>
@@ -52,13 +51,12 @@ const stages = [
3. **Static tokens in css()**: For static usage, you CAN use tokens directly in `css()`:
```typescript
// ✅ This works because it's static
css({ color: "blue.400" });
css({ color: 'blue.400' })
```
## How token() Works
The `token()` function:
- Takes a token path like `"colors.blue.400"`
- Looks it up in the generated token registry (`styled-system/tokens/index.mjs`)
- Returns the actual CSS value (e.g., `"#60a5fa"`)
@@ -67,7 +65,6 @@ The `token()` function:
## Token Type Definition
The `Token` type is a union of all valid token paths:
```typescript
type Token = "colors.blue.400" | "colors.green.400" | "colors.violet.400" | ...
```

View File

@@ -1,605 +0,0 @@
# Per-Player Stats Architecture & Implementation Plan
## Executive Summary
This document outlines the architecture for tracking game statistics per-player (not per-user). Each local player profile will maintain their own game history, wins, losses, and performance metrics. We'll build a universal framework that any arcade game can use to record results.
**Starting point**: Matching/Memory Lightning game
## Current State Problems
1. ❌ Global `user_stats` table exists but games never update it
2.`/games` page shows same global stats for all players
3. ❌ No framework for games to save results
4. ❌ Players table has no stats fields
## Architecture Design
### 1. Database Schema
#### New Table: `player_stats`
```sql
CREATE TABLE player_stats (
player_id TEXT PRIMARY KEY REFERENCES players(id) ON DELETE CASCADE,
-- Aggregate stats
games_played INTEGER NOT NULL DEFAULT 0,
total_wins INTEGER NOT NULL DEFAULT 0,
total_losses INTEGER NOT NULL DEFAULT 0,
-- Performance metrics
best_time INTEGER, -- Best completion time (ms)
highest_accuracy REAL NOT NULL DEFAULT 0, -- 0.0 - 1.0
-- Game preferences
favorite_game_type TEXT, -- Most played game
-- Per-game stats (JSON)
game_stats TEXT NOT NULL DEFAULT '{}', -- { "matching": { wins: 5, played: 10 }, ... }
-- Timestamps
last_played_at INTEGER, -- timestamp
created_at INTEGER NOT NULL, -- timestamp
updated_at INTEGER NOT NULL -- timestamp
);
CREATE INDEX player_stats_last_played_idx ON player_stats(last_played_at);
```
#### Per-Game Stats Structure (JSON)
```typescript
type PerGameStats = {
[gameName: string]: {
gamesPlayed: number;
wins: number;
losses: number;
bestTime: number | null;
highestAccuracy: number;
averageScore: number;
lastPlayed: number; // timestamp
};
};
```
#### Keep `user_stats`?
**Decision**: Deprecate `user_stats` table. All stats are now per-player.
**Reasoning**:
- Users can have multiple players
- Aggregate "user level" stats can be computed by summing player stats
- Simpler mental model: players compete, players have stats
- `/games` page displays players, so showing player stats makes sense
### 2. Universal Game Result Types
**Analysis**: Examined 5 arcade games (Matching, Complement Race, Memory Quiz, Card Sorting, Rithmomachia)
**Key Finding**: Cooperative games need special handling - all players share win/loss!
**See**: `.claude/GAME_STATS_COMPARISON.md` for detailed cross-game analysis
```typescript
// src/lib/arcade/stats/types.ts
/**
* Standard game result that all arcade games must provide
*
* Supports:
* - 1-N players
* - Competitive (individual winners)
* - Cooperative (team wins/losses)
* - Solo completion
* - Head-to-head (2-player)
*/
export interface GameResult {
// Game identification
gameType: string; // e.g., "matching", "complement-race", "memory-quiz"
// Player results (for multiplayer, array of results)
playerResults: PlayerGameResult[];
// Game metadata
completedAt: number; // timestamp
duration: number; // milliseconds
// Optional game-specific data
metadata?: {
// For cooperative games (Memory Quiz, Card Sorting collaborative)
isTeamVictory?: boolean; // All players share win/loss
// For specific win conditions (Rithmomachia)
winCondition?: string; // e.g., "HARMONY", "POINTS", "TIMEOUT"
// For game modes
gameMode?: string; // e.g., "solo", "competitive", "cooperative"
// Extensible for other game-specific info
[key: string]: unknown;
};
}
export interface PlayerGameResult {
playerId: string;
// Outcome
won: boolean; // For cooperative: all players have same value
placement?: number; // 1st, 2nd, 3rd place (for tournaments with 3+ players)
// Performance
score?: number;
accuracy?: number; // 0.0 - 1.0
completionTime?: number; // milliseconds (player-specific)
// Game-specific metrics (stored as JSON in DB)
metrics?: {
// Matching
moves?: number;
matchedPairs?: number;
difficulty?: number;
// Complement Race
streak?: number;
correctAnswers?: number;
totalQuestions?: number;
// Memory Quiz
correct?: number;
incorrect?: number;
// Card Sorting
exactMatches?: number;
inversions?: number;
lcsLength?: number;
// Rithmomachia
capturedPieces?: number;
points?: number;
// Extensible for future games
[key: string]: unknown;
};
}
/**
* Stats update returned from API
*/
export interface StatsUpdate {
playerId: string;
previousStats: PlayerStats;
newStats: PlayerStats;
changes: {
gamesPlayed: number;
wins: number;
losses: number;
};
}
export interface PlayerStats {
playerId: string;
gamesPlayed: number;
totalWins: number;
totalLosses: number;
bestTime: number | null;
highestAccuracy: number;
favoriteGameType: string | null;
gameStats: PerGameStats;
lastPlayedAt: number | null;
createdAt: number;
updatedAt: number;
}
```
### 3. API Endpoints
#### POST `/api/player-stats/record-game`
Records a game result and updates player stats.
**Request:**
```typescript
{
gameResult: GameResult;
}
```
**Response:**
```typescript
{
success: true,
updates: StatsUpdate[] // One per player
}
```
**Logic:**
1. Validate game result structure
2. For each player result:
- Fetch or create player_stats record
- Increment games_played
- Increment wins/losses based on outcome
- **Special case**: If `metadata.isTeamVictory === true`, all players share win/loss
- Cooperative games: all win or all lose together
- Competitive games: individual outcomes
- Update best_time if improved
- Update highest_accuracy if improved
- Update game-specific stats in JSON
- Update favorite_game_type based on most played
- Set last_played_at
3. Return updates for all players
**Example pseudo-code**:
```typescript
for (const playerResult of gameResult.playerResults) {
const stats = await getPlayerStats(playerResult.playerId);
stats.gamesPlayed++;
// Handle cooperative games specially
if (gameResult.metadata?.isTeamVictory !== undefined) {
// Cooperative: all players share outcome
if (playerResult.won) {
stats.totalWins++;
} else {
stats.totalLosses++;
}
} else {
// Competitive/Solo: individual outcome
if (playerResult.won) {
stats.totalWins++;
} else {
stats.totalLosses++;
}
}
// ... rest of stats update
}
```
#### GET `/api/player-stats/:playerId`
Fetch stats for a specific player.
**Response:**
```typescript
{
stats: PlayerStats;
}
```
#### GET `/api/player-stats`
Fetch stats for all current user's players.
**Response:**
```typescript
{
playerStats: PlayerStats[]
}
```
### 4. React Hooks
#### `useRecordGameResult()`
Main hook that games use to record results.
```typescript
// src/hooks/useRecordGameResult.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { GameResult, StatsUpdate } from "@/lib/arcade/stats/types";
export function useRecordGameResult() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (gameResult: GameResult): Promise<StatsUpdate[]> => {
const res = await fetch("/api/player-stats/record-game", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ gameResult }),
});
if (!res.ok) throw new Error("Failed to record game result");
const data = await res.json();
return data.updates;
},
onSuccess: (updates) => {
// Invalidate player stats queries to trigger refetch
queryClient.invalidateQueries({ queryKey: ["player-stats"] });
// Show success feedback (optional)
console.log("✅ Game result recorded:", updates);
},
onError: (error) => {
console.error("❌ Failed to record game result:", error);
},
});
}
```
#### `usePlayerStats(playerId?)`
Fetch stats for a player (or all players if no ID).
```typescript
// src/hooks/usePlayerStats.ts
import { useQuery } from "@tanstack/react-query";
import type { PlayerStats } from "@/lib/arcade/stats/types";
export function usePlayerStats(playerId?: string) {
return useQuery({
queryKey: playerId ? ["player-stats", playerId] : ["player-stats"],
queryFn: async (): Promise<PlayerStats | PlayerStats[]> => {
const url = playerId
? `/api/player-stats/${playerId}`
: "/api/player-stats";
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch player stats");
const data = await res.json();
return playerId ? data.stats : data.playerStats;
},
});
}
```
### 5. Game Integration Pattern
Every arcade game should follow this pattern when completing:
```typescript
// In results phase component (e.g., ResultsPhase.tsx)
import { useRecordGameResult } from '@/hooks/useRecordGameResult'
import type { GameResult } from '@/lib/arcade/stats/types'
export function ResultsPhase() {
const { state, activePlayers } = useGameContext()
const { mutate: recordGame, isPending } = useRecordGameResult()
// Record game result on mount (once)
useEffect(() => {
if (state.phase === 'results' && !state.recorded) {
const gameResult: GameResult = {
gameType: 'matching',
completedAt: Date.now(),
duration: state.gameEndTime - state.gameStartTime,
playerResults: activePlayers.map(player => ({
playerId: player.id,
won: player.id === winnerId,
score: player.matchCount,
accuracy: player.matchCount / state.totalPairs,
completionTime: player.completionTime,
})),
}
recordGame(gameResult, {
onSuccess: () => {
// Mark as recorded to prevent duplicates
setState({ recorded: true })
}
})
}
}, [state.phase, state.recorded])
// Show loading state while recording
if (isPending) {
return <div>Saving results...</div>
}
// Show results UI
return <div>...</div>
}
```
## Implementation Plan
### Phase 1: Foundation (Database & API)
1. **Create database schema**
- File: `src/db/schema/player-stats.ts`
- Define `player_stats` table with Drizzle ORM
- Add type exports
2. **Generate migration**
```bash
npx drizzle-kit generate:sqlite
```
3. **Create type definitions**
- File: `src/lib/arcade/stats/types.ts`
- Define `GameResult`, `PlayerGameResult`, `StatsUpdate`, `PlayerStats`
4. **Build API endpoint**
- File: `src/app/api/player-stats/record-game/route.ts`
- Implement POST handler with validation
- Handle per-player stat updates
- Transaction safety
5. **Build query endpoints**
- File: `src/app/api/player-stats/route.ts` (GET all)
- File: `src/app/api/player-stats/[playerId]/route.ts` (GET one)
### Phase 2: React Hooks & Integration
6. **Create React hooks**
- File: `src/hooks/useRecordGameResult.ts`
- File: `src/hooks/usePlayerStats.ts`
7. **Update GameModeContext**
- Expose helper to get player stats map
- Integrate with usePlayerStats hook
### Phase 3: Matching Game Integration
8. **Analyze matching game completion flow**
- Find where game completes
- Identify winner calculation
- Map state to GameResult format
9. **Integrate stats recording**
- Add useRecordGameResult to ResultsPhase
- Build GameResult from game state
- Handle recording state to prevent duplicates
10. **Test matching game stats**
- Play solo game, verify stats update
- Play multiplayer game, verify all players update
- Check accuracy calculations
- Check time tracking
### Phase 4: UI Updates
11. **Update /games page**
- Fetch per-player stats with usePlayerStats
- Display correct stats for each player card
- Remove dependency on global user profile
12. **Add stats visualization**
- Per-game breakdown
- Win/loss ratio
- Performance trends
### Phase 5: Documentation & Rollout
13. **Document integration pattern**
- Create guide for adding stats to other games
- Code examples
- Common pitfalls
14. **Roll out to other games**
- Complement Race
- Memory Quiz
- Card Sorting
- (Future games)
## Data Migration Strategy
### Handling Existing `user_stats`
**Option A: Drop the table**
- Simple, clean break
- No historical data
**Option B: Migrate to player stats**
- For each user with stats, assign to their first/active player
- More complex but preserves history
**Recommendation**: Option A (drop it) since:
- Very new feature, unlikely much data exists
- Cleaner architecture
- Users can rebuild stats by playing
### Migration SQL
```sql
-- Drop old user_stats table
DROP TABLE IF EXISTS user_stats;
-- Create new player_stats table
-- (Drizzle migration will handle this)
```
## Testing Strategy
### Unit Tests
- `GameResult` validation
- Stats calculation logic
- JSON merge for per-game stats
- Favorite game detection
### Integration Tests
- API endpoint: record game, verify DB update
- API endpoint: fetch stats, verify response
- React hook: record game, verify cache invalidation
### E2E Tests
- Play matching game solo, check stats on /games page
- Play matching game multiplayer, verify each player's stats
- Verify stats persist across sessions
## Success Criteria
✅ Player stats save correctly after game completion
✅ Each player maintains separate stats
✅ /games page displays correct per-player stats
✅ Stats survive page refresh
✅ Multiplayer games update all participants
✅ Framework is reusable for other games
✅ No duplicate recordings
✅ Performance acceptable (< 200ms to record)
## Open Questions
1. **Leaderboards?** - Future consideration, need global rankings
2. **Historical games?** - Store individual game records or just aggregates?
3. **Stats reset?** - Should users be able to reset player stats?
4. **Achievements?** - Track milestones? (100 games, 50 wins, etc.)
## File Structure
```
src/
├── db/
│ └── schema/
│ └── player-stats.ts # NEW: Drizzle schema
├── lib/
│ └── arcade/
│ └── stats/
│ ├── types.ts # NEW: Type definitions
│ └── utils.ts # NEW: Helper functions
├── hooks/
│ ├── useRecordGameResult.ts # NEW: Record game hook
│ └── usePlayerStats.ts # NEW: Fetch stats hook
├── app/
│ └── api/
│ └── player-stats/
│ ├── route.ts # NEW: GET all
│ ├── record-game/
│ │ └── route.ts # NEW: POST record
│ └── [playerId]/
│ └── route.ts # NEW: GET one
└── arcade-games/
└── matching/
└── components/
└── ResultsPhase.tsx # MODIFY: Add stats recording
.claude/
└── PER_PLAYER_STATS_ARCHITECTURE.md # THIS FILE
```
## Next Steps
1. Review this plan with user
2. Create database schema and types
3. Build API endpoints
4. Create React hooks
5. Integrate with matching game
6. Test thoroughly
7. Roll out to other games
---
**Document Status**: Draft for review
**Last Updated**: 2025-01-03
**Owner**: Claude Code

File diff suppressed because it is too large Load Diff

View File

@@ -1,435 +0,0 @@
# PlayingGuideModal - Complete Feature Specification
## Overview
Interactive, draggable, resizable modal for Rithmomachia game guide with i18n support and bust-out functionality.
## File Location
`src/arcade-games/rithmomachia/components/PlayingGuideModal.tsx`
## Dependencies
```typescript
import { useEffect, useState, useRef } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { useTranslation } from "react-i18next";
import { css } from "../../../../styled-system/css";
import { Z_INDEX } from "@/constants/zIndex";
import { useAbacusSettings } from "@/hooks/useAbacusSettings";
import { PieceRenderer } from "./PieceRenderer";
import { RithmomachiaBoard, type ExamplePiece } from "./RithmomachiaBoard";
import type { PieceType, Color } from "../types";
import "../i18n/config"; // Initialize i18n
```
## Props Interface
```typescript
interface PlayingGuideModalProps {
isOpen: boolean; // Controls visibility
onClose: () => void; // Called when modal closes
standalone?: boolean; // True when opened in popup window (full-screen mode)
}
```
## State Management
### Required State
```typescript
const { t, i18n } = useTranslation();
const { data: abacusSettings } = useAbacusSettings();
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false;
const [activeSection, setActiveSection] = useState<Section>("overview");
const [position, setPosition] = useState({ x: 0, y: 0 });
const [size, setSize] = useState({ width: 450, height: 600 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const [isResizing, setIsResizing] = useState(false);
const [resizeDirection, setResizeDirection] = useState<string>("");
const [isHovered, setIsHovered] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
```
### Section Type
```typescript
type Section =
| "overview"
| "pieces"
| "capture"
| "strategy"
| "harmony"
| "victory";
```
## Core Features
### 1. Radix Dialog Wrapper
**When NOT standalone:**
- Wrap entire modal in `<Dialog.Root open={isOpen} onOpenChange={onClose}>`
- Use `<Dialog.Portal>` for portal rendering
- Use `<Dialog.Overlay>` with backdrop styling
- Use `<Dialog.Content>` as container for draggable/resizable content
**Styling:**
- Overlay: semi-transparent black (`rgba(0, 0, 0, 0.5)`)
- Content: no default positioning (we control via position state)
- Z-index: Must be above game board - use `Z_INDEX.GAME.GUIDE_MODAL` or 15000+
**When standalone:**
- Skip Dialog wrapper entirely
- Render full-screen fixed container
### 2. Draggable Functionality
**Requirements:**
- Click and drag from header to move modal
- Disabled on mobile (`window.innerWidth < 768`)
- Cursor changes to 'move' when hovering header
- Position state tracks x, y coordinates
**Implementation:**
```typescript
const handleMouseDown = (e: React.MouseEvent) => {
if (window.innerWidth < 768) return;
setIsDragging(true);
setDragStart({
x: e.clientX - position.x,
y: e.clientY - position.y,
});
};
```
**Effects:**
- Global `mousemove` listener updates position while dragging
- Global `mouseup` listener stops dragging
- Cleanup listeners on unmount
### 3. Resizable Functionality
**Requirements:**
- 8 resize handles: N, S, E, W, NE, NW, SE, SW
- Handles visible only on hover (when `isHovered === true`)
- Disabled on mobile
- Min size: 450x600
- Max size: 90vw x 80vh
**Handle Positions & Cursors:**
- N (top): `cursor: 'ns-resize'`
- S (bottom): `cursor: 'ns-resize'`
- E (right): `cursor: 'ew-resize'`
- W (left): `cursor: 'ew-resize'`
- NE (top-right): `cursor: 'nesw-resize'`
- NW (top-left): `cursor: 'nwse-resize'`
- SE (bottom-right): `cursor: 'nwse-resize'`
- SW (bottom-left): `cursor: 'nesw-resize'`
**Handle Styling:**
- Width/height: 8px (invisible hit area)
- Visible border when hovered: 2px solid blue
- Positioned absolutely at edges/corners
**Implementation:**
```typescript
const handleResizeStart = (e: React.MouseEvent, direction: string) => {
if (window.innerWidth < 768) return;
e.stopPropagation();
setIsResizing(true);
setResizeDirection(direction);
setDragStart({ x: e.clientX, y: e.clientY });
};
```
### 4. Bust-Out Button
**Location:** Header, right side (before close button)
**Icon:** ↗ or external link icon
**Functionality:**
```typescript
const handleBustOut = () => {
const url = window.location.origin + "/arcade/rithmomachia/guide";
const features =
"width=600,height=800,menubar=no,toolbar=no,location=no,status=no";
window.open(url, "RithmomachiaGuide", features);
};
```
**Visibility:** Only show if NOT already standalone
**Route:** Must have a route at `/arcade/rithmomachia/guide` that renders:
```tsx
<PlayingGuideModal
isOpen={true}
onClose={() => window.close()}
standalone={true}
/>
```
### 5. Internationalization
**Setup:**
- i18n config file: `src/arcade-games/rithmomachia/i18n/config.ts`
- Translation files in: `src/arcade-games/rithmomachia/i18n/locales/`
- Languages: en.json, de.json (minimum)
**Usage:**
- All text uses `t('guide.section.key')` format
- Language switcher in header with buttons for each language
**Header Language Switcher:**
```tsx
<div className={css({ display: "flex", gap: "8px" })}>
{["en", "de"].map((lang) => (
<button
key={lang}
onClick={() => i18n.changeLanguage(lang)}
className={css({
px: "8px",
py: "4px",
fontSize: "12px",
fontWeight: i18n.language === lang ? "bold" : "normal",
bg: i18n.language === lang ? "#3b82f6" : "#e5e7eb",
color: i18n.language === lang ? "white" : "#374151",
border: "none",
borderRadius: "4px",
cursor: "pointer",
})}
>
{lang.toUpperCase()}
</button>
))}
</div>
```
### 6. Centering on Mount
**Effect:**
```typescript
useEffect(() => {
if (isOpen && modalRef.current && !standalone) {
const rect = modalRef.current.getBoundingClientRect();
setPosition({
x: (window.innerWidth - rect.width) / 2,
y: Math.max(50, (window.innerHeight - rect.height) / 2),
});
}
}, [isOpen, standalone]);
```
**Standalone Mode:**
- If standalone, don't center - use full viewport
- Position: fixed, top: 0, left: 0, width: 100vw, height: 100vh
## Layout Structure
```
<Dialog.Root> (if not standalone)
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content asChild>
<div ref={modalRef} style={{ position: absolute, top: position.y, left: position.x }}>
{/* Resize handles (8 total, only if hovered and not mobile) */}
<div> {/* Main container */}
{/* Header */}
<div onMouseDown={handleMouseDown} style={{ cursor: isDragging ? 'grabbing' : 'grab' }}>
<h2>{t('guide.title')}</h2>
<div> {/* Language switcher */}
<button onClick={handleBustOut}> {/* Bust-out (if not standalone) */}
<button onClick={onClose}> {/* Close X */}
</div>
{/* Navigation tabs */}
<div> {/* Section buttons: Overview, Pieces, Capture, Strategy, Harmony, Victory */}
{/* Content area - scrollable */}
<div style={{ overflow: 'auto', maxHeight: size.height - headerHeight }}>
{activeSection === 'overview' && <OverviewSection />}
{activeSection === 'pieces' && <PiecesSection useNativeAbacusNumbers={useNativeAbacusNumbers} />}
{activeSection === 'capture' && <CaptureSection />}
{/* ... etc */}
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
```
## Styling Requirements
### Main Container
- Background: `#ffffff`
- Border radius: `12px`
- Box shadow: `0 20px 60px rgba(0, 0, 0, 0.3)`
- Border: `1px solid #e5e7eb`
- Position: `absolute` (controlled by position state)
- Width/height: from size state
### Header
- Background: `#f9fafb`
- Border bottom: `1px solid #e5e7eb`
- Padding: `16px`
- Display: flex, justify-between, align-items: center
- Cursor: `move` on desktop (when not standalone)
- Prevent text selection while dragging
### Navigation Tabs
- Display: flex, gap: `8px`
- Padding: `12px 16px`
- Background: `#ffffff`
- Border bottom: `1px solid #e5e7eb`
### Tab Buttons
- Active: bold, blue background, white text
- Inactive: normal weight, gray background, dark text
- Padding: `8px 16px`
- Border radius: `6px`
- Cursor: pointer
- Transition: all 0.2s
### Content Area
- Padding: `24px`
- Overflow: auto
- Max height: calculated (size.height - header - tabs)
- Color: `#374151`
- Line height: `1.6`
### Resize Handles
- Position: absolute
- Width/height: 8px
- Background: transparent
- Border: visible on hover (2px solid `#3b82f6`)
- Z-index: 1 (above content)
## Content Sections
### PiecesSection Component
**Must have its own useAbacusSettings hook:**
```typescript
function PiecesSection() {
const { data: abacusSettings } = useAbacusSettings();
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false;
// ... piece rendering with useNativeAbacusNumbers prop
}
```
### All RithmomachiaBoard Uses
- Must pass `useNativeAbacusNumbers={useNativeAbacusNumbers}` prop
- Boards show game positions with pieces
### All PieceRenderer Uses
- Must pass `useNativeAbacusNumbers={useNativeAbacusNumbers}` prop
- Renders individual piece icons in pieces section
## Translation Keys (Minimum Required)
```json
{
"guide": {
"title": "Rithmomachia Playing Guide",
"overview": {
"title": "Overview",
"content": "..."
},
"pieces": {
"title": "Your Pieces",
"circle": "Circle",
"triangle": "Triangle",
"square": "Square",
"pyramid": "Pyramid"
},
"capture": {
"title": "Capture Rules",
"equality": "Equality",
"multiple": "Multiple",
"ratio": "Ratio",
"sum": "Sum",
"difference": "Difference",
"product": "Product"
},
"strategy": {
"title": "Strategy Tips"
},
"harmony": {
"title": "Harmony (Progressions)"
},
"victory": {
"title": "Victory Conditions"
}
}
}
```
## Error Prevention
1. **Z-Index Issue:** Must be higher than game board (use `Z_INDEX.GAME.GUIDE_MODAL` or 15000+)
2. **Lost Work:** Never use `git checkout --` on working files without confirming stash/commit first
3. **Dialog Overlay:** Must render with high z-index to cover game
4. **Mobile:** Disable drag/resize on mobile, make responsive
5. **Standalone Route:** Must exist at `/arcade/rithmomachia/guide`
## Testing Checklist
- [ ] Modal opens and closes correctly
- [ ] Dragging works on desktop
- [ ] Resizing works on desktop (all 8 handles)
- [ ] Drag/resize disabled on mobile
- [ ] Language switcher changes content
- [ ] Bust-out button opens new window
- [ ] New window renders standalone mode correctly
- [ ] Modal appears above game board
- [ ] Close button works
- [ ] All sections render correctly
- [ ] Native abacus numbers toggle respected
- [ ] Translations load for all languages
- [ ] Modal centers on first open
- [ ] Position/size persists while open
- [ ] Cleanup happens on unmount
## Implementation Priority
1. Basic Dialog structure with standalone mode
2. Header with title, close, bust-out
3. Language switcher and i18n setup
4. Navigation tabs
5. Content sections (start with existing content)
6. Dragging functionality
7. Resizing functionality
8. Native abacus numbers integration
9. Translation files
10. Standalone route page

View File

@@ -1,646 +0,0 @@
# Theme Implementation Audit - Abaci.One Web Application
## Executive Summary
The Abaci.One web application is **currently dark-mode only** with:
- Hardcoded dark colors throughout (gray.900, rgba-based colors)
- No existing system-level theme switcher
- A `GameThemeContext` for arcade-specific game backgrounds only (not a general theme system)
- No `prefers-color-scheme` media query support
- Heavy use of Panda CSS for styling (excellent foundation for theming)
- Multiple pages and components with interdependent color schemes
**Implementation Difficulty: MODERATE** - Requires careful coordination of multiple systems but has good Panda CSS foundation.
---
## 1. Current Styling Architecture
### Panda CSS Configuration
**Location:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/panda.config.ts`
**Current State:**
- ✅ Has brand color tokens (blue scale: 50-900)
- ✅ Has soroban-specific tokens (wood, bead, bar colors)
- ✅ Uses color tokens for most UI elements
- ⚠️ NO theme variants defined (no light/dark modes in tokens)
- ⚠️ NO prefers-color-scheme media queries
**Key Tokens Defined:**
```typescript
colors: {
brand: { 50-900 } // Sky blue scale
soroban: { wood, bead, inactive, bar } // Abacus-specific
}
```
### Global CSS
**Location:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/app/globals.css`
**Current State:**
- ✅ Clean, minimal CSS
- ✅ Only navigation height variables and keyframe definitions
- ✅ No hardcoded colors (good!)
- ⚠️ No theme variables
---
## 2. Page Inventory & Color Schemes
### Dark Mode Pages (Primary)
All pages use dark backgrounds. Here's the complete inventory:
#### Homepage (`/src/app/page.tsx`)
- **Background:** `gray.900` (hero section)
- **Text Colors:**
- Headings: White with gradient overlays (#fbbf24, #f59e0b - amber/yellow)
- Body text: `gray.400`, `purple.300`
- Links: White text
- **Special Elements:**
- Mini abacus with dark custom styles (white rgba fills/strokes)
- Skill cards with conditional gold/white borders
- Game cards with vibrant gradient backgrounds
**Line 49, 257:** `bg: 'gray.900'`
**Lines 83, 420-443:** Hardcoded linear-gradient colors with rgba values
#### Blog Pages (`/src/app/blog/page.tsx`, `/src/app/blog/[slug]/page.tsx`)
- **Background:** `gray.900` (main background)
- **Hero Gradient:** `linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)`
- **Text Colors:**
- Body text: `rgba(229, 231, 235, 0.95)` (light gray)
- Links: `rgba(147, 197, 253, 1)` (light blue)
- Code: `rgba(196, 181, 253, 1)` (light purple)
- Headings: `rgba(196, 181, 253, ...)` (purple variants)
- **Code Blocks:** `rgba(0, 0, 0, 0.4)` background
- **Blockquotes:** `rgba(139, 92, 246, ...)` (purple-tinted)
- **Tables:** Purple-tinted headers with dark rows
**Key Feature:** Blog content styling is HIGHLY detailed with nested selectors for markdown elements (h1-h3, p, ul, li, code, pre, blockquote, hr, table) - Lines 225-337
#### Guide Page (`/src/app/guide/page.tsx`)
- **Hero:** `linear-gradient(135deg, #667eea 0%, #764ba2 100%)` (purple gradient)
- **Tabs:** `bg: 'white'` with `borderColor: 'gray.200'` (LIGHT MODE!)
- **Tab Text:** `color: activeTab ? 'brand.600' : 'gray.600'` (LIGHT MODE!)
⚠️ **INCONSISTENCY ALERT:** Guide page uses light backgrounds while rest of site is dark
#### Arcade Games (`/src/app/arcade/**`)
- **Complement Race:**
- **GameDisplay:** `background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'` (purple gradient)
- **Pressure Gauge:** `background: 'rgba(255, 255, 255, 0.95)'` (nearly white!)
- **SVG Colors:** `fill: '#6366f1'` (indigo), `#7cb342` (green), `#d97757` (orange)
- **Text Colors:** `color: '#1f2937'` (dark gray) and `#6b7280` (medium gray)
- **Rithmomachia:**
- Varies by player (white/black pieces)
- **Guide Background:** `background: '#f3f4f6'` (very light gray - almost white!)
- Player badges: Light gradients vs dark backgrounds
- **Memory Quiz:** Not yet fully examined, likely dark-themed
#### Games Page (`/src/app/games/page.tsx`)
- Uses carousel with game cards
- Game cards have vibrant gradient backgrounds
- No explicit background color (inherits parent)
### Light Mode Pages (Inconsistent)
- **Guide Page:** Uses white backgrounds and light text (standalone anomaly)
- **Guide Components:** May have inline light styling
### Special Styling Cases
#### TutorialPlayer Component
- Has a `theme="dark"` prop (line 347 in page.tsx)
- Suggests theme support already exists in TutorialPlayer
- Background: `rgba(0, 0, 0, 0.4)` (dark transparent)
#### Arcade Games Styling
- **Complement Race:** Heavy use of hardcoded colors
- SVG fills: `#6366f1`, `#7cb342`, `#d97757`
- Text colors: `#1f2937`, `#6b7280`, `#3b82f6`, `#10b981`, `#f59e0b`
- Gradients: `linear-gradient(135deg, #3b82f6, #8b5cf6)`, `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`
#### Rithmomachia Guide Page
- Uses `#f3f4f6` (light gray) background
- Contains SVG diagrams that may need color adjustment
---
## 3. Color Usage Patterns
### Hardcoded Colors Found
#### In `/src/app/page.tsx`:
- **Line 63:** `rgba(255, 255, 255, 0.15)` - subtle white overlay pattern
- **Line 83:** `#fbbf24`, `#f59e0b` - amber gradient for title
- **Lines 173-179:** `rgba(255, 255, 255, 0.3-0.4)` - abacus custom styles
- **Lines 420-443:** Multiple gold/white rgba values for skill cards
#### In `/src/app/blog/[slug]/page.tsx`:
- **Line 116:** `rgba(196, 181, 253, 0.8)` - purple link (back button)
- **Line 138:** `rgba(75, 85, 99, 0.5)` - dark border
- **Line 222:** `rgba(229, 231, 235, 0.95)` - body text
- **Lines 239-277:** Extensive markdown styling with hardcoded RGBA values for code, links, blockquotes
- **Line 319:** `rgba(139, 92, 246, 0.2)` - table header background
- **Line 332:** `rgba(75, 85, 99, 0.3)` - table border
#### In `/src/app/guide/page.tsx`:
- **Line 23:** `linear-gradient(135deg, #667eea 0%, #764ba2 100%)` - purple gradient
#### In Arcade Games:
- `/src/app/arcade/complement-race/components/GameDisplay.tsx`: Multiple colors like `#667eea`, `#3b82f6`, `#10b981`, `#f59e0b`
- `/src/app/arcade/complement-race/components/PassengerCard.tsx`: `#e8d4a0`, `#ff6b35`, `#ffaa35`
- `/src/app/arcade/complement-race/components/PressureGauge.tsx`: `#6b7280`, `#1f2937`
- `/src/app/arcade/complement-race/components/RaceTrack/CircularTrack.tsx`: `#7cb342` (green), `#d97757` (orange)
### Missing Dark Mode Equivalents
- **Blog Content:** All markdown styling assumes dark background
- Links: Light blue (`rgba(147, 197, 253, 1)`)
- Code backgrounds: Nearly black
- Need light mode: dark links, light code backgrounds
- **Arcade Games:** Heavy hardcoded colors without token abstraction
- Need systematic color mapping for all 10+ hardcoded hex values
- **SVG Graphics:**
- Hardcoded stroke/fill colors (e.g., `#7cb342`, `#d97757`)
- Will need color inversion or variable injection for light mode
---
## 4. Existing Theme Infrastructure
### GameThemeContext (Current)
**Location:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/contexts/GameThemeContext.tsx`
**Current Capability:**
```typescript
interface GameTheme {
gameName: string
backgroundColor: string
}
```
**Limitations:**
- ❌ Only handles arcade game backgrounds
- ❌ Not a general theme system
- ❌ Only one property (backgroundColor)
- ⚠️ Not wired to system preference detection
### TutorialPlayer Theme Prop
**Location:** `/src/app/page.tsx` line 347
```typescript
<TutorialPlayer theme="dark" />
```
**Implication:** TutorialPlayer component likely has light/dark theme support already
### Context Providers
**Location:** `/src/components/ClientProviders.tsx`
**Current Providers:**
- AbacusDisplayProvider
- QueryClientProvider
- NextIntlClientProvider
- ToastProvider
- UserProfileProvider
- GameModeProvider
- FullscreenProvider
- HomeHeroProvider
- MyAbacusProvider
**NOTE:** No theme provider exists (would need to add)
---
## 5. Special Considerations
### SVG Graphics
**Location:** `/public/blog/difficulty-examples/`, `/public/blog/ten-frame-examples/`
**Challenge:** These SVGs may have hardcoded colors:
- Need to either:
1. Generate light/dark variants of each SVG
2. Use CSS filters for color inversion (lossy)
3. Re-render SVGs with theme-aware colors
4. Make SVGs theme-aware with CSS variables
### Blog Post Content
**Challenge:** Blog HTML is generated from Markdown via `remark-html`
**Current Flow:**
1. Markdown → remark processes → HTML string
2. HTML inserted via `dangerouslySetInnerHTML`
3. Styled via nested CSS selectors in the page component
**Dark Mode Problem:** All nested selectors assume dark background
- Code blocks: dark backgrounds need light backgrounds
- Links: light blue needs dark blue
- Blockquotes: purple tint needs different tone
**Solution Options:**
1. Define light/dark CSS selector variants in panda
2. Use CSS custom properties inside the CSS-in-JS
3. Generate two HTML versions (light/dark)
4. Add a global stylesheet with theme-aware variables
### Arcade Games with Custom Backgrounds
**Challenge:** Games have custom gradient backgrounds and SVG renders
**Components Affected:**
- Complement Race: Purple gradient + multiple SVG colors
- Rithmomachia: Player-specific backgrounds (white/black)
- Memory Quiz: Unknown (needs inspection)
**SVG Examples:**
- `/src/app/arcade/complement-race/components/RaceTrack/CircularTrack.tsx`
- Green path: `#7cb342`
- Orange path: `#d97757`
- Inline SVG with hardcoded fills
### Inline Styles vs CSS Classes
**Finding:** Heavy use of inline Panda `css()` function (good for JS-driven theming)
**Advantage:** Most colors are in code that can be updated programmatically
**Disadvantage:** Requires recompilation/rebuilding for CSS-in-JS changes
### Responsive Design
**Current:** Uses Panda CSS responsive breakpoints extensively
- No responsive theme switching needed (good news)
---
## 6. Component-Level Color Dependencies
### Components with Multiple Color Requirements
#### Skill Cards (Homepage)
- **Background:** Conditional gradient (gold selected vs white unselected)
- **Border:** Gold vs white
- **Text:** White (fixed)
- **Shadow:** Gold vs black
- **Needs:** 5-6 color variants for light/dark
#### Game Cards (Homepage)
- **Gradient Background:** Unique per game
- **Overlay Gradient:** `rgba(0,0,0,...)` (works for both themes if adjusted)
- **Text:** White (needs light mode variant)
#### Blog Markdown
- **Links:** Currently light blue
- **Code blocks:** Currently dark
- **Blockquotes:** Purple-tinted borders
- **Tables:** 4-5 color variants needed
#### Arcade Games
- **Complement Race:** 10+ hardcoded colors
- **Rithmomachia:** Player-based colors (may conflict with light mode)
- **SVG Elements:** Hardcoded stroke/fill
---
## 7. Files Requiring Modification
### High Priority (Core Styling)
1. **`panda.config.ts`** - Add theme tokens and color variants
2. **`globals.css`** - Add CSS custom properties for theme
3. **`ClientProviders.tsx`** - Add theme provider
4. **`AppNavBar.tsx`** - Theme-aware styling
5. **`src/app/layout.tsx`** - Detect system preference, set initial theme
6. **`src/app/page.tsx`** - Update hero and skill cards
### Medium Priority (Pages)
7. **`src/app/blog/page.tsx`** - Update blog index styling
8. **`src/app/blog/[slug]/page.tsx`** - Update blog post markdown styling (COMPLEX)
9. **`src/app/blog/layout.tsx`** - If needed for blog-specific overrides
10. **`src/app/guide/page.tsx`** - Refactor (currently has light mode, make consistent)
11. **`src/app/games/page.tsx`** - Update styling
### High Priority (Arcade)
12. **`src/arcade-games/complement-race/components/GameDisplay.tsx`** - 10+ color replacements
13. **`src/arcade-games/complement-race/components/PressureGauge.tsx`** - Background/text colors
14. **`src/arcade-games/complement-race/components/PassengerCard.tsx`** - Custom colors
15. **`src/arcade-games/complement-race/components/RaceTrack/CircularTrack.tsx`** - SVG colors
16. **`src/arcade-games/complement-race/components/RaceTrack/GhostTrain.tsx`** - SVG colors
### Medium Priority (Contexts & Components)
17. **`src/contexts/GameThemeContext.tsx`** - Extend to general theme system
18. **`src/components/HomeBlogSection.tsx`** - Blog preview card styling
19. **`src/components/TutorialPlayer.tsx`** - Ensure theme prop works correctly
20. **`src/components/tutorial/DecompositionWithReasons.tsx`** - If uses colors
21. **`src/components/matching/** - Any color dependencies
### Low Priority (Tests/Demo)
22. Test pages in `/src/app/test-*` - Update if needed
23. Storybook stories - Update examples
---
## 8. Hardcoded Color Reference Table
| Color | Hex | RGBA | Current Use | Light Mode Option |
|-------|-----|------|-------------|-------------------|
| Amber | #fbbf24 | - | Gradient titles, accents | Keep same? |
| Amber | #f59e0b | - | Gradient titles, accents | Keep same? |
| Purple | #667eea | - | Game gradients | Adjust |
| Purple | #764ba2 | - | Game gradients | Adjust |
| Indigo | #6366f1 | - | Train color | Adjust |
| Blue | #3b82f6 | - | Text/UI | Darken for light mode |
| Blue | #0284c7 | - | Brand (token) | Keep |
| Green | #7cb342 | - | Track path | Adjust |
| Orange | #d97757 | - | Track path | Darken |
| Amber | - | rgba(250, 204, 21, 0.X) | Skill card highlights | Adjust |
| White | - | rgba(255, 255, 255, 0.X) | Patterns, strokes | Invert to black |
| Black | - | rgba(0, 0, 0, 0.X) | Backgrounds, overlays | Invert to white |
| Purple | - | rgba(139, 92, 246, 0.X) | Blog accents | Adjust |
| Purple | - | rgba(196, 181, 253, 0.X) | Blog headings | Darken |
| Gray | - | rgba(209, 213, 219, 0.X) | Body text | Lighten to dark |
| Gray | - | rgba(229, 231, 235, 0.X) | Light text | Invert |
| Gray | - | rgba(75, 85, 99, 0.X) | Borders | Invert |
---
## 9. Potential Breaking Changes
### Layout/Overflow Issues
1. **White text on dark backgrounds** - When inverted for light mode:
- Page titles (white → black)
- Body text (light gray → dark gray)
- May need brand color adjustments
### Game Balance
2. **Arcade Game Colors** - Some colors may have semantic meaning:
- Green/orange track = visual clarity (may need adjustment)
- Train colors = player identification
- Need to test color contrast in light mode
### Blog Content
3. **External SVG Graphics** - May not adapt to light mode:
- Difficulty examples show code/diagrams
- May need PNG/PNG alternatives or CSS filters
### Navigation
4. **AppNavBar** - Currently dark with light text
- Light mode version may need inverse
- Hamburger menu styling
- Dropdown menus
---
## 10. Approach Recommendations
### Phased Implementation Strategy
**Phase 1: Foundation (Week 1)**
- Add theme tokens to `panda.config.ts`
- Create ThemeProvider context
- Add system preference detection
- Update `globals.css` with CSS custom properties
- Implement theme toggle in AppNavBar
**Phase 2: Core Pages (Week 1-2)**
- Homepage (hero, skill cards, game cards)
- Guide page (refactor from light-mode inconsistency)
- Blog index and list
- Basic arcade game components
**Phase 3: Complex Content (Week 2-3)**
- Blog post markdown styling (requires careful nested selector work)
- Arcade games (Complement Race, Rithmomachia)
- SVG graphics (evaluate color mapping strategy)
**Phase 4: Polish & Testing (Week 3)**
- Component testing
- Visual regression testing
- Contrast/accessibility audit
- User feedback
### Recommended Color Palette Structure
```typescript
// Light Mode
{
text: {
primary: '#1f2937', // Dark gray
secondary: '#6b7280', // Medium gray
tertiary: '#9ca3af', // Light gray
inverted: '#ffffff', // White for dark backgrounds
},
background: {
primary: '#ffffff', // White
secondary: '#f3f4f6', // Off-white
tertiary: '#e5e7eb', // Light gray
overlay: 'rgba(0, 0, 0, 0.05)',
},
accent: {
gold: '#f59e0b', // Keep amber
purple: '#7c3aed', // Dark purple
blue: '#2563eb', // Dark blue
},
}
// Dark Mode (current)
{
text: {
primary: '#e5e7eb', // Light gray
secondary: '#d1d5db', // Medium-light gray
tertiary: '#9ca3af', // Medium gray
inverted: '#1f2937', // Dark for light backgrounds
},
background: {
primary: '#111827', // gray.900
secondary: '#1f2937', // gray.800
tertiary: '#374151', // gray.700
overlay: 'rgba(255, 255, 255, 0.05)',
},
accent: {
gold: '#fbbf24', // Light amber
purple: '#c4b5fd', // Light purple
blue: '#93c5fd', // Light blue
},
}
```
### CSS Custom Properties Approach
```css
:root[data-theme="light"] {
--color-text-primary: #1f2937;
--color-bg-primary: #ffffff;
--color-accent-gold: #f59e0b;
/* ... more properties */
}
:root[data-theme="dark"],
:root {
--color-text-primary: #e5e7eb;
--color-bg-primary: #111827;
--color-accent-gold: #fbbf24;
/* ... more properties */
}
```
---
## 11. Accessibility Considerations
### Color Contrast
- **WCAG AA minimum:** 4.5:1 for normal text, 3:1 for large text
- **Current dark mode:** Generally good contrast
- **Light mode risk:** Some colors may not meet 4.5:1
- Purple headings may be too light on white
- Gray text may not be dark enough
### Recommendations
1. Run contrast checker on all color combinations
2. Test with ColorOracle color-blindness simulator
3. Ensure brand colors (amber, purple) meet WCAG AA in both modes
4. Consider reducing gold opacity in light mode
### Motion/Animation
- Particle effects and gradients should respect `prefers-reduced-motion`
- Currently using animations in panda.config.ts without media query check
---
## 12. Testing Strategy
### Visual Testing
- Screenshot comparison tool (Percy, Chromatic)
- Manual review of all pages in light/dark
- Test on multiple browsers
### Color Contrast Testing
- WAVE WebAIM contrast checker
- Axe DevTools
- Manual ColorOracle testing
### Component Testing
- Unit tests for theme switching logic
- Integration tests for context propagation
- E2E tests for theme persistence
### User Testing
- Usability testing with theme toggle
- Feedback on color choices
- Accessibility feedback from screen reader users
---
## 13. Implementation Complexity Matrix
| Component | Dark→Light Complexity | Reason |
|-----------|----------------------|--------|
| Homepage | MEDIUM | Skill cards, gradients need careful adjustment |
| Blog Index | LOW | Straightforward color inversions |
| Blog Posts | HIGH | Complex markdown selectors, many color variants |
| Guide Page | LOW | Mostly refactoring (already has light mode) |
| Arcade Games | HIGH | 10+ hardcoded colors per game, SVG issues |
| Complement Race | HIGH | Complex SVG, particle effects, gradients |
| Rithmomachia | MEDIUM | Player-based colors may need semantic rethink |
| Navigation | MEDIUM | Multiple menus, dropdowns need testing |
| SVG Graphics | MEDIUM-HIGH | Depends on CSS filter strategy |
| Tutorial Player | LOW | Already has theme prop (verify works) |
| Components | LOW-MEDIUM | Most use Panda CSS, easy to theme |
---
## 14. Risk Assessment
### High Risk Areas
1. **Blog Post Markdown Rendering** - Complex nested CSS selectors, many color variants
2. **Arcade Game SVGs** - Hardcoded fills/strokes, may not respond to CSS
3. **Color Contrast in Light Mode** - Some purple/gold combinations may fail WCAG
4. **System Preference Detection** - Browser APIs differ, SSR hydration issues
### Medium Risk Areas
1. **Theme Persistence** - localStorage, cookie handling, sync across tabs
2. **Performance** - Theme switching shouldn't cause layout shift
3. **Component Library** - Third-party components (Radix UI) may need theme updates
4. **Animation Color Interactions** - Particle effects, gradients may look odd in light mode
### Low Risk Areas
1. **Text Content** - No changes needed
2. **Layout/Structure** - Theme doesn't affect grid/flex
3. **Interactions** - Click handlers, form validation unchanged
4. **API Integration** - Backend unaffected
---
## 15. File Location Quick Reference
### Configuration
- Panda Config: `/panda.config.ts`
- Global CSS: `/src/app/globals.css`
- Theme Context: `/src/contexts/GameThemeContext.tsx` (needs extension)
### Core Pages
- Homepage: `/src/app/page.tsx` (49, 257, 310, 420-443)
- Blog: `/src/app/blog/page.tsx`, `/src/app/blog/[slug]/page.tsx` (225-337)
- Guide: `/src/app/guide/page.tsx` (23, 57-79)
- Games: `/src/app/games/page.tsx`
### Arcade Games
- Complement Race Display: `/src/app/arcade/complement-race/components/GameDisplay.tsx`
- Pressure Gauge: `/src/app/arcade/complement-race/components/PressureGauge.tsx`
- Passenger Card: `/src/app/arcade/complement-race/components/PassengerCard.tsx`
- Race Track: `/src/app/arcade/complement-race/components/RaceTrack/CircularTrack.tsx`
### Client Setup
- Providers: `/src/components/ClientProviders.tsx`
- Layout: `/src/app/layout.tsx`
- NavBar: `/src/components/AppNavBar.tsx`
---
## 16. Additional Exploration Needed
Before implementation, verify:
1. **TutorialPlayer Light Mode Support**
- Check if `theme="dark"` prop has light equivalent
- Verify theme prop integration
- Location: `/src/components/tutorial/TutorialPlayer.tsx`
2. **Memory Quiz Styling**
- Color scheme not examined
- Location: `/src/app/arcade/memory-quiz/**`
3. **Additional Arcade Games**
- Card Sorting, Matching Games
- Check for hardcoded colors
4. **Third-Party Components**
- Radix UI dropdown, tooltip, dialog theming
- Embla carousel colors
- Toast notifications theming
5. **CSS-in-JS Framework Capabilities**
- Panda CSS theme support
- CSS custom property interpolation
- Performance implications
---
## Summary Table
| Aspect | Current State | Status |
|--------|---------------|--------|
| **Overall Theme** | Dark mode only | ❌ |
| **Color Tokens** | Partial (brand + soroban) | ⚠️ |
| **Theme Variants** | None | ❌ |
| **System Preference Detection** | None | ❌ |
| **Theme Provider** | None (has GameThemeContext for arcade) | ⚠️ |
| **Hardcoded Colors** | ~30+ instances | ❌ |
| **CSS Custom Properties** | Navigation heights only | ⚠️ |
| **Light Mode Pages** | Guide page (inconsistent) | ⚠️ |
| **Accessibility Audit** | Not done | ❌ |
| **Test Coverage** | Likely none for theming | ❌ |
| **Documentation** | `.claude/GAME_THEMES.md` exists | ✅ |
**Overall Readiness: READY TO IMPLEMENT**
- Foundation is solid (Panda CSS)
- Clear color pattern usage
- No blocking architecture issues
- Requires systematic, careful implementation

View File

@@ -1,220 +0,0 @@
================================================================================
THEME IMPLEMENTATION AUDIT - SUMMARY
================================================================================
PROJECT: Abaci.One Web Application
AUDIT DATE: 2025-11-07
SCOPE: Full site light/dark mode planning
DIFFICULTY: MODERATE
TIME ESTIMATE: 2-3 weeks (phased implementation)
================================================================================
KEY FINDINGS
================================================================================
CURRENT STATE:
✅ Excellent Panda CSS foundation for theming
✅ Consistent use of CSS-in-JS (css() function) for styling
✅ Clean global CSS with minimal hardcoded values
❌ Completely dark-mode only (no light mode infrastructure)
❌ ~30+ hardcoded color values scattered throughout
❌ No system preference detection (prefers-color-scheme)
❌ No theme provider/context (except GameThemeContext for arcade only)
PAGES AFFECTED:
- Homepage: Hero section, skill cards, game cards (MEDIUM complexity)
- Blog Index: Straightforward (LOW complexity)
- Blog Posts: Complex markdown styling with 10+ color variants (HIGH complexity)
- Guide Page: Currently INCONSISTENT (has light mode but isolated)
- Arcade Games: 10+ hardcoded colors per game, SVG issues (HIGH complexity)
- Games Page: Carousel styling (LOW complexity)
- Navigation: Multiple components, dropdowns (MEDIUM complexity)
HARDCODED COLORS FOUND:
~30+ instances across:
- rgba() colors for dark mode overlays and text
- Hex colors for game components and SVGs
- Linear gradients with hardcoded values
- Blog markdown element styling
- Arcade game visuals (train, tracks, gauges, cards)
FILES REQUIRING CHANGES: 23+ files across all major sections
================================================================================
PHASED IMPLEMENTATION PLAN
================================================================================
PHASE 1: FOUNDATION (1 week)
✓ Extend panda.config.ts with light/dark color tokens
✓ Create ThemeProvider context (extend GameThemeContext)
✓ Add CSS custom properties to globals.css
✓ Implement system preference detection in layout.tsx
✓ Add theme toggle to AppNavBar
PHASE 2: CORE PAGES (1-2 weeks)
✓ Homepage (hero, skill cards, game cards)
✓ Guide page (fix light-mode inconsistency)
✓ Blog index and list
✓ Games page carousel
PHASE 3: COMPLEX CONTENT (1-2 weeks)
✓ Blog post markdown styling (HIGH EFFORT)
✓ Arcade game components (Complement Race, Rithmomachia)
✓ SVG graphics strategy
PHASE 4: TESTING & POLISH (1 week)
✓ Visual regression testing
✓ Accessibility audit (WCAG contrast)
✓ Cross-browser testing
✓ User feedback
================================================================================
HIGH-RISK AREAS (Require Extra Attention)
================================================================================
1. BLOG POST MARKDOWN (Highest Risk)
- Complex nested CSS selectors for h1-h3, p, ul, li, code, pre, blockquote, table
- ALL markdown styling assumes dark background
- Lines 225-337 in /src/app/blog/[slug]/page.tsx
- Solution: Use CSS custom properties or dual selector strategy
2. ARCADE GAME SVGs (High Risk)
- Inline SVG with hardcoded stroke/fill colors
- Examples: #7cb342 (green), #d97757 (orange), #6366f1 (indigo)
- Solution: CSS filter approach or re-render with theme-aware colors
3. COLOR CONTRAST IN LIGHT MODE (Medium Risk)
- Purple/gold accent colors may not meet WCAG AA (4.5:1)
- Need contrast testing for all combinations
- Especially purple headings on light backgrounds
4. SYSTEM PREFERENCE DETECTION (Medium Risk)
- SSR hydration issues (server dark mode, client light mode)
- localStorage sync across tabs
- Browser API compatibility
================================================================================
QUICK REFERENCE: FILES TO MODIFY
================================================================================
CRITICAL (Foundation):
□ panda.config.ts - Add theme tokens
□ src/app/globals.css - Add CSS custom properties
□ src/contexts/GameThemeContext.tsx - Extend to general theme system
□ src/components/ClientProviders.tsx - Add theme provider
□ src/app/layout.tsx - Detect system preference, set initial theme
□ src/components/AppNavBar.tsx - Add theme toggle
MAJOR PAGES:
□ src/app/page.tsx - Homepage hero and skill cards
□ src/app/blog/page.tsx - Blog index
□ src/app/blog/[slug]/page.tsx - Blog post markdown (COMPLEX)
□ src/app/guide/page.tsx - Fix light-mode inconsistency
□ src/app/games/page.tsx - Games page styling
ARCADE GAMES:
□ src/arcade-games/complement-race/components/GameDisplay.tsx
□ src/arcade-games/complement-race/components/PressureGauge.tsx
□ src/arcade-games/complement-race/components/PassengerCard.tsx
□ src/arcade-games/complement-race/components/RaceTrack/CircularTrack.tsx
□ src/arcade-games/complement-race/components/RaceTrack/GhostTrain.tsx
COMPONENTS:
□ src/components/HomeBlogSection.tsx
□ src/components/tutorial/TutorialPlayer.tsx - Verify theme prop works
================================================================================
COLOR PALETTE RECOMMENDATION
================================================================================
LIGHT MODE:
Text Primary: #1f2937 (Dark gray)
Text Secondary: #6b7280 (Medium gray)
Background: #ffffff (White)
Accent Gold: #f59e0b (Keep same as dark)
Accent Purple: #7c3aed (Dark purple)
Accent Blue: #2563eb (Dark blue)
DARK MODE (Current):
Text Primary: #e5e7eb (Light gray)
Text Secondary: #d1d5db (Medium-light gray)
Background: #111827 (gray.900)
Accent Gold: #fbbf24 (Light amber)
Accent Purple: #c4b5fd (Light purple)
Accent Blue: #93c5fd (Light blue)
================================================================================
TESTING CHECKLIST
================================================================================
Visual Testing:
□ All pages render correctly in light mode
□ All pages render correctly in dark mode
□ Theme toggle works and persists
□ System preference detected correctly
□ No layout shift on theme switch
Accessibility:
□ WCAG AA contrast (4.5:1) for all text
□ WCAG AA contrast (3:1) for large text
□ Test with ColorOracle (color-blindness simulator)
□ Screen reader compatibility
□ Reduced motion support
Browser Support:
□ Chrome/Edge (latest)
□ Firefox (latest)
□ Safari (latest)
□ Mobile browsers
□ SSR hydration (no mismatch)
Component Testing:
□ Blog markdown elements (all tags)
□ Arcade game visuals (gameplay intact)
□ Navigation menus (hover states)
□ Form inputs (dark/light variants)
□ SVG graphics (all variants)
================================================================================
KEY STATISTICS
================================================================================
Total Pages: 14 main pages
Dark Mode Only: 100% of pages
Hardcoded Colors: ~30+ instances
Files to Modify: 23+ files
Blog Markdown Elements: 10+ (h1, h2, h3, p, ul, li, code, pre, blockquote, table)
Arcade Game Colors: 10+ per game
SVG Graphics: 13 files in /public/blog/
Estimated Complexity: MODERATE
Estimated Time: 2-3 weeks (phased)
================================================================================
FULL AUDIT DOCUMENT
================================================================================
Location: /Users/antialias/projects/soroban-abacus-flashcards/apps/web/.claude/THEME_AUDIT.md
Contains:
✓ Detailed analysis of all pages and components
✓ Line-by-line color mapping
✓ Hardcoded color reference table
✓ Risk assessment
✓ Implementation complexity matrix
✓ Accessibility considerations
✓ Testing strategy
✓ File location quick reference
✓ Additional exploration needed
================================================================================
NEXT STEPS
================================================================================
1. Review audit document thoroughly
2. Verify findings (especially TutorialPlayer theme support)
3. Check 3rd-party component theming (Radix UI, Embla carousel)
4. Design color palette and get design approval
5. Begin Phase 1: Foundation work
6. Proceed with phased implementation per schedule
================================================================================

View File

@@ -1,334 +0,0 @@
# Theme Implementation Checklist
## Pre-Implementation Verification
Before starting implementation, verify these items:
- [ ] Read full THEME_AUDIT.md document
- [ ] Review THEME_AUDIT_SUMMARY.txt for quick overview
- [ ] Verify TutorialPlayer has `theme="light"` prop support
- [ ] Check Radix UI theming capabilities
- [ ] Check Embla carousel color customization
- [ ] Review panda.config.ts tokens thoroughly
- [ ] Get design approval on color palette
- [ ] Plan SVG graphics strategy (filters vs variants vs re-render)
## Phase 1: Foundation (Week 1)
### Configuration Files
- [ ] Extend `panda.config.ts` with light/dark color tokens
- [ ] Add theme tokens object with light/dark variants
- [ ] Define all color scales (text, background, accent)
- [ ] Add semantic color names (primary, secondary, etc.)
- [ ] Test token generation: `npm run build`
### Global Styling
- [ ] Update `src/app/globals.css`
- [ ] Add CSS custom properties for theme colors
- [ ] Add `:root[data-theme="light"]` selector
- [ ] Add `:root[data-theme="dark"]` selector
- [ ] Ensure animation/motion properties work with both themes
### Theme Provider
- [ ] Create/extend theme provider in `src/contexts/`
- [ ] Extend GameThemeContext or create new ThemeProvider
- [ ] Add theme detection (system preference, localStorage)
- [ ] Add theme toggle function
- [ ] Handle SSR hydration properly
- [ ] Add `data-theme` attribute to root element
### Integration
- [ ] Update `src/components/ClientProviders.tsx`
- [ ] Add theme provider to provider stack
- [ ] Ensure proper provider ordering
- [ ] Update `src/app/layout.tsx`
- [ ] Add system preference detection
- [ ] Set initial theme from localStorage or system
- [ ] Avoid hydration mismatch
- [ ] Update `src/components/AppNavBar.tsx`
- [ ] Add theme toggle button
- [ ] Update NavBar styling for both themes
- [ ] Test dropdown menus in both modes
- [ ] Test hamburger menu in both modes
### Testing Phase 1
- [ ] Theme detection works (system preference)
- [ ] Theme toggle works and persists
- [ ] No console errors in browser dev tools
- [ ] TypeScript compiles: `npm run type-check`
- [ ] Linting passes: `npm run lint`
## Phase 2: Core Pages (Week 1-2)
### Homepage
- [ ] Update `src/app/page.tsx`
- [ ] Update hero section styling
- [ ] Update skill card styling (gradients, borders)
- [ ] Update game card styling
- [ ] Update mini abacus dark/light styles
- [ ] Test all components render correctly
- [ ] Update `src/components/HomeBlogSection.tsx`
- [ ] Featured posts styling
- [ ] Card backgrounds and borders
- [ ] Text colors and contrast
### Blog Pages
- [ ] Update `src/app/blog/page.tsx`
- [ ] Blog index page styling
- [ ] Featured posts carousel
- [ ] Category filters (if applicable)
- [ ] Update `src/app/guide/page.tsx`
- [ ] Fix light-mode inconsistency
- [ ] Hero section gradient (light/dark variant)
- [ ] Tab styling (was using white background)
- [ ] Component styling
- [ ] Update `src/app/games/page.tsx`
- [ ] Games carousel styling
- [ ] Player carousel styling
- [ ] Game card styling
### Components
- [ ] Update `src/components/TutorialPlayer.tsx`
- [ ] Verify `theme="dark"` prop works
- [ ] Add `theme="light"` support if needed
- [ ] Test tutorial display in both themes
### Testing Phase 2
- [ ] All pages render in light mode
- [ ] All pages render in dark mode
- [ ] Text contrast passes WCAG AA (4.5:1)
- [ ] No layout shifts on theme change
- [ ] Responsive design works in both themes
- [ ] Run quality checks: `npm run pre-commit`
## Phase 3: Complex Content (Week 2-3)
### Blog Post Markdown (HIGHEST PRIORITY - COMPLEX)
- [ ] Update `src/app/blog/[slug]/page.tsx` markdown styling
- [ ] Update h1, h2, h3 styling for light mode
- [ ] Update paragraph text colors
- [ ] Update link colors (light blue → dark blue)
- [ ] Update code block styling (dark bg → light bg)
- [ ] Update pre/code colors
- [ ] Update blockquote styling
- [ ] Update table styling (headers, rows, borders)
- [ ] Update ul/ol/li styling
- [ ] Update hr styling
- [ ] Test all markdown elements render correctly
- [ ] Strategy for CSS selectors:
- Option A: Use CSS custom properties in nested selectors
- Option B: Use dual selectors with theme attribute
- Option C: Create wrapper with theme-specific class
### Arcade Games - Complement Race
- [ ] Update `src/app/arcade/complement-race/components/GameDisplay.tsx`
- [ ] Update background gradient
- [ ] Update text colors
- [ ] Update interactive element colors
- [ ] Update feedback message colors
- [ ] Update `src/app/arcade/complement-race/components/PressureGauge.tsx`
- [ ] Update gauge background
- [ ] Update text colors
- [ ] Update SVG colors
- [ ] Update `src/app/arcade/complement-race/components/PassengerCard.tsx`
- [ ] Update card styling
- [ ] Update custom color usage
- [ ] Update `src/app/arcade/complement-race/components/RaceTrack/CircularTrack.tsx`
- [ ] SVG fill colors (#7cb342, #d97757)
- [ ] SVG stroke colors
- [ ] Consider CSS filter strategy for SVG
- [ ] Update `src/app/arcade/complement-race/components/RaceTrack/GhostTrain.tsx`
- [ ] SVG fill colors
- [ ] Drop shadow colors
- [ ] Update `src/app/arcade/complement-race/components/RaceTrack/LinearTrack.tsx`
- [ ] Track colors
- [ ] Marker colors
### Arcade Games - Rithmomachia
- [ ] Update `src/arcade-games/rithmomachia/components/RithmomachiaGame.tsx`
- [ ] Player badge styling
- [ ] Board background
- [ ] UI element colors
- [ ] Update `src/app/arcade/rithmomachia/guide/page.tsx`
- [ ] Guide page background (#f3f4f6)
- [ ] Text colors
- [ ] Component styling
### SVG Graphics Strategy
- [ ] Decide on approach for blog SVGs:
- Option A: Generate light/dark variants (tedious but reliable)
- Option B: Use CSS filter inversion (lossy but quick)
- Option C: Use CSS variable injection (complex but elegant)
- Option D: Commit both variants to repo
- [ ] Apply chosen strategy to:
- [ ] `/public/blog/difficulty-examples/` (9 files)
- [ ] `/public/blog/ten-frame-examples/` (3 files)
### Testing Phase 3
- [ ] Blog markdown displays correctly in both themes
- [ ] All markdown element colors contrast properly
- [ ] Arcade games playable in both themes
- [ ] SVG graphics visible in both themes
- [ ] No visual artifacts from color changes
- [ ] Run quality checks: `npm run pre-commit`
## Phase 4: Testing & Polish (Week 3-4)
### Visual Testing
- [ ] Manual review of all pages
- [ ] Homepage
- [ ] Blog index
- [ ] Blog post (with all markdown elements)
- [ ] Guide page
- [ ] Games page
- [ ] All arcade games
- [ ] Navigation and dropdowns
- [ ] Screenshot comparison (if available)
- [ ] Compare light and dark variants
- [ ] Check for layout shifts
- [ ] Verify theme consistency
- [ ] Cross-browser testing
- [ ] Chrome/Edge (latest)
- [ ] Firefox (latest)
- [ ] Safari (latest)
- [ ] Mobile Chrome/Safari
### Accessibility Testing
- [ ] Color contrast audit
- [ ] Use WAVE WebAIM checker
- [ ] Use Axe DevTools
- [ ] Verify WCAG AA (4.5:1) for all text
- [ ] Verify WCAG AA (3:1) for large text
- [ ] Check color combinations for color-blind users
- [ ] Color-blindness simulation
- [ ] Use ColorOracle simulator
- [ ] Test protanopia (red-blind)
- [ ] Test deuteranopia (green-blind)
- [ ] Test tritanopia (blue-blind)
- [ ] Motion/animation
- [ ] Test with `prefers-reduced-motion`
- [ ] Verify animations still work appropriately
- [ ] Check for jarring transitions
### Functionality Testing
- [ ] Theme toggle functionality
- [ ] Toggle switches theme immediately
- [ ] Settings persist across page reloads
- [ ] Settings persist across tab closes
- [ ] System preference respected on first visit
- [ ] Game functionality
- [ ] All games playable in both themes
- [ ] No gameplay issues from color changes
- [ ] SVG rendering correct
- [ ] Interactive elements
- [ ] Buttons clickable and visible
- [ ] Form inputs usable
- [ ] Dropdowns functional
- [ ] Modals display correctly
### Performance Testing
- [ ] No layout shift on theme change
- [ ] Theme switch is instant (no flicker)
- [ ] No performance regression
- [ ] Bundle size unchanged significantly
### Documentation
- [ ] Update THEME_AUDIT.md with implementation notes
- [ ] Document color palette choices
- [ ] Document any deviations from plan
- [ ] Create theme customization guide for future work
### Final Quality Check
- [ ] Run `npm run pre-commit` (all checks pass)
- [ ] No console errors or warnings
- [ ] No TypeScript errors
- [ ] No linting errors
- [ ] All tests pass
## Deployment & Communication
### Pre-Launch
- [ ] Get stakeholder review and approval
- [ ] Test on staging environment
- [ ] Get final accessibility sign-off
- [ ] Prepare release notes
### Launch
- [ ] Create git commit with all changes
- [ ] Push to main branch
- [ ] Monitor GitHub Actions build
- [ ] Verify deployment to production
- [ ] Manual smoke test on production
### Post-Launch
- [ ] Monitor error logs for issues
- [ ] Gather user feedback
- [ ] Document any issues found
- [ ] Plan follow-up improvements
## Risk Mitigation
### Known Challenges
- [ ] Blog markdown styling complexity - plan extra time
- [ ] SVG color handling - test multiple browsers
- [ ] SSR hydration - test server vs client rendering
- [ ] Third-party component theming - verify compatibility
### Contingency Plans
- [ ] If SVG strategy fails: use PNG variants as fallback
- [ ] If markdown styling breaks: revert to inline styles temporarily
- [ ] If performance issues: consider lazy-loading theme CSS
- [ ] If accessibility fails: adjust color palette before launch
### Rollback Plan
- [ ] Keep previous version in git history
- [ ] Test rollback procedure before launch
- [ ] Have quick revert command ready
- [ ] Monitor metrics for issues
## Success Criteria
- [x] Site fully functional in light mode
- [x] Site fully functional in dark mode
- [x] System preference detection working
- [x] Theme persistence working
- [x] All WCAG AA accessibility requirements met
- [x] No performance regression
- [x] All tests passing
- [x] User feedback positive
- [x] No critical bugs reported
## Timeline
| Phase | Duration | Status |
|-------|----------|--------|
| Phase 1: Foundation | 1 week | Not started |
| Phase 2: Core Pages | 1-2 weeks | Not started |
| Phase 3: Complex Content | 1-2 weeks | Not started |
| Phase 4: Testing & Polish | 1 week | Not started |
| **Total** | **2-3 weeks** | **Not started** |
---
**Document Version:** 1.0
**Last Updated:** 2025-11-07
**Status:** Ready for implementation

View File

@@ -1,847 +0,0 @@
# Light/Dark Theme Implementation Plan
## Status: Phase 1 Complete ✅
**Last Updated:** 2025-01-07
## Overview
This document outlines the complete plan for implementing auto-switching light/dark themes across the entire Abaci.One website. The implementation follows a page-by-page approach to allow incremental testing and rollout.
---
## Phase 1: Foundation ✅ COMPLETE
**Goal:** Set up the infrastructure needed for theming across the entire app.
### Completed Tasks
- [x] Add semantic color tokens to Panda CSS config
- `bg.canvas`, `bg.surface`, `bg.subtle`, `bg.muted`
- `text.primary`, `text.secondary`, `text.muted`, `text.inverse`
- `border.default`, `border.muted`, `border.emphasis`
- `accent.default`, `accent.emphasis`, `accent.muted`, `accent.subtle`
- `interactive.hover`, `interactive.active`
- [x] Create ThemeProvider with system preference detection
- Detects `prefers-color-scheme` media query
- Supports `light`, `dark`, `system` modes
- Persists to localStorage
- Applies `data-theme` attribute to document root
- [x] Update global styles for theme support
- Added `color-scheme` CSS property
- Configured `data-theme` selectors
- [x] Create theme toggle component
- Simple button with ☀️/🌙 icons
- Added to navigation bar
- Uses semantic tokens as proof-of-concept
- [x] Regenerate Panda CSS with new tokens
- `pnpm panda codegen` executed successfully
### Files Modified
- `panda.config.ts` - Added semantic tokens and conditions
- `src/contexts/ThemeContext.tsx` - NEW
- `src/components/ThemeToggle.tsx` - NEW
- `src/components/ClientProviders.tsx` - Added ThemeProvider
- `src/components/AppNavBar.tsx` - Added ThemeToggle
- `src/app/globals.css` - Added color-scheme property
### Testing Notes
**Current State:** Only the theme toggle button uses semantic tokens. Rest of site still hardcoded to dark colors.
**What to Test:**
- Theme toggle button appears in nav bar (top-right)
- Clicking toggles between light/dark
- Theme persists on page reload
- Button styling changes with theme (bg, border, text colors)
- System preference is detected on first visit
---
## Phase 2: Core Pages (Week 1-2)
**Goal:** Convert high-traffic, simple pages to use semantic tokens.
### 2.1: Homepage (`src/app/page.tsx`)
**Complexity:** MEDIUM - Gradients and hero section need careful adjustment
**Current Issues:**
- Background: `rgba(15, 23, 42, 1)` hardcoded
- Hero gradient: Dark purple/blue hardcoded
- Skill cards: Dark backgrounds with light borders
- Game cards: `rgba(30, 41, 59, 1)` backgrounds
**Changes Needed:**
```typescript
// Hero section background
bg: 'bg.canvas' // instead of rgba(15, 23, 42, 1)
// Hero gradient overlay
background: 'linear-gradient(135deg,
token(colors.accent.subtle) 0%,
token(colors.bg.canvas) 100%)'
// Skill cards
bg: 'bg.surface'
borderColor: 'border.default'
color: 'text.primary'
// Headings
color: 'text.primary' // instead of white/#f1f5f9
```
**Lines to Change:**
- Line 45-50: Main container background
- Line 88-95: Hero gradient
- Line 182-190: Skill cards
- Line 282-290: Game cards
- Line 156: Main heading color
**Testing Checklist:**
- [ ] Hero gradient looks good in both modes
- [ ] Skill cards have proper contrast
- [ ] Game preview cards are readable
- [ ] Hover states work in both themes
- [ ] Text hierarchy is maintained
---
### 2.2: Blog Index (`src/app/blog/page.tsx`)
**Complexity:** LOW - Straightforward color replacements
**Current Issues:**
- Background: `gray.900` (`#111827`)
- Card backgrounds: `rgba(30, 41, 59, 0.6)`
- Text colors: Hardcoded light grays
- Borders: `rgba(75, 85, 99, 0.5)`
**Changes Needed:**
```typescript
// Main container
bg: 'bg.canvas'
// Blog cards
bg: 'bg.surface'
borderColor: 'border.default'
// Title
color: 'text.primary'
// Description
color: 'text.secondary'
// Meta text (date, author)
color: 'text.muted'
// Tags
bg: 'accent.muted'
color: 'accent.emphasis'
```
**Lines to Change:**
- Line 31: Container background
- Line 55-65: Card styling
- Line 82: Title color
- Line 94: Description color
- Line 107-115: Tag styling
**Testing Checklist:**
- [ ] Card backgrounds visible in both modes
- [ ] Text readable with proper contrast
- [ ] Hover states clear
- [ ] Tags stand out appropriately
---
### 2.3: Blog Post Page (`src/app/blog/[slug]/page.tsx`)
**Complexity:** HIGH - Complex markdown styling with 10+ nested selectors
**Current Issues:**
- Background pattern: Assumes dark background
- Markdown content: 100+ lines of nested CSS (h1, h2, p, code, tables, blockquotes, etc.)
- Code blocks: Dark background required
- Inline SVGs: Use CSS custom properties (already done ✅)
**Changes Needed:**
```typescript
// Main container
bg: 'bg.canvas'
// Article content wrapper
color: 'text.primary'
// Headings
'& h1': { color: 'text.primary' }
'& h2': { color: 'accent.emphasis' }
'& h3': { color: 'accent.default' }
// Paragraphs
'& p': { color: 'text.primary' }
// Links
'& a': {
color: 'accent.default',
_hover: { color: 'accent.emphasis' }
}
// Code blocks
'& pre': {
bg: 'bg.muted',
borderColor: 'border.emphasis',
color: 'text.primary'
}
// Inline code
'& code': {
bg: 'bg.subtle',
color: 'accent.emphasis',
borderColor: 'border.default'
}
// Blockquotes
'& blockquote': {
borderColor: 'accent.default',
bg: 'accent.subtle',
color: 'text.secondary'
}
// Tables
'& th': {
bg: 'accent.muted',
color: 'accent.emphasis',
borderColor: 'accent.default'
}
'& td': {
borderColor: 'border.default',
color: 'text.secondary'
}
'& tr:hover td': {
bg: 'interactive.hover'
}
```
**Lines to Change:**
- Line 78-82: Main container background
- Line 86-96: Background pattern (may need two versions)
- Line 217-357: ALL markdown content styles
**Special Considerations:**
- Background pattern might need `opacity` adjustment for light mode
- Code syntax highlighting might need separate light/dark themes
- SVG custom properties already work (done in earlier work) ✅
**Testing Checklist:**
- [ ] All heading levels readable
- [ ] Links have proper contrast
- [ ] Code blocks readable in both modes
- [ ] Tables properly styled
- [ ] Blockquotes stand out but aren't jarring
- [ ] Inline SVGs (ten-frames) still work
- [ ] Background pattern doesn't interfere
---
### 2.4: Guide Page (`src/app/guide/page.tsx`)
**Complexity:** LOW - Already has light styling, needs refactoring for consistency
**Current Issues:**
- Uses isolated light mode styling
- Doesn't use semantic tokens
- Needs integration with theme system
**Changes Needed:**
```typescript
// Convert existing light styles to semantic tokens
bg: 'bg.canvas' // instead of white
color: 'text.primary' // instead of gray.900
// Section backgrounds
bg: 'bg.surface'
// Borders
borderColor: 'border.default'
```
**Lines to Change:**
- Line 28: Main background
- Line 45-50: Section cards
- Line 72: Text colors
**Testing Checklist:**
- [ ] Maintains current light mode appearance
- [ ] Works in dark mode too
- [ ] Consistent with other pages
---
### 2.5: Games Listing (`src/app/games/page.tsx`)
**Complexity:** LOW - Similar to blog index
**Current Issues:**
- Background: Dark hardcoded
- Game cards: Dark backgrounds
- Text: Light grays hardcoded
**Changes Needed:**
```typescript
bg: 'bg.canvas'
// Game cards
bg: 'bg.surface'
borderColor: 'border.default'
// Card hover
bg: 'interactive.hover'
```
**Lines to Change:**
- Line 35: Main background
- Line 58-65: Game card styling
- Line 88: Title colors
**Testing Checklist:**
- [ ] Cards visible in both modes
- [ ] Hover states work
- [ ] Game thumbnails look good
---
## Phase 3: Complex Content (Week 2-3)
**Goal:** Handle arcade games, SVGs, and complex interactive components.
### 3.1: Arcade Games
**Affected Files (5+ games):**
- `src/arcade-games/complement-race/`
- `src/arcade-games/card-sorting/`
- `src/arcade-games/memory-quiz/`
- `src/arcade-games/matching-pairs/`
- `src/arcade-games/rithmomachia/`
**Common Issues:**
- Inline SVGs with hardcoded colors (#7cb342, #d97757, #6366f1, etc.)
- Game board backgrounds assume dark theme
- Score displays, timers hardcoded
- Each game has 10+ color references
**Strategy:**
1. **Create game-specific theme tokens** (extend Panda config):
```typescript
semanticTokens: {
colors: {
'game.background': {
value: { base: '#f8fafc', _dark: '#1e293b' }
},
'game.surface': {
value: { base: '#ffffff', _dark: '#334155' }
},
'game.success': {
value: { base: '#22c55e', _dark: '#4ade80' }
},
'game.error': {
value: { base: '#ef4444', _dark: '#f87171' }
},
'game.warning': {
value: { base: '#f59e0b', _dark: '#fbbf24' }
}
}
}
```
2. **Handle SVGs:**
- Option A: Convert to inline React components using `currentColor`
- Option B: Create dual SVG versions (light/dark)
- Option C: Use CSS filters to invert colors
3. **Game-by-Game Conversion:**
- Start with simplest (Memory Quiz, Card Sorting)
- Then Complement Race (most complex)
- Finally Rithmomachia (largest, but well-structured)
**Per-Game Checklist Template:**
- [ ] Convert background colors
- [ ] Update text colors
- [ ] Fix SVG colors
- [ ] Test game board visibility
- [ ] Verify score/timer readability
- [ ] Check button states
- [ ] Test animations don't break
---
### 3.2: Navigation Components
**Affected Files:**
- `src/components/AppNavBar.tsx` (partially done ✅)
- `src/components/nav/*.tsx` (dropdowns, modals)
**Current Issues:**
- Dropdowns use hardcoded dark backgrounds
- Room creation modal dark themed
- Player indicators hardcoded colors
**Changes Needed:**
```typescript
// Dropdown menus
bg: 'bg.surface'
borderColor: 'border.default'
// Modal overlays
bg: 'rgba(0, 0, 0, 0.5)' // Keep semi-transparent overlay
// Modal content
bg: 'bg.canvas'
// Player status indicators
// Need semantic tokens: online/offline/away colors
```
**Files to Update:**
- `CreateRoomModal.tsx`
- `JoinRoomModal.tsx`
- `RoomInfo.tsx`
- `NetworkPlayerIndicator.tsx`
- `AbacusDisplayDropdown.tsx`
- `LanguageSelector.tsx`
---
### 3.3: External SVG Strategy
**Problem:** Blog ten-frame examples use external SVG files loaded via `<img>` tags. CSS variables don't pass through.
**Current Solution:** Hardcoded light colors (white text, etc.) for dark background.
**Future Options:**
**Option 1: Dual SVG Versions** (Recommended)
```typescript
// Generate both versions
generateTenFrameExamples.ts produces:
- with-ten-frames-light.svg
- with-ten-frames-dark.svg
// In markdown, use theme-aware img src
<img src={`/blog/ten-frame-examples/${filename}-${resolvedTheme}.svg`} />
```
**Option 2: Inline SVGs in Markdown**
- Embed SVG code directly (larger file size)
- CSS variables work
- More maintainable
**Option 3: Dynamic SVG Loading**
- Use React component to load and theme SVGs
- Requires converting blog posts to MDX
**Decision:** Start with Option 1 (dual versions) for blog examples.
---
## Phase 4: Polish & Testing (Week 3)
**Goal:** Ensure quality, accessibility, and performance.
### 4.1: Accessibility Audit
**WCAG AA Requirements:**
- Contrast ratio ≥ 4.5:1 for normal text
- Contrast ratio ≥ 3:1 for large text (18pt+)
- Contrast ratio ≥ 3:1 for UI components
**Testing Tools:**
- Chrome DevTools Lighthouse
- axe DevTools
- WebAIM Contrast Checker
**Critical Areas:**
- Purple accent on light backgrounds (may need darker shade)
- Gold/amber accents (check contrast)
- Game board elements (must be distinguishable)
**Token Adjustments Needed:**
```typescript
// If contrast fails, adjust:
'accent.default': {
value: {
base: '#6d28d9', // Darker purple for light mode
_dark: '#a78bfa',
}
}
```
---
### 4.2: Cross-Browser Testing
**Browsers to Test:**
- Chrome/Edge (Chromium)
- Firefox
- Safari (macOS + iOS)
- Mobile browsers
**Known Issues:**
- Safari has different `prefers-color-scheme` behavior
- Firefox may handle `color-scheme` differently
- Mobile browsers: test on actual devices
**Test Matrix:**
| Browser | Light Mode | Dark Mode | System Auto |
|---------|-----------|-----------|-------------|
| Chrome | ⬜ | ⬜ | ⬜ |
| Firefox | ⬜ | ⬜ | ⬜ |
| Safari | ⬜ | ⬜ | ⬜ |
| iOS Safari | ⬜ | ⬜ | ⬜ |
| Android Chrome | ⬜ | ⬜ | ⬜ |
---
### 4.3: Performance Optimization
**Considerations:**
1. **Initial Render Flash Prevention:**
```typescript
// Add script to <head> to set theme before render
<script dangerouslySetInnerHTML={{
__html: `
(function() {
const theme = localStorage.getItem('theme') || 'system';
const resolvedTheme = theme === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme;
document.documentElement.setAttribute('data-theme', resolvedTheme);
document.documentElement.classList.add(resolvedTheme);
})();
`
}} />
```
2. **CSS Variable Performance:**
- Semantic tokens compile to CSS vars
- Modern browsers handle this efficiently
- No measurable perf impact expected
3. **SVG Loading:**
- Dual SVG versions add minimal overhead
- Consider lazy loading for below-fold content
---
### 4.4: Enhanced Theme Toggle (Optional)
**Current:** Simple button with emoji
**Enhanced Version:**
- Three-state toggle (Light / System / Dark)
- Animated transition
- Keyboard accessible
- Show current system preference
**Design:**
```
┌─────────────────────────┐
│ ☀️ Light System 🌙 Dark │
│ ●─────────────○ │
└─────────────────────────┘
```
**Implementation:**
```typescript
<SegmentedControl>
<Option value="light"> Light</Option>
<Option value="system">🖥 System</Option>
<Option value="dark">🌙 Dark</Option>
</SegmentedControl>
```
---
## Token Reference
### Complete Semantic Token List
```typescript
// Backgrounds
bg.canvas // Main page background
bg.surface // Card/panel backgrounds
bg.subtle // Subtle backgrounds (hover states)
bg.muted // Muted backgrounds (disabled states)
// Text
text.primary // Main text color
text.secondary // Secondary/helper text
text.muted // Muted text (metadata, captions)
text.inverse // Text on colored backgrounds
// Borders
border.default // Standard borders
border.muted // Subtle borders
border.emphasis // Emphasized borders
// Accents (Purple theme)
accent.default // Primary accent color
accent.emphasis // Stronger accent (hover, active)
accent.muted // Very subtle accent (backgrounds)
accent.subtle // Subtle accent (highlights)
// Interactive
interactive.hover // Hover state backgrounds
interactive.active // Active/pressed state backgrounds
```
### Usage Examples
```typescript
// Card component
<div className={css({
bg: 'bg.surface',
borderWidth: '1px',
borderColor: 'border.default',
borderRadius: '0.5rem',
p: '1rem',
_hover: {
bg: 'interactive.hover',
borderColor: 'border.emphasis',
}
})}>
<h3 className={css({ color: 'text.primary' })}>
Title
</h3>
<p className={css({ color: 'text.secondary' })}>
Description text
</p>
</div>
```
---
## Migration Checklist
Use this checklist when converting each page:
### Before Starting
- [ ] Read page code, note all hardcoded colors
- [ ] Identify special cases (gradients, SVGs, animations)
- [ ] Check for any inline styles or !important overrides
### During Conversion
- [ ] Replace background colors with `bg.*` tokens
- [ ] Replace text colors with `text.*` tokens
- [ ] Replace border colors with `border.*` tokens
- [ ] Update hover states to use `interactive.*` tokens
- [ ] Convert accent colors to `accent.*` tokens
- [ ] Test in light mode
- [ ] Test in dark mode
- [ ] Test system auto-switch
### After Conversion
- [ ] Run `npm run pre-commit` (type-check, format, lint)
- [ ] Visual regression test (compare before/after screenshots)
- [ ] Verify no console errors
- [ ] Check accessibility contrast ratios
- [ ] Test on mobile viewport
- [ ] Commit with descriptive message
---
## Known Issues & Workarounds
### Issue 1: Panda CSS Token Syntax
**Problem:** Tokens in conditions need specific syntax.
**Wrong:**
```typescript
color: theme === 'dark' ? 'text.primary' : 'gray.900'
```
**Right:**
```typescript
color: 'text.primary' // Token handles both modes
```
---
### Issue 2: Gradients with Tokens
**Problem:** CSS gradients can't directly use semantic tokens in string templates.
**Workaround:**
```typescript
// Use token() function
background: `linear-gradient(135deg,
token(colors.accent.subtle),
token(colors.bg.canvas))`
// Or use CSS variables
background: 'linear-gradient(135deg,
var(--colors-accent-subtle),
var(--colors-bg-canvas))'
```
---
### Issue 3: Third-Party Components
**Problem:** Some components (Radix UI, etc.) have their own theming.
**Strategy:**
- Use CSS variables to style Radix components
- Override with Panda tokens where possible
- Some components may need separate light/dark styling
---
## Testing Instructions
### For Each Converted Page:
1. **Visual Test:**
- Open page in light mode
- Take screenshot
- Switch to dark mode
- Take screenshot
- Compare: all content should be readable
2. **Interaction Test:**
- Test all buttons, links, forms
- Verify hover states
- Check focus indicators
- Test animations
3. **Responsive Test:**
- Test on mobile (375px width)
- Test on tablet (768px width)
- Test on desktop (1440px width)
4. **Browser Test:**
- Test in Chrome
- Test in Firefox
- Test in Safari (if available)
5. **Accessibility Test:**
- Run Lighthouse audit
- Check contrast ratios
- Test keyboard navigation
---
## Progress Tracking
### Pages Converted to Semantic Tokens
- [ ] Homepage (`src/app/page.tsx`)
- [ ] Blog Index (`src/app/blog/page.tsx`)
- [ ] Blog Post (`src/app/blog/[slug]/page.tsx`)
- [ ] Guide Page (`src/app/guide/page.tsx`)
- [ ] Games Listing (`src/app/games/page.tsx`)
- [ ] Join Page (`src/app/join/[code]/page.tsx`)
### Components Converted
- [x] ThemeToggle ✅
- [x] AppNavBar (partial - toggle only) ✅
- [ ] CreateRoomModal
- [ ] JoinRoomModal
- [ ] RoomInfo
- [ ] AbacusDisplayDropdown
- [ ] LanguageSelector
- [ ] NetworkPlayerIndicator
- [ ] Tutorial components
### Arcade Games Converted
- [ ] Complement Race
- [ ] Card Sorting
- [ ] Memory Quiz
- [ ] Matching Pairs
- [ ] Rithmomachia
---
## Rollback Plan
If major issues arise:
1. **Quick Rollback:**
```bash
git revert <commit-hash>
```
2. **Feature Flag (Future):**
```typescript
// Add environment variable
NEXT_PUBLIC_ENABLE_THEME_SWITCHING=true
// In ThemeProvider
if (!process.env.NEXT_PUBLIC_ENABLE_THEME_SWITCHING) {
return children; // Skip theming
}
```
3. **Gradual Rollout:**
- Deploy with theme system disabled by default
- Enable per-page using URL param: `?theme=light`
- Monitor for issues
- Enable globally when stable
---
## Future Enhancements
### Post-Launch Improvements
1. **Theme Customization:**
- Allow users to customize accent colors
- Save preferred color schemes per-game
- Export/import theme preferences
2. **Automatic Theme Scheduling:**
- Auto-switch based on time of day
- Sunrise/sunset detection
3. **High Contrast Mode:**
- Extra high contrast tokens for accessibility
- Separate from light/dark modes
4. **Theme Preview:**
- Live preview before applying
- A/B test different color schemes
---
## Resources
### Documentation
- [Panda CSS Themes](https://panda-css.com/docs/theming/tokens)
- [Next.js Dark Mode](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts#with-tailwind-css)
- [WCAG Contrast Guidelines](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)
### Tools
- [Coolors Contrast Checker](https://coolors.co/contrast-checker)
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Chrome DevTools Color Picker](https://developer.chrome.com/docs/devtools/accessibility/contrast/)
---
## Notes
- Implementation started: 2025-01-07
- Target completion: 3 weeks from start
- Priority: User-facing pages first, admin/debug pages last
- Breaking changes: None expected (additive only)

View File

@@ -7,9 +7,7 @@ The tutorial system is a sophisticated interactive learning platform for teachin
## Key Components
### 1. TutorialPlayer (`/src/components/tutorial/TutorialPlayer.tsx`)
The main tutorial playback component that:
- Displays tutorial steps progressively
- Highlights specific beads users should interact with
- Provides real-time feedback and tooltips
@@ -18,7 +16,6 @@ The main tutorial playback component that:
- Auto-advances to next step on correct completion
**Key Features:**
- **Bead Highlighting**: Visual indicators showing which beads to manipulate
- **Step Progress**: Shows current step out of total steps
- **Error Feedback**: Provides hints when user makes mistakes
@@ -26,9 +23,7 @@ The main tutorial playback component that:
- **Pedagogical Decomposition**: Explains the "why" behind each operation
### 2. TutorialEditor (`/src/components/tutorial/TutorialEditor.tsx`)
A full-featured editor for creating and editing tutorials:
- Visual step editor
- Bead highlight configuration
- Multi-step instruction editor
@@ -42,69 +37,67 @@ A full-featured editor for creating and editing tutorials:
```typescript
interface Tutorial {
id: string;
title: string;
description: string;
category: string;
difficulty: "beginner" | "intermediate" | "advanced";
estimatedDuration: number; // minutes
steps: TutorialStep[];
tags: string[];
author: string;
version: string;
createdAt: Date;
updatedAt: Date;
isPublished: boolean;
id: string
title: string
description: string
category: string
difficulty: 'beginner' | 'intermediate' | 'advanced'
estimatedDuration: number // minutes
steps: TutorialStep[]
tags: string[]
author: string
version: string
createdAt: Date
updatedAt: Date
isPublished: boolean
}
interface TutorialStep {
id: string;
title: string;
problem: string; // e.g. "2 + 3"
description: string; // User-facing explanation
startValue: number; // Initial abacus value
targetValue: number; // Goal value
expectedAction: "add" | "remove" | "multi-step";
actionDescription: string;
id: string
title: string
problem: string // e.g. "2 + 3"
description: string // User-facing explanation
startValue: number // Initial abacus value
targetValue: number // Goal value
expectedAction: 'add' | 'remove' | 'multi-step'
actionDescription: string
// Bead highlighting
highlightBeads?: Array<{
placeValue: number; // 0=ones, 1=tens, etc.
beadType: "heaven" | "earth";
position?: number; // For earth beads: 0-3
}>;
placeValue: number // 0=ones, 1=tens, etc.
beadType: 'heaven' | 'earth'
position?: number // For earth beads: 0-3
}>
// Progressive step highlighting
stepBeadHighlights?: Array<{
placeValue: number;
beadType: "heaven" | "earth";
position?: number;
stepIndex: number; // Which instruction step
direction: "up" | "down" | "activate" | "deactivate";
order?: number; // Order within step
}>;
placeValue: number
beadType: 'heaven' | 'earth'
position?: number
stepIndex: number // Which instruction step
direction: 'up' | 'down' | 'activate' | 'deactivate'
order?: number // Order within step
}>
totalSteps?: number; // For multi-step operations
multiStepInstructions?: string[]; // Sequential instructions
totalSteps?: number // For multi-step operations
multiStepInstructions?: string[] // Sequential instructions
// Tooltips and guidance
tooltip: {
content: string; // Short title
explanation: string; // Detailed explanation
};
content: string // Short title
explanation: string // Detailed explanation
}
}
```
### 4. Tutorial Converter (`/src/utils/tutorialConverter.ts`)
Utility that converts the original `GuidedAdditionTutorial` data into the new tutorial format:
- `guidedAdditionSteps`: Array of tutorial steps from basic addition to complements
- `convertGuidedAdditionTutorial()`: Converts to Tutorial object
- `getTutorialForEditor()`: Main export used in the app
**Current Tutorial Steps:**
1. Basic Addition (0+1, 1+1, 2+1, 3+1)
2. Heaven Bead Introduction (0+5, 5+1)
3. Five Complements (3+4, 2+3 using 5-complement method)
@@ -113,13 +106,11 @@ Utility that converts the original `GuidedAdditionTutorial` data into the new tu
### 5. Supporting Utilities
**`/src/utils/abacusInstructionGenerator.ts`**
- Automatically generates step-by-step instructions from start/target values
- Creates bead highlight data
- Determines movement directions
**`/src/utils/beadDiff.ts`**
- Calculates differences between abacus states
- Generates visual feedback tooltips
- Explains what changed and why
@@ -167,53 +158,50 @@ return <TutorialPlayer tutorial={friendsOf5Tutorial} />
```typescript
const customTutorial: Tutorial = {
id: "my-tutorial",
title: "My Custom Tutorial",
description: "Learning something new",
category: "Custom",
difficulty: "beginner",
id: 'my-tutorial',
title: 'My Custom Tutorial',
description: 'Learning something new',
category: 'Custom',
difficulty: 'beginner',
estimatedDuration: 5,
steps: [
{
id: "step-1",
title: "Add 2",
problem: "0 + 2",
description: "Move two earth beads up",
id: 'step-1',
title: 'Add 2',
problem: '0 + 2',
description: 'Move two earth beads up',
startValue: 0,
targetValue: 2,
expectedAction: "add",
actionDescription: "Add two earth beads",
expectedAction: 'add',
actionDescription: 'Add two earth beads',
highlightBeads: [
{ placeValue: 0, beadType: "earth", position: 0 },
{ placeValue: 0, beadType: "earth", position: 1 },
{ placeValue: 0, beadType: 'earth', position: 0 },
{ placeValue: 0, beadType: 'earth', position: 1 }
],
tooltip: {
content: "Adding 2",
explanation: "Push two earth beads up to represent 2",
},
},
content: 'Adding 2',
explanation: 'Push two earth beads up to represent 2'
}
}
],
tags: ["custom"],
author: "Me",
version: "1.0.0",
tags: ['custom'],
author: 'Me',
version: '1.0.0',
createdAt: new Date(),
updatedAt: new Date(),
isPublished: true,
};
isPublished: true
}
```
## Current Implementation Locations
**Live Tutorials:**
- `/guide` - Second tab "Arithmetic Operations" contains the full guided addition tutorial
**Editor:**
- `/tutorial-editor` - Full tutorial editing interface
**Storybook:**
- Multiple tutorial stories in `/src/components/tutorial/*.stories.tsx`
## Key Design Principles

View File

@@ -3,13 +3,11 @@
## Confirmations and Dialogs
**NEVER use native browser dialogs:**
-`alert()`
-`confirm()`
-`prompt()`
**ALWAYS use inline React-based confirmations:**
- Show confirmation UI in-place using React state
- Provide Cancel and Confirm buttons
- Use descriptive warning messages with appropriate emoji (⚠️)
@@ -48,7 +46,6 @@ const [confirming, setConfirming] = useState(false)
### Real Examples
See `/src/components/nav/ModerationPanel.tsx` for production examples:
- Transfer ownership confirmation (lines 1793-1929)
- Unban user confirmation (shows inline warning with Cancel/Confirm)
@@ -63,7 +60,6 @@ See `/src/components/nav/ModerationPanel.tsx` for production examples:
### Migration Checklist
When replacing native dialogs:
- [ ] Add state variable for confirmation (e.g., `const [confirming, setConfirming] = useState(false)`)
- [ ] Remove the `confirm()` or `alert()` call from the handler
- [ ] Replace the original UI with conditional rendering
@@ -87,7 +83,6 @@ See `.claude/CLAUDE.md` for complete Panda CSS documentation.
## Emoji Usage
Emojis are used liberally throughout the UI for visual communication:
- 👑 Host/owner status
- ⏳ Waiting states
- ⚠️ Warnings and confirmations

View File

@@ -11,25 +11,25 @@ This document tracks z-index values and stacking contexts across the application
All z-index values should be defined in this file and imported where needed:
```typescript
import { Z_INDEX } from "../constants/zIndex";
import { Z_INDEX } from '../constants/zIndex'
// Use it like this:
zIndex: Z_INDEX.NAV_BAR;
zIndex: Z_INDEX.MODAL;
zIndex: Z_INDEX.GAME_NAV.HAMBURGER_MENU;
zIndex: Z_INDEX.NAV_BAR
zIndex: Z_INDEX.MODAL
zIndex: Z_INDEX.GAME_NAV.HAMBURGER_MENU
```
## Z-Index Layering Hierarchy
From lowest to highest:
| Layer | Range | Purpose | Examples |
| -------------------------- | ----------- | ------------------------------------------------ | ----------------------------------------------------------------- |
| **Base Content** | 0-99 | Default page content, game elements | Background elements, game tracks, cards |
| **Navigation & UI Chrome** | 100-999 | Fixed navigation, sticky headers | AppNavBar, page headers |
| **Overlays & Dropdowns** | 1000-9999 | Tooltips, popovers, dropdowns, tutorial tooltips | Tutorial tooltips (50-100), ConfigForm (50), dropdowns (999-1000) |
| **Modals & Dialogs** | 10000-19999 | Modal dialogs, confirmation dialogs | Modal backdrop (10000), Modal content (10001) |
| **Top-Level Overlays** | 20000+ | Toasts, critical notifications | Toast notifications (20000) |
| Layer | Range | Purpose | Examples |
|-------|-------|---------|----------|
| **Base Content** | 0-99 | Default page content, game elements | Background elements, game tracks, cards |
| **Navigation & UI Chrome** | 100-999 | Fixed navigation, sticky headers | AppNavBar, page headers |
| **Overlays & Dropdowns** | 1000-9999 | Tooltips, popovers, dropdowns, tutorial tooltips | Tutorial tooltips (50-100), ConfigForm (50), dropdowns (999-1000) |
| **Modals & Dialogs** | 10000-19999 | Modal dialogs, confirmation dialogs | Modal backdrop (10000), Modal content (10001) |
| **Top-Level Overlays** | 20000+ | Toasts, critical notifications | Toast notifications (20000) |
## Stacking Context Rules
@@ -70,78 +70,77 @@ If Element A creates a stacking context with `z-index: 1` and Element B is outsi
### ✅ Using Z_INDEX Constants (Good!)
| Component | Value | Source |
| ------------------------- | ---------------------------------------------------- | --------------------------------------------- |
| AppNavBar (Panda section) | `Z_INDEX.NAV_BAR` (100) | `src/components/AppNavBar.tsx:464` |
| AppNavBar hamburger | `Z_INDEX.GAME_NAV.HAMBURGER_MENU` (9999) | `src/components/AppNavBar.tsx:165` |
| AbacusDisplayDropdown | `Z_INDEX.GAME_NAV.HAMBURGER_NESTED_DROPDOWN` (10000) | `src/components/AbacusDisplayDropdown.tsx:99` |
| Component | Value | Source |
|-----------|-------|--------|
| AppNavBar (Panda section) | `Z_INDEX.NAV_BAR` (100) | `src/components/AppNavBar.tsx:464` |
| AppNavBar hamburger | `Z_INDEX.GAME_NAV.HAMBURGER_MENU` (9999) | `src/components/AppNavBar.tsx:165` |
| AbacusDisplayDropdown | `Z_INDEX.GAME_NAV.HAMBURGER_NESTED_DROPDOWN` (10000) | `src/components/AbacusDisplayDropdown.tsx:99` |
### ⚠️ Hardcoded Z-Index Values (Need Migration)
#### Critical Navigation Issues
| Component | Line | Value | Issue | Fix |
| ----------------------------- | ---- | ------ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| **AppNavBar (fixed section)** | 587 | `1000` | ❌ Should use `Z_INDEX.NAV_BAR` (100), but increased to 1000 to fix tutorial tooltip overlap | Define `TUTORIAL_TOOLTIP` in constants, set nav to proper layer |
| AppNavBar (badge) | 645 | `50` | Should use constant | Add `Z_INDEX.BADGE` |
| Component | Line | Value | Issue | Fix |
|-----------|------|-------|-------|-----|
| **AppNavBar (fixed section)** | 587 | `1000` | ❌ Should use `Z_INDEX.NAV_BAR` (100), but increased to 1000 to fix tutorial tooltip overlap | Define `TUTORIAL_TOOLTIP` in constants, set nav to proper layer |
| AppNavBar (badge) | 645 | `50` | Should use constant | Add `Z_INDEX.BADGE` |
#### Tutorial System
| Component | Line | Value | Purpose |
| -------------------------------- | ------------- | ------------ | ---------------------------------------- |
| TutorialPlayer | 643 | `50` | Tooltip container |
| Tutorial shared/EditorComponents | 569, 590 | `50` | Tooltip button |
| Tutorial shared/EditorComponents | 612 | `100` | Dropdown content (must be above tooltip) |
| Tutorial decomposition CSS | 73 | `50` | Legacy CSS |
| TutorialEditor | 65, 812, 2339 | `1000`, `10` | Various overlays |
| Component | Line | Value | Purpose |
|-----------|------|-------|---------|
| TutorialPlayer | 643 | `50` | Tooltip container |
| Tutorial shared/EditorComponents | 569, 590 | `50` | Tooltip button |
| Tutorial shared/EditorComponents | 612 | `100` | Dropdown content (must be above tooltip) |
| Tutorial decomposition CSS | 73 | `50` | Legacy CSS |
| TutorialEditor | 65, 812, 2339 | `1000`, `10` | Various overlays |
#### Modals & Overlays
| Component | Line | Value | Purpose |
| --------------- | ---------- | ---------------- | -------------------------------------- |
| Modal (common) | 59 | `10000` | Modal backdrop |
| ModerationPanel | 1994, 2009 | `10001`, `10002` | Moderation overlays |
| ToastContext | 171 | `10001` | Toast notifications (should be 20000!) |
| Join page | 35 | `10000` | Join page overlay |
| EmojiPicker | 636 | `10000` | Emoji picker modal |
| Component | Line | Value | Purpose |
|-----------|------|-------|---------|
| Modal (common) | 59 | `10000` | Modal backdrop |
| ModerationPanel | 1994, 2009 | `10001`, `10002` | Moderation overlays |
| ToastContext | 171 | `10001` | Toast notifications (should be 20000!) |
| Join page | 35 | `10000` | Join page overlay |
| EmojiPicker | 636 | `10000` | Emoji picker modal |
#### Dropdowns & Popovers
| Component | Line | Value | Purpose |
| ------------------- | -------- | --------------- | ----------------- |
| FormatSelectField | 115 | `999` | Dropdown |
| DeploymentInfoModal | 37, 55 | `9998`, `9999` | Info modal layers |
| RoomInfo | 338, 562 | `9999`, `10000` | Room tooltips |
| GameTitleMenu | 119 | `9999` | Game menu |
| PlayerTooltip | 69 | `9999` | Player tooltip |
| Component | Line | Value | Purpose |
|-----------|------|-------|---------|
| FormatSelectField | 115 | `999` | Dropdown |
| DeploymentInfoModal | 37, 55 | `9998`, `9999` | Info modal layers |
| RoomInfo | 338, 562 | `9999`, `10000` | Room tooltips |
| GameTitleMenu | 119 | `9999` | Game menu |
| PlayerTooltip | 69 | `9999` | Player tooltip |
#### Game Elements
| Component | Line | Value | Purpose |
| ------------------------ | ----------------------- | ------------------------- | ------------------------ |
| Complement Race Game | Multiple | `0`, `1` | Base game layers |
| Complement Race Track | 118, 140, 151 | `10`, `5`, `20` | Track, AI racers, player |
| Complement Race HUD | 51, 106, 119, 137, 168 | `10`, `1000` | HUD elements |
| GameCountdown | 58 | `1000` | Countdown overlay |
| RouteCelebration | 31 | `9999` | Celebration overlay |
| Matching GameCard | 203, 229, 243, 272, 386 | `9`, `10`, `-1`, `8`, `1` | Card layers |
| Matching PlayerStatusBar | 154, 181, 202 | `10`, `10`, `5` | Status bars |
| Component | Line | Value | Purpose |
|-----------|------|-------|---------|
| Complement Race Game | Multiple | `0`, `1` | Base game layers |
| Complement Race Track | 118, 140, 151 | `10`, `5`, `20` | Track, AI racers, player |
| Complement Race HUD | 51, 106, 119, 137, 168 | `10`, `1000` | HUD elements |
| GameCountdown | 58 | `1000` | Countdown overlay |
| RouteCelebration | 31 | `9999` | Celebration overlay |
| Matching GameCard | 203, 229, 243, 272, 386 | `9`, `10`, `-1`, `8`, `1` | Card layers |
| Matching PlayerStatusBar | 154, 181, 202 | `10`, `10`, `5` | Status bars |
#### Misc UI
| Component | Line | Value | Purpose |
| ---------------------- | ----------------------- | ------------------------- | ------------------- |
| HeroAbacus | 89, 127, 163 | `10` | Hero section layers |
| ChampionArena | 425, 514, 554, 614 | `10`, `1`, `1`, `10` | Arena layers |
| NetworkPlayerIndicator | 118, 145, 169, 192, 275 | `-1`, `2`, `1`, `2`, `10` | Player avatars |
| ConfigurationForm | 521, 502 | `50` | Config overlays |
| Component | Line | Value | Purpose |
|-----------|------|-------|---------|
| HeroAbacus | 89, 127, 163 | `10` | Hero section layers |
| ChampionArena | 425, 514, 554, 614 | `10`, `1`, `1`, `10` | Arena layers |
| NetworkPlayerIndicator | 118, 145, 169, 192, 275 | `-1`, `2`, `1`, `2`, `10` | Player avatars |
| ConfigurationForm | 521, 502 | `50` | Config overlays |
## The Recent Bug: Tutorial Tooltips Over Nav Bar
**Problem:** Tutorial tooltips (z-index: 50, 100) were appearing over the navigation bar.
**Root Cause:**
- Nav bar was using `Z_INDEX.NAV_BAR` = 100 in one place
- But also hardcoded `zIndex: 30` in the fixed positioning section (line 587)
- Tutorial tooltips use hardcoded `zIndex: 50` and `zIndex: 100`
@@ -150,7 +149,6 @@ If Element A creates a stacking context with `z-index: 1` and Element B is outsi
**Temporary Fix:** Increased nav bar's hardcoded value from 30 to 1000
**Proper Fix Needed:**
1. Define tutorial tooltip z-indexes in constants file
2. Update nav bar to consistently use `Z_INDEX.NAV_BAR`
3. Ensure NAV_BAR > TUTORIAL_TOOLTIP in the hierarchy
@@ -162,11 +160,11 @@ If Element A creates a stacking context with `z-index: 1` and Element B is outsi
```typescript
// ✅ Good
import { Z_INDEX } from "../constants/zIndex";
zIndex: Z_INDEX.NAV_BAR;
import { Z_INDEX } from '../constants/zIndex'
zIndex: Z_INDEX.NAV_BAR
// ❌ Bad
zIndex: 100; // Magic number!
zIndex: 100 // Magic number!
```
### 2. **Add New Values to Constants File First**
@@ -178,16 +176,15 @@ export const Z_INDEX = {
// ... existing values ...
TUTORIAL: {
TOOLTIP: 500, // Tutorial tooltips (overlays layer)
DROPDOWN: 600, // Tutorial dropdown (above tooltip)
TOOLTIP: 500, // Tutorial tooltips (overlays layer)
DROPDOWN: 600, // Tutorial dropdown (above tooltip)
},
} as const;
} as const
```
### 3. **Choose the Right Layer**
Ask yourself:
- Is this base content? → Use 0-99
- Is this navigation/UI chrome? → Use 100-999
- Is this a dropdown/tooltip/overlay? → Use 1000-9999
@@ -197,7 +194,6 @@ Ask yourself:
### 4. **Understand Your Stacking Context**
Before setting z-index, ask:
- What is my parent's stacking context?
- Am I comparing against siblings or global elements?
- Does my element create a new stacking context?
@@ -209,7 +205,7 @@ If you must deviate from the constants, document why:
```typescript
// HACK: Needs to be above tutorial tooltips (50) but below modals (10000)
// TODO: Migrate to Z_INDEX.TUTORIAL.TOOLTIP system
zIndex: 100;
zIndex: 100
```
## Migration Plan
@@ -223,7 +219,7 @@ export const Z_INDEX = {
// Base content layer (0-99)
BASE: 0,
CONTENT: 1,
HERO_SECTION: 10, // Hero abacus components
HERO_SECTION: 10, // Hero abacus components
// Game content layers (0-99)
GAME_CONTENT: {
@@ -236,15 +232,15 @@ export const Z_INDEX = {
},
// Navigation and UI chrome (100-999)
NAV_BAR: 1000, // ⚠️ Currently needs to be 1000 due to tutorial tooltips
NAV_BAR: 1000, // ⚠️ Currently needs to be 1000 due to tutorial tooltips
STICKY_HEADER: 100,
BADGE: 50,
// Overlays and dropdowns (1000-9999)
TUTORIAL: {
TOOLTIP: 500, // Tutorial tooltips
DROPDOWN: 600, // Tutorial dropdowns (must be > tooltip)
EDITOR: 700, // Tutorial editor
TOOLTIP: 500, // Tutorial tooltips
DROPDOWN: 600, // Tutorial dropdowns (must be > tooltip)
EDITOR: 700, // Tutorial editor
},
DROPDOWN: 1000,
TOOLTIP: 1000,
@@ -271,13 +267,12 @@ export const Z_INDEX = {
HAMBURGER_MENU: 9999,
HAMBURGER_NESTED_DROPDOWN: 10000,
},
} as const;
} as const
```
### Phase 2: Migrate High-Priority Components
Priority order:
1. **Navigation components** (AppNavBar, etc.) - most critical for user experience
2. **Tutorial system** (TutorialPlayer, tooltips) - currently conflicting
3. **Modals and overlays** - ensure they're always on top
@@ -325,13 +320,11 @@ When elements aren't layering correctly:
### DevTools Tips
**Chrome DevTools:**
1. Open DevTools → More Tools → Layers
2. Select an element and see its stacking context
3. View the 3D layer composition
**Firefox DevTools:**
1. Inspector → Layout → scroll to "Z-index"
2. Shows the stacking context parent

View File

@@ -104,82 +104,9 @@
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')",
"Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')",
"Bash(git rev-parse HEAD)",
"Bash(gh run watch --exit-status 18662351595)",
"WebFetch(domain:github.com)",
"WebSearch",
"WebFetch(domain:www.npmjs.com)",
"mcp__sqlite__list_tables",
"mcp__sqlite__describe_table",
"mcp__sqlite__read_query",
"Bash(git rebase:*)",
"Bash(gh run watch:*)",
"Bash(git reflog:*)",
"Bash(do echo -e \"\\n$hash:\")",
"Bash(git fsck:*)",
"Bash(do echo \"=== Stash @{$i} ===\")",
"Bash(git diff-tree:*)",
"Bash(git merge-base:*)",
"Bash(sed:*)",
"Bash(while read file)",
"Bash(do if git show HEAD:\"$file\")",
"Bash(/dev/null)",
"Bash(then echo \"✓ $file\")",
"Bash(git rev-parse:*)",
"Bash(node scripts/parseBoardCSV.js:*)",
"Bash(do echo \"=== HEAD~$i ===\")",
"Read(//private/tmp/**)",
"Bash(do echo \"=== $commit ===\")",
"Bash(do echo \"=== stash@{$i} ===\")",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(jq:*)",
"Bash(src/arcade-games/rithmomachia/components/guide-sections/OverviewSection.tsx )",
"Bash(src/arcade-games/rithmomachia/components/guide-sections/PiecesSection.tsx )",
"Bash(src/arcade-games/rithmomachia/components/guide-sections/CaptureSection.tsx )",
"Bash(src/arcade-games/rithmomachia/components/guide-sections/HarmonySection.tsx )",
"Bash(src/arcade-games/rithmomachia/components/guide-sections/VictorySection.tsx)",
"Bash(pnpm remove:*)",
"Bash(__NEW_LINE__ sed -n '68,73p' CaptureSection.tsx.bak)",
"WebFetch(domain:hub.docker.com)",
"Bash(gcloud auth:*)",
"Bash(gcloud config list:*)",
"WebFetch(domain:www.boardspace.net)",
"WebFetch(domain:www.gamecabinet.com)",
"WebFetch(domain:en.wikipedia.org)",
"Bash(pnpm search:*)",
"Bash(mkdir:*)",
"Bash(timeout 10 npx drizzle-kit generate:sqlite:*)",
"Bash(brew install:*)",
"Bash(sudo ln:*)",
"Bash(cd:*)",
"Bash(git clone:*)",
"Bash(git ls-remote:*)",
"Bash(openscad:*)",
"Bash(npx eslint:*)",
"Bash(env)",
"Bash(security find-generic-password -s 'Anthropic API Key' -w)",
"Bash(printenv:*)",
"Bash(typst:*)",
"Bash(npx tsx:*)",
"Bash(sort:*)",
"Bash(scp:*)",
"Bash(npx prettier:*)",
"Bash(timeout 180 npm run build:*)",
"Bash(.dockerignore.test)",
"Bash(do echo \"=== Check $i ===\")",
"Bash(pnpm panda codegen:*)",
"Bash(npx biome lint:*)",
"Bash(TZ=America/Chicago date:*)",
"Bash(git hash-object:*)",
"Bash(git ls-tree:*)",
"Bash(git -C /Users/antialias/projects/soroban-abacus-flashcards show HEAD:apps/web/src/app/icon/route.tsx)",
"Bash(git -C /Users/antialias/projects/soroban-abacus-flashcards show HEAD:apps/web/package.json)",
"Bash(git revert:*)",
"WebFetch(domain:typst.app)"
"Bash(gh run watch --exit-status 18662351595)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["sqlite"]
}
}

View File

@@ -1 +0,0 @@
# Docker build test

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
# Test deployment - Mon Nov 3 16:31:57 CST 2025

View File

@@ -1,437 +0,0 @@
/**
* @vitest-environment node
*/
import { eq } from 'drizzle-orm'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { db, schema } from '../src/db'
import { createRoom } from '../src/lib/arcade/room-manager'
import { createInvitation, getInvitation } from '../src/lib/arcade/room-invitations'
import { addRoomMember } from '../src/lib/arcade/room-membership'
/**
* Join Flow with Invitation Acceptance E2E Tests
*
* Tests the bug fix for invitation acceptance:
* - When a user joins a restricted room with an invitation
* - The invitation should be marked as "accepted"
* - This prevents the invitation from showing up again
*
* Regression test for the bug where invitations stayed "pending" forever.
*/
describe('Join Flow: Invitation Acceptance', () => {
let hostUserId: string
let guestUserId: string
let hostGuestId: string
let guestGuestId: string
let roomId: string
beforeEach(async () => {
// Create test users
hostGuestId = `test-host-${Date.now()}-${Math.random().toString(36).slice(2)}`
guestGuestId = `test-guest-${Date.now()}-${Math.random().toString(36).slice(2)}`
const [host] = await db.insert(schema.users).values({ guestId: hostGuestId }).returning()
const [guest] = await db.insert(schema.users).values({ guestId: guestGuestId }).returning()
hostUserId = host.id
guestUserId = guest.id
})
afterEach(async () => {
// Clean up invitations
if (roomId) {
await db.delete(schema.roomInvitations).where(eq(schema.roomInvitations.roomId, roomId))
}
// Clean up room
if (roomId) {
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, roomId))
}
// Clean up users
await db.delete(schema.users).where(eq(schema.users.id, hostUserId))
await db.delete(schema.users).where(eq(schema.users.id, guestUserId))
})
describe('BUG FIX: Invitation marked as accepted after join', () => {
it('marks invitation as accepted when guest joins restricted room', async () => {
// 1. Host creates a restricted room
const room = await createRoom({
name: 'Restricted Room',
createdBy: hostGuestId,
creatorName: 'Host User',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted', // Requires invitation
})
roomId = room.id
// 2. Host invites guest
const invitation = await createInvitation({
roomId,
userId: guestUserId,
userName: 'Guest User',
invitedBy: hostUserId,
invitedByName: 'Host User',
invitationType: 'manual',
})
// 3. Verify invitation is pending
expect(invitation.status).toBe('pending')
// 4. Guest joins the room (simulating the join API flow)
// In the real API, it checks the invitation and then adds the member
const invitationCheck = await getInvitation(roomId, guestUserId)
expect(invitationCheck?.status).toBe('pending')
// Simulate what the join API does: add member
await addRoomMember({
roomId,
userId: guestGuestId,
displayName: 'Guest User',
isCreator: false,
})
// 5. BUG: Before fix, invitation would still be "pending" here
// AFTER FIX: The join API now explicitly marks it as "accepted"
// Simulate the fix from join API
const { acceptInvitation } = await import('../src/lib/arcade/room-invitations')
await acceptInvitation(invitation.id)
// 6. Verify invitation is now marked as accepted
const updatedInvitation = await getInvitation(roomId, guestUserId)
expect(updatedInvitation?.status).toBe('accepted')
expect(updatedInvitation?.respondedAt).toBeDefined()
})
it('prevents showing the same invitation again after accepting', async () => {
// This tests the exact bug scenario from the issue:
// "even if I accept the invite and join the room,
// if I try to join room SFK3GD again, then I'm shown the same invite notice"
// 1. Create Room A and Room B
const roomA = await createRoom({
name: 'Room KHS3AE',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
const roomB = await createRoom({
name: 'Room SFK3GD',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'open', // Guest can join without invitation
})
roomId = roomA.id // For cleanup
// 2. Invite guest to Room A
const invitationA = await createInvitation({
roomId: roomA.id,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
// 3. Guest sees invitation to Room A
const { getUserPendingInvitations } = await import('../src/lib/arcade/room-invitations')
let pendingInvites = await getUserPendingInvitations(guestUserId)
expect(pendingInvites).toHaveLength(1)
expect(pendingInvites[0].roomId).toBe(roomA.id)
// 4. Guest accepts and joins Room A
const { acceptInvitation } = await import('../src/lib/arcade/room-invitations')
await acceptInvitation(invitationA.id)
await addRoomMember({
roomId: roomA.id,
userId: guestGuestId,
displayName: 'Guest',
isCreator: false,
})
// 5. Guest tries to visit Room B link (/join/SFK3GD)
// BUG: Before fix, they'd see Room A invitation again because it's still "pending"
// FIX: Invitation is now "accepted", so it won't show in pending list
pendingInvites = await getUserPendingInvitations(guestUserId)
expect(pendingInvites).toHaveLength(0) // ✅ No longer shows Room A
// 6. Guest can successfully join Room B without being interrupted
await addRoomMember({
roomId: roomB.id,
userId: guestGuestId,
displayName: 'Guest',
isCreator: false,
})
// Clean up
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, roomB.id))
})
})
describe('Invitation flow with multiple rooms', () => {
it('only shows pending invitations, not accepted ones', async () => {
// Create 3 rooms, invite to all of them
const room1 = await createRoom({
name: 'Room 1',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
const room2 = await createRoom({
name: 'Room 2',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
const room3 = await createRoom({
name: 'Room 3',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
roomId = room1.id // For cleanup
// Invite to all 3
const inv1 = await createInvitation({
roomId: room1.id,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
const inv2 = await createInvitation({
roomId: room2.id,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
const inv3 = await createInvitation({
roomId: room3.id,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
// All 3 should be pending
const { getUserPendingInvitations, acceptInvitation } = await import(
'../src/lib/arcade/room-invitations'
)
let pending = await getUserPendingInvitations(guestUserId)
expect(pending).toHaveLength(3)
// Accept invitation 1 and join
await acceptInvitation(inv1.id)
// Now only 2 should be pending
pending = await getUserPendingInvitations(guestUserId)
expect(pending).toHaveLength(2)
expect(pending.map((p) => p.roomId)).not.toContain(room1.id)
// Clean up
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, room2.id))
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, room3.id))
})
})
describe('Host re-joining their own restricted room', () => {
it('host can rejoin without invitation (no acceptance needed)', async () => {
// Create restricted room as host
const room = await createRoom({
name: 'Host Room',
createdBy: hostGuestId,
creatorName: 'Host User',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
roomId = room.id
// Host joins their own room
await addRoomMember({
roomId,
userId: hostGuestId,
displayName: 'Host User',
isCreator: true,
})
// No invitation needed, no acceptance
// This should not create any invitation records
const invitation = await getInvitation(roomId, hostUserId)
expect(invitation).toBeUndefined()
})
})
describe('Edge cases', () => {
it('handles multiple invitations from same host to same guest (updates, not duplicates)', async () => {
const room = await createRoom({
name: 'Test Room',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
roomId = room.id
// Send first invitation
const inv1 = await createInvitation({
roomId,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
message: 'First message',
})
// Send second invitation (should update, not create new)
const inv2 = await createInvitation({
roomId,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
message: 'Second message',
})
// Should be same invitation (same ID)
expect(inv1.id).toBe(inv2.id)
expect(inv2.message).toBe('Second message')
// Should only have 1 invitation in database
const allInvitations = await db
.select()
.from(schema.roomInvitations)
.where(eq(schema.roomInvitations.roomId, roomId))
expect(allInvitations).toHaveLength(1)
})
it('re-sends invitation after previous was declined', async () => {
const room = await createRoom({
name: 'Test Room',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'restricted',
})
roomId = room.id
// First invitation
const inv1 = await createInvitation({
roomId,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
// Guest declines
const { declineInvitation, getUserPendingInvitations } = await import(
'../src/lib/arcade/room-invitations'
)
await declineInvitation(inv1.id)
// Should not be in pending list
let pending = await getUserPendingInvitations(guestUserId)
expect(pending).toHaveLength(0)
// Host sends new invitation (should reset to pending)
await createInvitation({
roomId,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
// Should now be in pending list again
pending = await getUserPendingInvitations(guestUserId)
expect(pending).toHaveLength(1)
expect(pending[0].status).toBe('pending')
})
it('accepts invitations to OPEN rooms (not just restricted)', async () => {
// This tests the root cause of the bug:
// Invitations to OPEN rooms were never being marked as accepted
const openRoom = await createRoom({
name: 'Open Room',
createdBy: hostGuestId,
creatorName: 'Host',
gameName: 'rithmomachia',
gameConfig: {},
accessMode: 'open', // Open access - no invitation required to join
})
roomId = openRoom.id
// Host sends invitation anyway (e.g., to notify guest about the room)
const inv = await createInvitation({
roomId: openRoom.id,
userId: guestUserId,
userName: 'Guest',
invitedBy: hostUserId,
invitedByName: 'Host',
invitationType: 'manual',
})
// Guest should see pending invitation
const { getUserPendingInvitations, acceptInvitation } = await import(
'../src/lib/arcade/room-invitations'
)
let pending = await getUserPendingInvitations(guestUserId)
expect(pending).toHaveLength(1)
// Guest joins the open room (invitation not required, but present)
await addRoomMember({
roomId: openRoom.id,
userId: guestGuestId,
displayName: 'Guest',
isCreator: false,
})
// Simulate the join API accepting the invitation
await acceptInvitation(inv.id)
// BUG FIX: Invitation should now be accepted, not stuck in pending
pending = await getUserPendingInvitations(guestUserId)
expect(pending).toHaveLength(0) // ✅ No longer pending
// Verify it's marked as accepted
const acceptedInv = await getInvitation(openRoom.id, guestUserId)
expect(acceptedInv?.status).toBe('accepted')
})
})
})

View File

@@ -1,195 +0,0 @@
---
title: "Beyond Easy and Hard: A 2D Approach to Worksheet Difficulty"
description: "Most educational software uses a simple 1D difficulty slider. We built something better: a constrained 2D space that separates problem complexity from instructional support."
author: "Abaci.one Team"
publishedAt: "2025-11-07"
updatedAt: "2025-11-07"
tags: ["education", "difficulty", "pedagogy", "soroban", "worksheets"]
featured: true
---
# Beyond Easy and Hard: A 2D Approach to Worksheet Difficulty
Most educational software treats difficulty as a one-dimensional slider: easy → medium → hard. But anyone who's taught students knows that difficulty is more nuanced than that.
We've built a new approach for our addition worksheet generator that treats difficulty as **two independent dimensions**: problem complexity (Challenge) and instructional support (Support). And critically, we constrain the combinations to only those that are pedagogically valid.
Here's why this matters and how it works.
## The Problem with 1D Difficulty
Imagine you're a teacher working with two students:
**Student A**: Ready for harder problems with multi-digit regrouping, but still benefits from visual aids like ten-frames and place value colors.
**Student B**: Comfortable working independently without scaffolding, but struggles with complex regrouping and needs simpler problems.
With a traditional "easy/medium/hard" system, you're stuck:
- Setting difficulty to "hard" gives Student A complex problems... but removes all the visual support they still need
- Setting it to "easy" gives Student B the scaffolding-free experience they want... but the problems are too simple
**You can't express "hard problems with visual aids" or "easy problems without scaffolding"** because difficulty conflates two completely different things: the intrinsic complexity of the problem and the amount of instructional support provided.
## Our Solution: Challenge × Support
We split difficulty into two independent dimensions:
### Challenge Axis (Regrouping Complexity)
How complex is the problem itself?
- **Low**: Simple addition, no carrying (23 + 15)
- **Medium**: Some regrouping in ones or tens place (47 + 38)
- **High**: Frequent regrouping across multiple place values (587 + 798)
This is **intrinsic cognitive load** — the inherent difficulty of the problem regardless of how it's presented.
### Support Axis (Scaffolding Level)
How much instructional support is shown?
- **High support**: Carry boxes, answer boxes, place value colors, ten-frames
- **Medium support**: Carry boxes when needed, colors for larger numbers
- **Low support**: Minimal or no scaffolding, student works independently
This is **extraneous cognitive load** — the mental effort required by how the problem is presented and supported.
## But Here's the Crucial Part: Constraints
Not all combinations of Challenge and Support are pedagogically valid.
**High challenge + High support** doesn't work well. If you're giving students complex multi-digit regrouping problems but showing them every step with maximum scaffolding, you're preventing them from developing problem-solving strategies. They're just following the scaffolds, not thinking.
**Low challenge + Low support** is pointless practice. If the problems are trivially simple and you're not providing any instructional structure, students aren't learning anything new.
So we constrain the space to a **diagonal band** of valid combinations:
```
Support (Scaffolding) →
Low Medium High
Challenge High ✓ ✓ ✗
(Regrouping) ✓ ✓ ✓
Medium ✗ ✓ ✓
✗ ✗ ✓
Low ✗ ✓ ✓
```
**As challenge increases, support must decrease** (and vice versa). This encodes a fundamental pedagogical principle: students learning new concepts need support, but as they master the concept, support should fade.
### Visual Examples
Here's what this looks like in practice. Below are actual worksheet examples showing **the same problem complexity** (problems with moderate regrouping) but with **different levels of scaffolding**:
#### Full Scaffolding
![Worksheet with full scaffolding](/blog/difficulty-examples/full-scaffolding.svg)
*Maximum visual support: carry boxes always shown, answer boxes, place value colors, and ten-frames for every step.*
#### Medium Scaffolding
![Worksheet with medium scaffolding](/blog/difficulty-examples/medium-scaffolding.svg)
*Strategic support: carry boxes appear when regrouping occurs, answer boxes provided, place value colors for 3+ digit numbers.*
#### Minimal Scaffolding
![Worksheet with minimal scaffolding](/blog/difficulty-examples/minimal-scaffolding.svg)
*Minimal scaffolding: carry boxes only for complex problems with multiple regroups, no answer boxes or colors.*
#### No Scaffolding
![Worksheet with no scaffolding](/blog/difficulty-examples/no-scaffolding.svg)
*Zero scaffolding: students work completely independently with no visual aids.*
Notice how the **problem complexity stays constant** (all use the same regrouping probability), but the **scaffolding progressively fades**. This demonstrates how support can be adjusted independently from problem difficulty, allowing teachers to precisely target their students' needs.
## Theoretical Foundation
This isn't just intuition — it maps to established learning theory:
**Zone of Proximal Development** (Vygotsky): The diagonal band represents the learnable space. Too easy = already mastered. Too hard without support = beyond reach. The valid combinations are where learning happens.
**Cognitive Load Theory** (Sweller): Effective instruction balances intrinsic load (problem complexity) and extraneous load (instructional design). Our constraints prevent overload from either source.
**Scaffolding Fading** (Wood, Bruner, Ross): Temporary supports should be gradually removed as competence develops. The constraint band enforces this fading principle.
## How Teachers Use It
The UI provides three ways to adjust difficulty:
### 1. Default: "Make Harder" / "Make Easier"
The main buttons adjust **both dimensions** simultaneously, moving diagonally through the valid space toward appropriate preset levels (Beginner → Early Learner → Intermediate → Advanced → Expert).
This is the simple, no-thought-required option that works for most cases.
### 2. Challenge-Only Adjustment
Click the dropdown arrow, select "More challenge" or "Less challenge".
This moves **horizontally** — changing problem complexity while maintaining current scaffolding level.
**Use case**: Student A above. They're ready for harder problems but still need the visual aids. Click "More challenge" to increase regrouping while keeping support constant.
### 3. Support-Only Adjustment
Click the dropdown arrow, select "More support" or "Less support".
This moves **vertically** — changing scaffolding level while maintaining current problem complexity.
**Use case**: Student B above. They understand the concepts and don't need the training wheels anymore. Click "Less support" to remove scaffolding while keeping problems at the same complexity.
## Implementation Details
Under the hood, we use a **hybrid discrete/continuous architecture**:
- **Discrete indices** for navigation: 19 regrouping levels (0-18), 13 scaffolding levels (0-12)
- **Continuous scores** for visualization: Calculated on-the-fly for the difficulty graph and preset detection
- **Constraint validation** at every step: The system auto-corrects invalid states
This gives us:
- Predictable, testable behavior (discrete states)
- Smooth visualization (continuous scores)
- Guaranteed pedagogical validity (constraint enforcement)
Each preset profile (Beginner/Intermediate/etc.) is a specific (challenge, support) coordinate in the valid space. The "Make Harder" button finds the nearest harder preset and navigates toward it, automatically adjusting both dimensions as needed.
## Try It Yourself
The system is live at **[abaci.one/create/worksheets/addition](https://abaci.one/create/worksheets/addition)**.
Try these scenarios:
1. **Start at Beginner**, click "Make Harder" repeatedly → watch it move diagonally through the space
2. **Start at Intermediate**, use the dropdown to select "More challenge" only → see problems get harder while keeping visual aids
3. **Start at Early Learner**, use "Less support" → watch scaffolding disappear while problem complexity stays constant
4. **Click on the 2D graph** (the orange debug visualization) → jump directly to any valid difficulty point
The graph shows:
- Gray diagonal band: Valid pedagogical combinations
- Colored dots: Preset profiles (B=Beginner, I=Intermediate, etc.)
- Blue cross: Your current position
- Click anywhere to jump there (system auto-corrects to nearest valid point)
## Why This Matters
Traditional 1D difficulty forces teachers into a one-size-fits-all progression. Every student moves along the same path from "easy" to "hard", regardless of their individual needs.
**Our 2D constrained space enables precise differentiation**:
- Students who grasp concepts quickly can reduce support while maintaining challenge
- Students who need more time get continued support while still progressing to harder problems
- Students can move through the space at different angles, not just along a single path
And because the constraints encode pedagogical principles, teachers can't accidentally create nonsensical combinations. The system guides them toward valid instructional choices.
## What's Next
This is currently implemented for addition worksheets, but the approach generalizes:
- Subtraction, multiplication, division
- Other domains entirely (reading comprehension, programming exercises, etc.)
- Any learning task where you can separate intrinsic difficulty from instructional support
The code is **open source**: [github.com/antialias/soroban-abacus-flashcards](https://github.com/antialias/soroban-abacus-flashcards)
Technical details: [SMART_DIFFICULTY_SPEC.md](https://github.com/antialias/soroban-abacus-flashcards/blob/main/apps/web/src/app/create/worksheets/addition/SMART_DIFFICULTY_SPEC.md)
## Feedback Welcome
We'd love to hear from educators using this system:
- Does the 2D model match your mental model of difficulty?
- Are the dimension-specific controls useful?
- What other domains would benefit from this approach?
Reach out via [GitHub issues](https://github.com/antialias/soroban-abacus-flashcards/issues) or try the system and let us know what you think.
---
*This post describes research-in-progress. We're exploring publication in learning sciences venues (ACM Learning @ Scale, IJAIED). If you're interested in collaboration or want to cite this work, see our [publication plan](https://github.com/antialias/soroban-abacus-flashcards/blob/main/apps/web/src/app/create/worksheets/addition/PUBLICATION_PLAN.md).*

View File

@@ -1,382 +0,0 @@
---
title: "Making the Invisible Visible: Ten-Frames for Teaching Regrouping"
description: "How visual scaffolding with ten-frames helps students understand the 'make ten' strategy in addition with regrouping, and when to fade this support."
author: "Abaci.one Team"
publishedAt: "2025-11-07"
updatedAt: "2025-11-07"
tags: ["education", "ten-frames", "regrouping", "pedagogy", "scaffolding", "worksheets"]
featured: true
---
# Making the Invisible Visible: Ten-Frames for Teaching Regrouping
When you ask a child "What is 7 + 5?", they might count on their fingers, use mental strategies, or if they're just learning, stare blankly while their brain tries to process what you're asking. But when you show them ten-frames, something magical happens: the abstract becomes concrete, and the "make ten" strategy becomes obvious.
## What Are Ten-Frames?
A ten-frame is a simple 2×5 rectangular grid—ten boxes arranged in two rows of five. Originally developed for teaching number sense and subitizing (instantly recognizing quantities), ten-frames have become an essential tool for teaching addition, especially when regrouping (carrying) is involved.
<svg width="180" height="90" viewBox="0 0 180 90" xmlns="http://www.w3.org/2000/svg">
<!-- Empty ten-frame -->
<rect x="5" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="5" y1="45" x2="175" y2="45" stroke="black" stroke-width="1"/>
<line x1="39" y1="5" x2="39" y2="85" stroke="black" stroke-width="1"/>
<line x1="73" y1="5" x2="73" y2="85" stroke="black" stroke-width="1"/>
<line x1="107" y1="5" x2="107" y2="85" stroke="black" stroke-width="1"/>
<line x1="141" y1="5" x2="141" y2="85" stroke="black" stroke-width="1"/>
<text x="90" y="105" font-family="sans-serif" font-size="14" text-anchor="middle">Empty ten-frame</text>
</svg>
The genius of ten-frames is their structure: **two rows of five boxes make visualizing groups of ten natural**. When you see 7 dots in a ten-frame, you immediately see "5 plus 2 more":
<svg width="180" height="110" viewBox="0 0 180 110" xmlns="http://www.w3.org/2000/svg">
<!-- Ten-frame with 7 -->
<rect x="5" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="5" y1="45" x2="175" y2="45" stroke="black" stroke-width="1"/>
<line x1="39" y1="5" x2="39" y2="85" stroke="black" stroke-width="1"/>
<line x1="73" y1="5" x2="73" y2="85" stroke="black" stroke-width="1"/>
<line x1="107" y1="5" x2="107" y2="85" stroke="black" stroke-width="1"/>
<line x1="141" y1="5" x2="141" y2="85" stroke="black" stroke-width="1"/>
<!-- Top row filled (5) -->
<circle cx="22" cy="25" r="12" fill="#93c5fd"/>
<circle cx="56" cy="25" r="12" fill="#93c5fd"/>
<circle cx="90" cy="25" r="12" fill="#93c5fd"/>
<circle cx="124" cy="25" r="12" fill="#93c5fd"/>
<circle cx="158" cy="25" r="12" fill="#93c5fd"/>
<!-- Bottom row partial (2) -->
<circle cx="22" cy="65" r="12" fill="#93c5fd"/>
<circle cx="56" cy="65" r="12" fill="#93c5fd"/>
<text x="90" y="105" font-family="sans-serif" font-size="14" text-anchor="middle">7 = "5 full + 2 more"</text>
</svg>
When you add 5 more dots and the frame fills up, you physically see the creation of a complete ten, plus extras that don't fit:
<svg width="380" height="110" viewBox="0 0 380 110" xmlns="http://www.w3.org/2000/svg">
<!-- Ten-frame showing 7 + 5 = 12 -->
<!-- First frame (full with 10) -->
<rect x="5" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="5" y1="45" x2="175" y2="45" stroke="black" stroke-width="1"/>
<line x1="39" y1="5" x2="39" y2="85" stroke="black" stroke-width="1"/>
<line x1="73" y1="5" x2="73" y2="85" stroke="black" stroke-width="1"/>
<line x1="107" y1="5" x2="107" y2="85" stroke="black" stroke-width="1"/>
<line x1="141" y1="5" x2="141" y2="85" stroke="black" stroke-width="1"/>
<!-- All 10 filled -->
<circle cx="22" cy="25" r="12" fill="#86efac"/>
<circle cx="56" cy="25" r="12" fill="#86efac"/>
<circle cx="90" cy="25" r="12" fill="#86efac"/>
<circle cx="124" cy="25" r="12" fill="#86efac"/>
<circle cx="158" cy="25" r="12" fill="#86efac"/>
<circle cx="22" cy="65" r="12" fill="#86efac"/>
<circle cx="56" cy="65" r="12" fill="#86efac"/>
<circle cx="90" cy="65" r="12" fill="#86efac"/>
<circle cx="124" cy="65" r="12" fill="#86efac"/>
<circle cx="158" cy="65" r="12" fill="#86efac"/>
<!-- Plus sign -->
<text x="190" y="50" font-family="sans-serif" font-size="24" text-anchor="middle">+</text>
<!-- Second frame (2 remaining) -->
<rect x="205" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="205" y1="45" x2="375" y2="45" stroke="black" stroke-width="1"/>
<line x1="239" y1="5" x2="239" y2="85" stroke="black" stroke-width="1"/>
<line x1="273" y1="5" x2="273" y2="85" stroke="black" stroke-width="1"/>
<line x1="307" y1="5" x2="307" y2="85" stroke="black" stroke-width="1"/>
<line x1="341" y1="5" x2="341" y2="85" stroke="black" stroke-width="1"/>
<!-- 2 filled -->
<circle cx="222" cy="25" r="12" fill="#93c5fd"/>
<circle cx="256" cy="25" r="12" fill="#93c5fd"/>
<text x="190" y="105" font-family="sans-serif" font-size="14" text-anchor="middle">1 ten (green)</text>
<text x="290" y="105" font-family="sans-serif" font-size="14" text-anchor="middle">+ 2 ones (blue) = 12</text>
</svg>
## Why Ten-Frames Matter for Regrouping
Regrouping in addition—the concept that when you add numbers and get more than 10 in a place value, you "carry" to the next column—is one of the first abstract mathematical concepts children encounter. And it's hard.
Consider the problem **47 + 38**:
- When adding the ones place: 7 + 8 = 15
- That's "1 ten and 5 ones"
- The ten gets carried to the tens place
This is abstract. What does it *mean* that 15 is "1 ten and 5 ones"? Why does the "1" move to the tens column? For many students, this becomes a mechanical procedure they follow without understanding.
**Ten-frames make this visible.**
When you represent 7 + 8 with ten-frames:
1. You have a ten-frame with 7 filled boxes
2. You have 8 more to add
3. First, 3 boxes fill up the remaining spaces in the ten-frame → **you made a ten!**
4. The remaining 5 boxes overflow into a second ten-frame
5. Result: **1 full ten-frame (= 10) + 5 extra boxes = 15**
The regrouping isn't a mysterious rule anymore—it's a physical consequence of filling up frames.
## How We Use Ten-Frames in Our Worksheet Generator
Our addition worksheet generator integrates ten-frames directly into problem layout to scaffold the regrouping process. Here's how it works:
### Ten-Frames Appear When Regrouping Happens
The worksheets show **stacked ten-frames** below each place value column that needs regrouping:
- **Bottom frame**: Shows the overflow from the current place value (the "extra" ones that make regrouping necessary)
- **Top frame**: Shows where that overflow goes (carried to the next place value)
- **Color-coded**: Place value colors (blue for ones, green for tens, yellow for hundreds) help connect the frames to their respective columns
For example, in **47 + 38**:
- When adding the ones column (7 + 8), a ten-frame appears below the ones column
- The bottom portion shows the 5 extra ones (in blue) that remain after making a ten
- The top portion shows the 1 ten (in green) that gets carried to the tens column
- Students can literally *see* how the overflow becomes a carry
### Visual Examples
Let's compare the same problem with and without ten-frames to see the difference:
#### With Ten-Frames: Visual Support for Regrouping
![Problem 47 + 38 with ten-frames](/blog/ten-frame-examples/with-ten-frames.svg)
*Ten-frames appear below the ones column, showing how 7 + 8 = 15 breaks down into 1 ten (carried) and 5 ones (remaining). The bottom frame (blue) shows the 5 ones that stay, while the top frame (green) shows the 1 ten that gets carried.*
#### Without Ten-Frames: Abstract Representation
![Problem 47 + 38 without ten-frames](/blog/ten-frame-examples/without-ten-frames.svg)
*The same problem without ten-frames requires students to mentally visualize the regrouping process.*
Notice how the ten-frames make the invisible visible. In 47 + 38, when adding the ones column:
- Students see 7 + 8 creates enough to fill one complete ten-frame (10) with 5 left over
- The filled frame (green, top) represents the carry to the tens place
- The 5 remaining boxes (blue, bottom) stay in the ones place
- This visual directly maps to writing "1" in the carry box and "5" in the ones answer
## Pedagogical Progression: When to Show Ten-Frames
Like all scaffolding, ten-frames should be **introduced when needed and faded when mastered**. Our worksheet generator supports three levels of ten-frame scaffolding:
### 1. Beginner Level: Learning with Ten-Frames
**Use when**: Introducing regrouping for the first time
![Beginner problem 28 + 15 with ten-frames](/blog/ten-frame-examples/beginner-ten-frames.svg)
*A simpler problem (28 + 15) with ten-frames. Students see 8 + 5 = 13, which requires regrouping. The ten-frame shows this as 1 full ten (carried) plus 3 ones (remaining).*
At this level, ten-frames appear when problems involve regrouping. This helps students:
- Build visual familiarity with the ten-frame representation
- Practice the "make ten" strategy with concrete support
- Develop number sense about what sums greater than 10 look like
- Connect the visual representation to the carry notation
**Key insight**: Start with problems that have single-digit sums needing regrouping (like 8 + 5, 7 + 6, 9 + 4), where the ten-frame pattern is clearest.
### 2. Intermediate Level: Ten-Frames for Multiple Regroups
**Use when**: Students understand basic regrouping but need support for complex problems
![Problem with ten-frames in multiple columns](/blog/ten-frame-examples/ten-frames-both-columns.svg)
*A more complex problem (57 + 68) that requires regrouping in BOTH place values. Ten-frames appear below both the ones column (7 + 8 = 15) and the tens column (5 + 6 + 1 = 12), showing students how each overflow creates a carry.*
This is the "smart scaffolding" level. Ten-frames appear only when they're needed—when a column sum exceeds 10. This:
- Reduces visual clutter on simpler problems
- Draws attention to where regrouping is happening
- Lets students practice both with and without visual support
- Shows how regrouping can cascade across multiple place values
**Key insight**: Problems with multiple regroups (like 57 + 68) are where ten-frames really shine—students can see the parallel structure of "making tens" in different place values.
### 3. Never Show Ten-Frames (Advanced Level)
**Use when**: Students have internalized the regrouping concept
At advanced levels, ten-frames are removed entirely. Students should have developed mental models for regrouping and can work abstractly with just carry boxes and place value colors (which also fade over time).
## The "Make Ten" Strategy in Action
Ten-frames teach more than just regrouping—they teach a fundamental mental math strategy called **"make ten."** Here's how a child thinks through 7 + 8 using ten-frames:
### Step 1: "I have 7"
<svg width="180" height="110" viewBox="0 0 180 110" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="5" y1="45" x2="175" y2="45" stroke="black" stroke-width="1"/>
<line x1="39" y1="5" x2="39" y2="85" stroke="black" stroke-width="1"/>
<line x1="73" y1="5" x2="73" y2="85" stroke="black" stroke-width="1"/>
<line x1="107" y1="5" x2="107" y2="85" stroke="black" stroke-width="1"/>
<line x1="141" y1="5" x2="141" y2="85" stroke="black" stroke-width="1"/>
<circle cx="22" cy="25" r="12" fill="#93c5fd"/>
<circle cx="56" cy="25" r="12" fill="#93c5fd"/>
<circle cx="90" cy="25" r="12" fill="#93c5fd"/>
<circle cx="124" cy="25" r="12" fill="#93c5fd"/>
<circle cx="158" cy="25" r="12" fill="#93c5fd"/>
<circle cx="22" cy="65" r="12" fill="#93c5fd"/>
<circle cx="56" cy="65" r="12" fill="#93c5fd"/>
<text x="90" y="105" font-family="sans-serif" font-size="14" text-anchor="middle">"I have 7... I need 3 more to fill the frame!"</text>
</svg>
*Child sees: The top row is full (5), bottom row has 2. Three empty boxes remain to make a complete ten.*
### Step 2: "I can take 3 from the 8 to fill my frame"
<svg width="380" height="140" viewBox="0 0 380 140" xmlns="http://www.w3.org/2000/svg">
<!-- First frame (7) -->
<rect x="5" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="5" y1="45" x2="175" y2="45" stroke="black" stroke-width="1"/>
<line x1="39" y1="5" x2="39" y2="85" stroke="black" stroke-width="1"/>
<line x1="73" y1="5" x2="73" y2="85" stroke="black" stroke-width="1"/>
<line x1="107" y1="5" x2="107" y2="85" stroke="black" stroke-width="1"/>
<line x1="141" y1="5" x2="141" y2="85" stroke="black" stroke-width="1"/>
<circle cx="22" cy="25" r="12" fill="#93c5fd"/>
<circle cx="56" cy="25" r="12" fill="#93c5fd"/>
<circle cx="90" cy="25" r="12" fill="#93c5fd"/>
<circle cx="124" cy="25" r="12" fill="#93c5fd"/>
<circle cx="158" cy="25" r="12" fill="#93c5fd"/>
<circle cx="22" cy="65" r="12" fill="#93c5fd"/>
<circle cx="56" cy="65" r="12" fill="#93c5fd"/>
<!-- The 3 that will move -->
<circle cx="90" cy="65" r="12" fill="#fbbf24" stroke="#000" stroke-width="2" stroke-dasharray="4"/>
<circle cx="124" cy="65" r="12" fill="#fbbf24" stroke="#000" stroke-width="2" stroke-dasharray="4"/>
<circle cx="158" cy="65" r="12" fill="#fbbf24" stroke="#000" stroke-width="2" stroke-dasharray="4"/>
<!-- Arrow -->
<path d="M 90 95 Q 130 110 170 95" fill="none" stroke="#000" stroke-width="2" marker-end="url(#arrowhead)"/>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<polygon points="0 0, 10 3, 0 6" fill="#000"/>
</marker>
</defs>
<text x="130" y="130" font-family="sans-serif" font-size="12" text-anchor="middle">Take 3 from 8</text>
<!-- Second frame (remaining 5) -->
<rect x="205" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="205" y1="45" x2="375" y2="45" stroke="black" stroke-width="1"/>
<line x1="239" y1="5" x2="239" y2="85" stroke="black" stroke-width="1"/>
<line x1="273" y1="5" x2="273" y2="85" stroke="black" stroke-width="1"/>
<line x1="307" y1="5" x2="307" y2="85" stroke="black" stroke-width="1"/>
<line x1="341" y1="5" x2="341" y2="85" stroke="black" stroke-width="1"/>
<circle cx="222" cy="25" r="12" fill="#93c5fd"/>
<circle cx="256" cy="25" r="12" fill="#93c5fd"/>
<circle cx="290" cy="25" r="12" fill="#93c5fd"/>
<circle cx="324" cy="25" r="12" fill="#93c5fd"/>
<circle cx="358" cy="25" r="12" fill="#93c5fd"/>
<text x="290" y="105" font-family="sans-serif" font-size="14" text-anchor="middle">5 left over</text>
</svg>
*Child thinks: "8 is really 3 and 5. I'll use the 3 to complete my ten-frame, and I have 5 extras."*
### Step 3: "Now I have 10 + 5 = 15!"
<svg width="380" height="110" viewBox="0 0 380 110" xmlns="http://www.w3.org/2000/svg">
<!-- Full frame (10) -->
<rect x="5" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="5" y1="45" x2="175" y2="45" stroke="black" stroke-width="1"/>
<line x1="39" y1="5" x2="39" y2="85" stroke="black" stroke-width="1"/>
<line x1="73" y1="5" x2="73" y2="85" stroke="black" stroke-width="1"/>
<line x1="107" y1="5" x2="107" y2="85" stroke="black" stroke-width="1"/>
<line x1="141" y1="5" x2="141" y2="85" stroke="black" stroke-width="1"/>
<circle cx="22" cy="25" r="12" fill="#86efac"/>
<circle cx="56" cy="25" r="12" fill="#86efac"/>
<circle cx="90" cy="25" r="12" fill="#86efac"/>
<circle cx="124" cy="25" r="12" fill="#86efac"/>
<circle cx="158" cy="25" r="12" fill="#86efac"/>
<circle cx="22" cy="65" r="12" fill="#86efac"/>
<circle cx="56" cy="65" r="12" fill="#86efac"/>
<circle cx="90" cy="65" r="12" fill="#86efac"/>
<circle cx="124" cy="65" r="12" fill="#86efac"/>
<circle cx="158" cy="65" r="12" fill="#86efac"/>
<text x="190" y="50" font-family="sans-serif" font-size="24" text-anchor="middle">+</text>
<!-- Partial frame (5) -->
<rect x="205" y="5" width="170" height="80" fill="none" stroke="black" stroke-width="2"/>
<line x1="205" y1="45" x2="375" y2="45" stroke="black" stroke-width="1"/>
<line x1="239" y1="5" x2="239" y2="85" stroke="black" stroke-width="1"/>
<line x1="273" y1="5" x2="273" y2="85" stroke="black" stroke-width="1"/>
<line x1="307" y1="5" x2="307" y2="85" stroke="black" stroke-width="1"/>
<line x1="341" y1="5" x2="341" y2="85" stroke="black" stroke-width="1"/>
<circle cx="222" cy="25" r="12" fill="#93c5fd"/>
<circle cx="256" cy="25" r="12" fill="#93c5fd"/>
<circle cx="290" cy="25" r="12" fill="#93c5fd"/>
<circle cx="324" cy="25" r="12" fill="#93c5fd"/>
<circle cx="358" cy="25" r="12" fill="#93c5fd"/>
<text x="190" y="105" font-family="sans-serif" font-size="14" text-anchor="middle" font-weight="bold">10</text>
<text x="290" y="105" font-family="sans-serif" font-size="14" text-anchor="middle" font-weight="bold">+ 5 = 15</text>
</svg>
*Child concludes: "One complete frame is 10, plus 5 more makes 15. So 7 + 8 = 15!"*
This strategy becomes automatic through ten-frame practice. Eventually, students can mentally visualize the frames without seeing them, dramatically improving addition fluency.
## Research Foundations
The use of ten-frames for teaching addition aligns with established learning science:
**Concrete-Representational-Abstract (CRA) Sequence**: Students learn best by progressing from concrete manipulatives → visual representations → abstract symbols. Ten-frames serve as the representational bridge between counting physical objects and working with pure numbers.
**Making Thinking Visible**: Regrouping is an abstract mental process. Ten-frames externalize this process, allowing teachers to see what students understand and where they struggle.
**Cognitive Load Theory**: Visual scaffolds like ten-frames reduce extraneous cognitive load (figuring out how to represent the problem) so students can focus on germane load (understanding the mathematical relationships).
## When to Fade Ten-Frames
The goal isn't to use ten-frames forever—it's to use them as a bridge to abstract understanding. Signs that students are ready to move beyond ten-frames:
1. **Automatic recognition**: They can instantly recognize ten-frame patterns without counting
2. **Mental visualization**: They describe using ten-frames even when not shown ("I made a ten with the 7 and 3")
3. **Fluent regrouping**: They correctly regroup without visual support on most problems
4. **Preference for speed**: They start viewing ten-frames as "extra work" rather than helpful
Our scaffold fading system automates this progression: use the "Less support" difficulty adjustment to reduce ten-frames from "always" → "when regrouping" → "never" as students demonstrate mastery.
## Try It Yourself
Our worksheet generator at **[abaci.one/create/worksheets/addition](https://abaci.one/create/worksheets/addition)** gives you complete control over ten-frame scaffolding:
**For early learners**:
- Set difficulty to "Beginner" or "Early Learner"
- Ten-frames will appear when problems involve regrouping
- Problems start simple to build confidence
**To practice the "make ten" strategy**:
- Use "More support" to set ten-frames to "always"
- Generate problems with moderate regrouping (pAnyStart = 0.5-0.7)
- Students see ten-frames on every problem to build pattern recognition
**To fade scaffolding gradually**:
- Start at "Early Learner" (ten-frames when regrouping)
- Use "Less support" to reduce other scaffolds first (carry boxes, colors)
- Finally use "Less support" again to remove ten-frames
- Students transition smoothly from concrete to abstract
## The Bigger Picture: Adaptive Scaffolding
Ten-frames are one component of our larger scaffolding system. The power comes from **adaptive, conditional scaffolding** that appears exactly when needed:
- Ten-frames when regrouping
- Carry boxes when carrying
- Place value colors for larger numbers
- Answer boxes for alignment practice
This creates worksheets that provide just enough support for each problem's complexity, following Vygotsky's Zone of Proximal Development: challenging enough to promote learning, supported enough to prevent frustration.
## What's Next
We're exploring extensions of the ten-frame approach to:
- **Subtraction with borrowing**: Showing how taking away requires "breaking" a ten
- **Decimal addition**: Using ten-frames to show regrouping across the decimal point
- **Fraction concepts**: Visual representation of part-whole relationships
The code for our ten-frame implementation is **open source**: [github.com/antialias/soroban-abacus-flashcards](https://github.com/antialias/soroban-abacus-flashcards)
See the technical details in our [typstHelpers.ts](https://github.com/antialias/soroban-abacus-flashcards/blob/main/apps/web/src/app/create/worksheets/addition/typstHelpers.ts) file, which generates the ten-frame visualizations.
## Feedback Welcome
We'd love to hear from teachers using ten-frames:
- Are the stacked frames (showing both the overflow and the carry) helpful or confusing?
- Should we add configuration for single vs. double ten-frames?
- What other visual representations would support regrouping?
Share your thoughts via [GitHub issues](https://github.com/antialias/soroban-abacus-flashcards/issues) or try the worksheet generator and let us know how it works with your students.
---
*The ten-frame scaffolding system described here is part of our 2D difficulty research. Students progress through a pedagogically-constrained space where problem complexity and instructional support balance appropriately for each learner. Read more in our [2D difficulty post](/blog/beyond-easy-and-hard).*

View File

@@ -1,306 +0,0 @@
---
title: "The Calculator Won: Why the Abacus Never Reached American Schools"
description: "In Japan, every third-grader learns soroban. In the US, we relegated it to preschool counting toys. Here's what happened—and what we're missing."
author: "Abaci.one Team"
publishedAt: "2025-11-07"
updatedAt: "2025-11-07"
tags: ["education", "history", "soroban", "pedagogy", "mental-math"]
featured: true
---
# The Calculator Won: Why the Abacus Never Reached American Schools
Every Japanese third-grader spends part of their school day manipulating beads on a soroban, the Japanese abacus. They learn to add, subtract, multiply, and divide—first with the physical tool, then by visualizing it mentally. By fifth grade, many can perform multi-digit arithmetic faster than most adults can type numbers into a calculator.
In American schools, the abacus sits in preschool classrooms as a counting toy. We teach kindergarteners to slide colorful beads along wires to learn "1, 2, 3, 4, 5," and then we move on. By third grade, the abacus is gone, replaced by pencil-and-paper algorithms and, increasingly, by calculators.
This divergence wasn't inevitable. The abacus had a foothold in 19th-century American classrooms. But by the mid-20th century, a combination of pedagogical shifts, technological change, and cultural assumptions pushed it to the margins. Today, as a handful of programs attempt to reintroduce soroban training to U.S. students, we're left wondering: what might have been different?
## A Brief, Unremarkable History: The Abacus in America
The counting frame—a horizontal bead rack similar to the Russian *schoty*—arrived in American classrooms in the 1820s, imported from Europe by educational reformers inspired by Pestalozzi's hands-on teaching methods. Throughout the 19th century, it was a common sight: teachers used bead frames to help children visualize numbers, understand place value, and learn basic arithmetic.
But it never became more than a beginner's tool. American math education was moving toward abstract written methods—Hindu-Arabic numerals, pencil-and-paper algorithms, memorization of math facts. The abacus served as training wheels for counting and simple addition, but teachers didn't develop sophisticated calculation techniques with it. By the early 20th century, progressive educators still supported manipulatives for young learners, but the abacus was just one tool among many, and not a particularly special one.
When electronic calculators became affordable in the 1970s, the abacus's fate was sealed. Why teach students to manipulate beads when they could press buttons? The calculator was faster, more accurate, and represented modernity. The abacus, by contrast, seemed antiquated—a relic from a pre-computational age.
**The result:** By the 1980s, the abacus had virtually disappeared from American public school curricula. It survived only in two narrow contexts:
1. **Early childhood education**: As a simple counting manipulative for ages 4-6
2. **Special education for the blind**: The Cranmer abacus remains essential for visually impaired students who can't use pencil and paper
In mainstream elementary education, from first grade onward, the abacus was effectively extinct.
## Meanwhile, in Japan: A Different Path
Japan's experience with the soroban followed an entirely different trajectory. Introduced from China in the 15th century, the soroban was refined over centuries into a sophisticated calculation tool. By the late 19th century, Japanese mathematicians had standardized techniques for addition, subtraction, multiplication, division—even square roots.
In **1938**, Japan's Education Ministry made a decisive move: they formally included soroban operation techniques in the national elementary arithmetic curriculum. This wasn't a suggestion; it was mandated instruction. Every Japanese child would learn to calculate on the abacus.
Even as Japan modernized rapidly—embracing computers, electronics, and cutting-edge technology—the soroban remained in classrooms. When debates arose in the post-war period about whether it was still relevant in the calculator age, the government doubled down. In **1989**, far from phasing out the soroban, the Ministry of Education *expanded* instruction to include both third and fourth grades.
Why? The reasoning wasn't nostalgia. It was pedagogical:
- **Mental calculation skills**: Soroban training develops anzan—the ability to visualize an abacus mentally and perform calculations without a physical tool
- **Number sense**: Constant manipulation of place values builds deep understanding of how numbers work
- **Cognitive benefits**: Research suggests soroban training enhances working memory, concentration, and numerical processing
- **Cultural continuity**: Soroban proficiency is a mark of academic discipline and mathematical skill
Today, Japanese elementary students still receive systematic soroban instruction. After-school soroban academies (*juku*) operate across the country, where children practice for certification exams that millions have taken since 1928. Elite soroban users can add 10-digit numbers in seconds and perform complex mental arithmetic that looks like magic to untrained observers.
## The Cost of the Calculator: What We Gave Up
American education made a bet: calculators would render manual calculation obsolete, so we could skip the tedious bead-pushing and focus on higher-order mathematical thinking.
**The bet hasn't paid off.**
U.S. students consistently struggle with basic arithmetic fluency. National assessments show that many fourth-graders can't reliably add two-digit numbers with regrouping. Mental math is nearly non-existent—ask an adult to compute 47 + 38 without pencil, paper, or phone, and watch them struggle.
This isn't just about speed. **Weak number sense undermines everything that comes later.** If students don't have an intuitive grasp of how numbers combine, decompose, and relate to each other, they'll struggle with fractions, algebra, and every subsequent mathematical concept.
Calculators haven't freed students to focus on "higher-order thinking." Instead, they've created a generation that can't estimate, can't verify answers for reasonableness, and reaches for a device to compute 15% of $40.
The Japanese approach suggests an alternative. **What if calculation fluency isn't a tedious prerequisite to real math, but rather the foundation that makes advanced thinking possible?** What if the hours spent manipulating beads—making tangible the abstract relationships between numbers—builds cognitive infrastructure that no calculator can replace?
## What Japan Has That We Don't
The difference isn't just about soroban vs. no soroban. It's about fundamentally different assumptions about what elementary math education should accomplish.
### Mental Math as a Core Skill
In Japan, mental calculation is an explicit instructional goal. Students are expected to compute multi-digit problems in their heads by visualizing the soroban. This isn't parlor trick memorization—it's systematic training in holding numerical representations in working memory and manipulating them mentally.
American students, by contrast, are rarely taught mental math as a distinct skill. We emphasize written algorithms and, increasingly, calculator use. Mental math is something clever kids figure out on their own, not something we systematically cultivate.
### Calculation as Understanding, Not Just Procedure
When Japanese students use the soroban, they're not just executing algorithms—they're physically manipulating place values, seeing how carries propagate, feeling the structure of multidigit operations. The tool makes the abstract concrete.
American algorithms (the "carrying" method for addition, long division, etc.) are taught as procedures to memorize. Many students execute them correctly without understanding why they work. The soroban forces understanding because you can't manipulate it correctly without grasping the underlying place value logic.
### Patience for Mastery
The Japanese curriculum assumes that students need years of practice to develop true computational fluency. Soroban training starts in third grade and continues through elementary school. After-school programs extend this to thousands of hours of deliberate practice.
American education is impatient. We introduce concepts quickly, provide limited practice, and move on. "They can use a calculator" becomes the escape hatch when fluency doesn't develop.
## The Transformative Potential: What Could Change
Imagine if American elementary schools adopted soroban training comparable to what Japan mandates. What would change?
### 1. Number Sense as a Foundation
Students who spend hundreds of hours manipulating an abacus develop an intuitive understanding of:
- **Place value**: Each column represents a power of ten, visibly distinct
- **Decomposition**: Numbers break apart and recombine fluidly (47 is "4 tens and 7 ones")
- **Magnitude**: Larger numbers require more columns; the scale is visible
- **Operations**: Addition is physically "adding beads"; subtraction is "removing beads"
This isn't abstract knowledge you memorize for a test. It's embodied understanding that makes every subsequent mathematical concept more comprehensible.
### 2. Mental Calculation That Actually Works
The progression from physical soroban to mental visualization (*anzan*) creates a lasting mental tool. Students learn to:
- Hold visual representations in working memory
- Manipulate those representations systematically
- Compute without external aids at remarkable speed
This isn't about competing with calculators—it's about having a reliable internal verification system. Can the student tell if the calculator's answer makes sense? Can they estimate before computing? With mental math fluency, yes.
### 3. Cognitive Benefits Beyond Math
Research on abacus-trained students (primarily from Asia) suggests broader cognitive gains:
- **Working memory**: Holding and manipulating visual-spatial information improves general working memory capacity
- **Concentration**: Hours of focused bead manipulation builds sustained attention
- **Confidence**: Mastery of calculation builds mathematical self-efficacy
One neuroscience review found that abacus-based mental calculation training enhances mathematics ability, working memory, and numerical processing, with measurable changes in brain regions linked to memory and reasoning.
### 4. Equity and Accessibility
The soroban is fundamentally democratic:
- **Low cost**: A durable wooden abacus costs $10-20 and lasts decades
- **No prerequisites**: Any child can start with basic counting
- **Immediate feedback**: You can see if your answer is right by reading the beads
- **Scalable**: One teacher can guide 30 students practicing individually
Compare this to educational software (requires devices, internet, maintenance) or intensive tutoring (expensive, doesn't scale). The soroban is cheap, robust, and proven.
## The Reality Check: Where's the "Revival"?
The research document provided claims a "revival" of abacus education in U.S. schools. Let's be honest about what that means.
**The truth: There is no meaningful revival in American public schools.**
Yes, there are efforts:
- **DRANREF Foundation's Abacus Project** ran pilot programs in D.C., reporting that 89% of students improved math fluency over 12 weeks
- **Private programs** like UCMAS, ALOHA, and others operate after-school centers in major cities
- **Some elementary schools** offer abacus as an optional after-school club
But scale matters. Since 2015, the DRANREF Foundation has reached about 2,000 students and 170 teachers—across the entire United States. For context, there are roughly 35 million elementary school students in the U.S.
The "revival" consists of:
- A handful of pilot programs in individual schools
- Private enrichment centers serving families who can afford them (typically $100-200/month)
- Scattered after-school clubs with limited capacity
**This is not a revival. This is boutique experimentation.**
No state has adopted soroban training in their standards. No major school district has made it part of the core curriculum. Teacher preparation programs don't train educators in soroban instruction. The federal government hasn't funded large-scale research or implementation.
The abacus remains, as it has been for 50 years, marginal in American education.
## Why the Revival Isn't Happening (And Won't Soon)
Several structural barriers prevent widespread adoption:
### 1. Curricular Inertia
American math standards (Common Core, state frameworks) don't mention the abacus beyond kindergarten manipulatives. Changing standards is a slow, politically fraught process. Without standards support, textbook publishers won't create soroban materials, and schools won't adopt them.
### 2. Teacher Training
No U.S. teacher preparation program trains elementary educators in soroban instruction. Even if a school wanted to adopt it, where would they find qualified teachers? You can't mandate something the workforce isn't prepared to teach.
### 3. The Calculator Culture
American culture deeply believes that manual calculation is obsolete. "Why waste time on that when they have calculators?" is the dominant view. Convincing educators, administrators, parents, and policymakers that calculation fluency matters requires shifting a decades-old consensus.
### 4. No Compelling Crisis
Until student math performance becomes a genuine crisis that demands novel solutions, inertia will win. The current approach is "good enough"—we can point to some students who succeed, blame failures on external factors, and continue business as usual.
### 5. Equity Optics
Ironically, the abacus can be dismissed as culturally foreign ("That's an Asian thing") or antiquated ("We're not going back to the 1800s"). Any proposal to adopt techniques from other countries faces suspicion and resistance.
## What It Would Actually Take
If the U.S. wanted to seriously integrate soroban training into elementary math education, here's what would be required:
### Phase 1: Research and Pilot Programs (5 years)
- **Randomized controlled trials** comparing soroban-trained students to matched controls on standardized math assessments
- **Longitudinal studies** tracking cognitive development and long-term outcomes
- **Implementation research** documenting teacher training requirements, student engagement, and cost-effectiveness
- **Curriculum development** aligning soroban instruction with Common Core and state standards
**Estimated cost**: $50-100 million for rigorous research across multiple sites
### Phase 2: Teacher Preparation (10 years)
- **Pre-service training**: Integrate soroban instruction into elementary education programs at universities
- **In-service professional development**: Train current teachers through workshops, coaching, and certification programs
- **Master teacher networks**: Create cohorts of soroban specialists who can mentor peers
**Estimated cost**: $500 million - $1 billion for national-scale training infrastructure
### Phase 3: Curriculum Integration (10 years)
- **Standards revision**: Update state math standards to explicitly include soroban/mental math outcomes
- **Materials development**: Create textbooks, digital tools, and assessment instruments
- **Implementation support**: Provide ongoing coaching and resources to schools
**Estimated cost**: Billions of dollars (standards change is politically expensive; materials development is ongoing)
### Total Timeline: 20-25 years from serious commitment to widespread practice
**This is not happening.** There is no political will, no funding, and no constituency demanding it.
## So What Can We Do?
If a national transformation isn't coming, individual educators and parents still have options.
### For Teachers
- **Experiment in your classroom**: Use soroban as a manipulative for place value and mental math, even if it's not in your curriculum
- **Start an after-school club**: Offer abacus as enrichment for interested students
- **Share results**: Document student progress and share with colleagues to build interest
### For Parents
- **Enroll in private programs**: UCMAS, ALOHA, and similar centers exist in many cities
- **Learn together at home**: Purchase a soroban ($10-20) and use online tutorials (YouTube has many excellent series)
- **Advocate for school adoption**: Ask your school's math department to consider pilot programs
### For Education Leaders
- **Fund pilot studies**: Small-scale experiments in a few schools can generate local evidence
- **Partner with researchers**: University education departments may be interested in studying implementation
- **Connect with existing programs**: The DRANREF Foundation and other nonprofits offer training and materials
### For All of Us: Use Tools Like This One
The website you're on right now—**Abaci.one**—exists because mainstream education isn't meeting this need. We're building open-source tools to make soroban training accessible:
- **Interactive tutorials** for learning to read and manipulate the soroban
- **Practice games** that build mental calculation fluency
- **Printable flashcards** for deliberate practice
- **Research-based pedagogy** grounded in learning science
You don't need to wait for your school district. You don't need to pay $200/month for classes. You can start today, for free, right here.
## The Bigger Picture: What the Abacus Represents
This isn't really about beads and wires. The abacus is a proxy for a deeper question: **What do we believe about how children learn mathematics?**
The American approach assumes:
- Abstract symbolic manipulation is sufficient
- Calculators make manual calculation obsolete
- Speed matters more than understanding
- Innovation means new technology, not old tools refined
The Japanese approach (and that of many Asian countries) assumes:
- Embodied, physical practice builds durable understanding
- Mental calculation develops cognitive skills beyond computation
- Mastery requires thousands of hours of deliberate practice
- Old tools, deeply understood, can be more powerful than new ones superficially adopted
**We chose calculators. They chose soroban. The results speak for themselves.**
International assessments consistently show Japanese students outperforming American peers in mathematical problem-solving, number sense, and computational fluency. This isn't solely because of the soroban—Japan invests heavily in math education overall—but it's a piece of the puzzle we've ignored.
## A Modest Proposal
We're not going to transform American math education overnight. But we could start with something achievable:
**Make mental math a valued, explicitly taught skill in elementary school.**
It doesn't have to be soroban (though that would be great). It could be:
- **Vedic mathematics** (Indian mental math techniques)
- **Number talks** (structured classroom discussions about mental strategies)
- **Estimation routines** (daily practice with reasonable approximation)
- **Rekenrek** (the Dutch arithmetic rack, already used in some U.S. classrooms)
The specific tool matters less than the commitment: **We teach students to compute mentally, systematically, with sufficient practice to develop real fluency.**
If we made this commitment, the soroban would be an obvious candidate for inclusion. It's proven, it's cheap, it's scalable, and it works. But even if we never adopt it, we could learn from the principle it represents: **computational fluency isn't the enemy of conceptual understanding. It's the foundation.**
## Try It Yourself
Don't take my word for it. Spend 20 minutes learning to add two-digit numbers on a soroban. You'll experience:
- How visible place value becomes when each column is physically distinct
- How regrouping (carrying) makes tangible sense when you physically move beads
- How mental visualization naturally develops from physical practice
- Why students trained this way develop both speed and understanding
We've built an interactive tutorial right here at [abaci.one/tutorial](/tutorial). It's free, it's self-paced, and it will convince you—more than any essay could—that there's something powerful here we've been missing.
## The Uncomfortable Truth
The abacus never reached American schools because **we decided it wasn't worth the effort.**
We had it. We used it. And we consciously chose to set it aside when calculators appeared. That choice made sense in the moment—why cling to old tools when new ones are available?
But Japan made a different choice. They kept the soroban, integrated it into their curriculum, and systematically developed their students' mental calculation abilities to levels we can barely imagine.
**Fifty years later, the results are in. They were right. We were wrong.**
The question now is whether we have the humility to admit it, and the will to do something about it.
Probably not. American education is extremely resistant to learning from other countries, especially when it means acknowledging that older methods might be superior to our "modern" approaches.
But maybe, at the margins, some teachers will try. Some parents will enroll their kids. Some students will discover that moving beads on wires unlocks mathematical understanding in a way abstract algorithms never did.
And maybe, in 20 years, we'll have enough of these stories that someone will finally ask: **Why isn't everyone doing this?**
That would be a real revival.
---
## Further Reading
- **Historical context**: Pat Ballew's [blog on the abacus in American education](http://pballew.blogspot.com/) documents its early use and decline
- **Japanese system**: The [League of Japan Abacus Associations](https://www.shuzan.jp/english/) provides history and current practices
- **Cognitive research**: Wang et al. (2020), "Abacus Training Enhances Neural Correlates of Phonological Processing," *Frontiers in Neuroscience*
- **Pilot programs**: [The Abacus Project by DRANREF Foundation](https://www.tdfgives.org/)
---
*This post reflects our conviction at Abaci.one that accessible, evidence-based tools can democratize mathematical fluency. The soroban won't solve every problem in math education, but it's a proven approach we've foolishly ignored. We're building digital tools to make it accessible to everyone—because no student should be denied effective instruction just because their school made the wrong choice 50 years ago.*

View File

@@ -27,7 +27,6 @@ Successfully implemented **all 3 critical architectural improvements** identifie
**Solution**: Accept any string, validate at runtime against validator registry.
**Changes**:
- `arcade-rooms.ts`: `gameName: text('game_name')` (removed enum)
- `arcade-sessions.ts`: `currentGame: text('current_game').notNull()` (removed enum)
- `room-game-configs.ts`: `gameName: text('game_name').notNull()` (removed enum)
@@ -35,7 +34,6 @@ Successfully implemented **all 3 critical architectural improvements** identifie
- Updated settings API to use `isValidGameName()` instead of hardcoded array
**Impact**:
```diff
- BEFORE: Update 3 database schemas + run migration for each game
+ AFTER: No database changes needed - just register validator
@@ -53,38 +51,35 @@ Successfully implemented **all 3 critical architectural improvements** identifie
**Solution**: Move validation to game definitions - games own their validation logic.
**Changes**:
- Added `validateConfig?: (config: unknown) => config is TConfig` to `GameDefinition`
- Updated `defineGame()` to accept and return `validateConfig`
- Added validation to Number Guesser and Math Sprint
- Updated `validateGameConfig()` to call `game.validateConfig()` from registry
**Impact**:
```diff
- BEFORE: Add case to 50-line switch statement in helper file
+ AFTER: Add validateConfig function to game definition
```
**Example**:
```typescript
// In game index.ts
function validateMathSprintConfig(config: unknown): config is MathSprintConfig {
return (
typeof config === "object" &&
typeof config === 'object' &&
config !== null &&
["easy", "medium", "hard"].includes(config.difficulty) &&
typeof config.questionsPerRound === "number" &&
['easy', 'medium', 'hard'].includes(config.difficulty) &&
typeof config.questionsPerRound === 'number' &&
config.questionsPerRound >= 5 &&
config.questionsPerRound <= 20
);
)
}
export const mathSprintGame = defineGame({
// ... other fields
validateConfig: validateMathSprintConfig,
});
})
```
**Files Modified**: 5 files
@@ -96,18 +91,18 @@ export const mathSprintGame = defineGame({
### Adding a New Game
| Task | Before | After (Phase 1-3) |
| --------------------------- | --------------------------------------- | ----------------------------------------- |
| **Database Schemas** | Update 3 enum types | ✅ No changes needed |
| **Settings API** | Add to validGames array | ✅ No changes needed (runtime validation) |
| **Config Helpers** | Add switch case + validation (25 lines) | ✅ No changes needed |
| **Game Config Types** | Manually define interface (10-15 lines) | ✅ One-line type inference |
| **GameConfigByName** | Add entry manually | ✅ Add entry (auto-typed) |
| **RoomGameConfig** | Add optional property | ✅ Auto-derived from GameConfigByName |
| **Default Config** | Add to DEFAULT_X_CONFIG constant | ✔️ Still needed (3-5 lines) |
| **Validator Registry** | Register in validators.ts | ✔️ Still needed (1 line) |
| **Game Registry** | Register in game-registry.ts | ✔️ Still needed (1 line) |
| **validateConfig Function** | N/A | ✔️ Add to game definition (10-15 lines) |
| Task | Before | After (Phase 1-3) |
|------|--------|----------|
| **Database Schemas** | Update 3 enum types | ✅ No changes needed |
| **Settings API** | Add to validGames array | ✅ No changes needed (runtime validation) |
| **Config Helpers** | Add switch case + validation (25 lines) | ✅ No changes needed |
| **Game Config Types** | Manually define interface (10-15 lines) | ✅ One-line type inference |
| **GameConfigByName** | Add entry manually | ✅ Add entry (auto-typed) |
| **RoomGameConfig** | Add optional property | ✅ Auto-derived from GameConfigByName |
| **Default Config** | Add to DEFAULT_X_CONFIG constant | ✔️ Still needed (3-5 lines) |
| **Validator Registry** | Register in validators.ts | ✔️ Still needed (1 line) |
| **Game Registry** | Register in game-registry.ts | ✔️ Still needed (1 line) |
| **validateConfig Function** | N/A | ✔️ Add to game definition (10-15 lines) |
**Total Files to Update**: 12 → **3** (75% reduction)
**Total Lines of Boilerplate**: ~60 lines → ~20 lines (67% reduction)
@@ -115,7 +110,6 @@ export const mathSprintGame = defineGame({
### What's Left
Three items still require manual updates:
1. **Default Config Constants** (`game-configs.ts`) - 3-5 lines per game
2. **Validator Registry** (`validators.ts`) - 1 line per game
3. **Game Registry** (`game-registry.ts`) - 1 line per game
@@ -126,18 +120,15 @@ Three items still require manual updates:
## Migration Impact
### Existing Data
-**No data migration needed** - strings remain strings
-**Backward compatible** - existing games work unchanged
### TypeScript Changes
- ⚠️ Database columns now accept `string` instead of specific enum
- ✅ Runtime validation prevents invalid data
- ✅ Type safety maintained through validator registry
### Developer Experience
```diff
- BEFORE: 15-20 minutes of boilerplate per game
+ AFTER: 2-3 minutes to add validation function
@@ -148,25 +139,21 @@ Three items still require manual updates:
## Architectural Wins
### 1. Single Source of Truth
- ✅ Validator registry is the authoritative list of games
- ✅ All validation checks against registry at runtime
- ✅ No duplication across database/API/helpers
### 2. Self-Contained Games
- ✅ Games define their own validation logic
- ✅ No scattered switch statements
- ✅ Easy to understand - everything in one place
### 3. True Modularity
- ✅ Database schemas accept any registered game
- ✅ API endpoints dynamically validate
- ✅ Helper functions delegate to games
### 4. Developer Friction Reduced
- ✅ No database schema changes
- ✅ No API endpoint updates
- ✅ No helper switch statements
@@ -181,7 +168,6 @@ Three items still require manual updates:
**Solution**: Use TypeScript utility types to infer from game definitions.
**Changes**:
- Added `InferGameConfig<T>` utility type that extracts config from game definitions
- `NumberGuesserGameConfig` now inferred: `InferGameConfig<typeof numberGuesserGame>`
- `MathSprintGameConfig` now inferred: `InferGameConfig<typeof mathSprintGame>`
@@ -189,35 +175,30 @@ Three items still require manual updates:
- Changed `RoomGameConfig` from interface to type for auto-derivation
**Impact**:
```diff
- BEFORE: Manually define interface with 10-15 lines per game
+ AFTER: One-line type inference from game definition
```
**Example**:
```typescript
// Type-only import (won't load React components)
import type { mathSprintGame } from "@/arcade-games/math-sprint";
import type { mathSprintGame } from '@/arcade-games/math-sprint'
// Utility type
type InferGameConfig<T> = T extends { defaultConfig: infer Config }
? Config
: never;
type InferGameConfig<T> = T extends { defaultConfig: infer Config } ? Config : never
// Inferred type (was 6 lines, now 1 line!)
export type MathSprintGameConfig = InferGameConfig<typeof mathSprintGame>;
export type MathSprintGameConfig = InferGameConfig<typeof mathSprintGame>
// Auto-derived RoomGameConfig (was 5 manual entries, now automatic!)
export type RoomGameConfig = {
[K in keyof GameConfigByName]?: GameConfigByName[K];
};
[K in keyof GameConfigByName]?: GameConfigByName[K]
}
```
**Files Modified**: 2 files
**Commits**:
- `271b8ec3 - refactor(arcade): implement Phase 3 - infer config types from game definitions`
- `4c15c13f - docs(arcade): update README with Phase 3 type inference architecture`
@@ -228,7 +209,6 @@ export type RoomGameConfig = {
## Future Work (Optional)
### Phase 4: Extract Config-Only Exports
**Optional improvement**: Create separate `config.ts` files in each game directory that export just config and validation (no React dependencies). This would allow importing default configs directly without duplication.
---
@@ -236,7 +216,6 @@ export type RoomGameConfig = {
## Testing
### Manual Testing
- ✅ Math Sprint works end-to-end
- ✅ Number Guesser works end-to-end
- ✅ Room settings API accepts math-sprint
@@ -244,7 +223,6 @@ export type RoomGameConfig = {
- ✅ TypeScript compilation succeeds
### Test Coverage Needed
- [ ] Unit tests for `isValidGameName()`
- [ ] Unit tests for game `validateConfig()` functions
- [ ] Integration test: Add new game without touching infrastructure
@@ -255,20 +233,17 @@ export type RoomGameConfig = {
## Lessons Learned
### What Worked Well
1. **Incremental Approach** - Fixed one issue at a time
2. **Backward Compatibility** - Legacy games still work
3. **Runtime Validation** - Flexible and extensible
4. **Clear Commit Messages** - Easy to track changes
### Challenges
1. **TypeScript Enums → Runtime Checks** - Required migration strategy
2. **Fallback for Legacy Games** - Switch statement still exists for old games
3. **Type Inference** - Config types still manually defined
### Best Practices Established
1. **Games own validation** - Self-contained, testable
2. **Registry as source of truth** - No duplicate lists
3. **Runtime validation** - Catch errors early with good messages
@@ -281,14 +256,12 @@ export type RoomGameConfig = {
The modular game system is now **significantly improved across all three phases**:
**Before (Phases 1-3)**:
- Must update 12 files to add a game (~60 lines of boilerplate)
- Database migration required for each new game
- Easy to forget a step (manual type definitions, switch statements)
- Scattered validation logic across multiple files
**After (All Phases Complete)**:
- Update 3 files to add a game (75% reduction)
- ~20 lines of boilerplate (67% reduction)
- No database migration needed
@@ -297,14 +270,12 @@ The modular game system is now **significantly improved across all three phases*
- Clear runtime error messages
**Key Achievements**:
1.**Phase 1**: Runtime validation replaces database enums
2.**Phase 2**: Games own their validation logic
3.**Phase 3**: TypeScript types inferred from game definitions
**Remaining Work**:
- Optional Phase 4: Extract config-only exports to eliminate DEFAULT\_\*\_CONFIG duplication
- Optional Phase 4: Extract config-only exports to eliminate DEFAULT_*_CONFIG duplication
- Add comprehensive test suite for validation and type inference
- Migrate legacy games (matching, memory-quiz) to new system
@@ -320,8 +291,8 @@ The architecture is now **production-ready** and can scale to dozens of games wi
4. Register game in `game-registry.ts` (1 line)
5. Add type inference to `game-configs.ts`:
```typescript
import type { myGame } from "@/arcade-games/my-game";
export type MyGameConfig = InferGameConfig<typeof myGame>;
import type { myGame } from '@/arcade-games/my-game'
export type MyGameConfig = InferGameConfig<typeof myGame>
```
6. Add to `GameConfigByName` (1 line - type is auto-inferred!)
7. Add defaults to `game-configs.ts` (3-5 lines)

View File

@@ -23,22 +23,17 @@ The unified validator registry successfully solved the dual registration problem
**Problem**: The `room_game_configs` table schema hard-codes game names, preventing true modularity.
**Evidence**:
```typescript
// db/schema/room-game-configs.ts
gameName: text("game_name").$type<
"matching" | "memory-quiz" | "number-guesser" | "complement-race"
>();
gameName: text('game_name').$type<'matching' | 'memory-quiz' | 'number-guesser' | 'complement-race'>()
```
When adding 'math-sprint':
```
Type '"math-sprint"' is not assignable to type '"matching" | "memory-quiz" | "number-guesser" | "complement-race"'
```
**Impact**:
- ❌ Must manually update database schema for every new game
- ❌ TypeScript errors force schema migration
- ❌ Breaks "just register and go" promise
@@ -59,7 +54,6 @@ Type '"math-sprint"' is not assignable to type '"matching" | "memory-quiz" | "nu
3. `validateGameConfig()` - add validation logic
**Example** (from Math Sprint):
```typescript
// Must add to imports
import { DEFAULT_MATH_SPRINT_CONFIG } from './game-configs'
@@ -79,7 +73,6 @@ case 'math-sprint':
```
**Impact**:
- ⏱️ 5-10 minutes of boilerplate per game
- 🐛 Easy to forget a switch case
- 📝 Repetitive validation logic
@@ -99,40 +92,38 @@ case 'math-sprint':
5. Create `DEFAULT_X_CONFIG` constant
**Example** (from Math Sprint):
```typescript
// 1. Import
import type { Difficulty as MathSprintDifficulty } from "@/arcade-games/math-sprint/types";
import type { Difficulty as MathSprintDifficulty } from '@/arcade-games/math-sprint/types'
// 2. Interface
export interface MathSprintGameConfig {
difficulty: MathSprintDifficulty;
questionsPerRound: number;
timePerQuestion: number;
difficulty: MathSprintDifficulty
questionsPerRound: number
timePerQuestion: number
}
// 3. Add to union
export type GameConfigByName = {
"math-sprint": MathSprintGameConfig;
'math-sprint': MathSprintGameConfig
// ...
};
}
// 4. Add to RoomGameConfig
export interface RoomGameConfig {
"math-sprint"?: MathSprintGameConfig;
'math-sprint'?: MathSprintGameConfig
// ...
}
// 5. Default constant
export const DEFAULT_MATH_SPRINT_CONFIG: MathSprintGameConfig = {
difficulty: "medium",
difficulty: 'medium',
questionsPerRound: 10,
timePerQuestion: 30,
};
}
```
**Impact**:
- ⏱️ 10-15 lines of boilerplate per game
- 🐛 Easy to forget one of the 5 updates
- 🔄 Repeating type information (already in game definition)
@@ -145,16 +136,15 @@ export const DEFAULT_MATH_SPRINT_CONFIG: MathSprintGameConfig = {
**Files Required Per Game**:
| Category | Files | Purpose |
| ---------------- | ------------ | ------------------------------------------------------------- |
| **Game Code** | 7 files | types.ts, Validator.ts, Provider.tsx, index.ts, 3x components |
| **Registration** | 2 files | validators.ts, game-registry.ts |
| **Config** | 2 files | game-configs.ts, game-config-helpers.ts |
| **Database** | 1 file | schema migration |
| **Total** | **12 files** | For one game! |
| Category | Files | Purpose |
|----------|-------|---------|
| **Game Code** | 7 files | types.ts, Validator.ts, Provider.tsx, index.ts, 3x components |
| **Registration** | 2 files | validators.ts, game-registry.ts |
| **Config** | 2 files | game-configs.ts, game-config-helpers.ts |
| **Database** | 1 file | schema migration |
| **Total** | **12 files** | For one game! |
**Lines of Boilerplate** (non-game-logic):
- game-configs.ts: ~15 lines
- game-config-helpers.ts: ~25 lines
- validators.ts: ~2 lines
@@ -162,7 +152,6 @@ export const DEFAULT_MATH_SPRINT_CONFIG: MathSprintGameConfig = {
- **Total: ~44 lines of pure boilerplate per game**
**Comparison**:
- Number Guesser: ~500 lines of actual game logic
- Boilerplate: ~44 lines (8.8% overhead) ✅ Acceptable
- But spread across 4 different files ⚠️ Developer friction
@@ -203,14 +192,12 @@ export const DEFAULT_MATH_SPRINT_CONFIG: MathSprintGameConfig = {
## Comparison: Number Guesser vs. Math Sprint
### Similarities (Good!)
- ✅ Same file structure
- ✅ Same SDK usage patterns
- ✅ Same Provider pattern
- ✅ Same component phases
### Differences (Revealing!)
- Math Sprint uses TEAM_MOVE (no turn owner)
- Math Sprint has server-generated questions
- Database schema didn't support Math Sprint name
@@ -223,13 +210,13 @@ export const DEFAULT_MATH_SPRINT_CONFIG: MathSprintGameConfig = {
### Time to Add a Game
| Task | Time | Notes |
| ------------------------ | ------------- | --------------------------------------- |
| Write game logic | 2-4 hours | Validator, state management, components |
| Registration boilerplate | 15-20 min | 4 files to update |
| Database migration | 10-15 min | Schema update, migration file |
| Debugging type errors | 10-30 min | Database schema mismatches |
| **Total** | **3-5 hours** | For a simple game |
| Task | Time | Notes |
|------|------|-------|
| Write game logic | 2-4 hours | Validator, state management, components |
| Registration boilerplate | 15-20 min | 4 files to update |
| Database migration | 10-15 min | Schema update, migration file |
| Debugging type errors | 10-30 min | Database schema mismatches |
| **Total** | **3-5 hours** | For a simple game |
### Pain Points
@@ -263,27 +250,22 @@ export const DEFAULT_MATH_SPRINT_CONFIG: MathSprintGameConfig = {
**1. Fix Database Schema Coupling**
**Current**:
```typescript
gameName: text("game_name").$type<
"matching" | "memory-quiz" | "number-guesser" | "complement-race"
>();
gameName: text('game_name').$type<'matching' | 'memory-quiz' | 'number-guesser' | 'complement-race'>()
```
**Recommended**:
```typescript
// Accept any string, validate at runtime
gameName: text("game_name").$type<string>().notNull();
gameName: text('game_name').$type<string>().notNull()
// Runtime validation in helper functions
export function validateGameName(gameName: string): gameName is GameName {
return hasValidator(gameName);
return hasValidator(gameName)
}
```
**Benefits**:
- ✅ No schema migration per game
- ✅ Works with auto-derived GameName
- ✅ Runtime validation is sufficient
@@ -293,7 +275,6 @@ export function validateGameName(gameName: string): gameName is GameName {
**2. Infer Config Types from Game Definitions**
**Current** (manual):
```typescript
// In game-configs.ts
export interface MathSprintGameConfig { ... }
@@ -304,25 +285,23 @@ const defaultConfig: MathSprintGameConfig = { ... }
```
**Recommended**:
```typescript
// In game definition (single source of truth)
export const mathSprintGame = defineGame({
defaultConfig: {
difficulty: "medium",
difficulty: 'medium',
questionsPerRound: 10,
timePerQuestion: 30,
},
validator: mathSprintValidator,
// ...
});
})
// Auto-infer types
type MathSprintConfig = typeof mathSprintGame.defaultConfig;
type MathSprintConfig = typeof mathSprintGame.defaultConfig
```
**Benefits**:
- ✅ No duplication
- ✅ Single source of truth
- ✅ Type inference handles it
@@ -332,18 +311,16 @@ type MathSprintConfig = typeof mathSprintGame.defaultConfig;
**3. Move Config Validation to Game Definition**
**Current** (switch statement in helper):
```typescript
function validateGameConfig(gameName: GameName, config: any): boolean {
switch (gameName) {
case "math-sprint":
return; /* 15 lines of validation */
case 'math-sprint':
return /* 15 lines of validation */
}
}
```
**Recommended**:
```typescript
// In game definition
export const mathSprintGame = defineGame({
@@ -362,7 +339,6 @@ export function validateGameConfig(gameName: GameName, config: any): boolean {
```
**Benefits**:
- ✅ No switch statement
- ✅ Validation lives with game
- ✅ One place to update
@@ -378,14 +354,12 @@ npm run create-game math-sprint "Math Sprint" "🧮"
```
Generates:
- File structure
- Boilerplate code
- Registration entries
- Types
**Benefits**:
- ✅ Eliminates manual boilerplate
- ✅ Consistent structure
- ✅ Reduces errors
@@ -395,7 +369,6 @@ Generates:
**5. Add Runtime Registry Validation**
On app start, verify:
- ✅ All games in registry have validators
- ✅ All validators have games
- ✅ No orphaned configs
@@ -403,12 +376,12 @@ On app start, verify:
```typescript
function validateRegistries() {
const games = getAllGames();
const validators = getRegisteredGameNames();
const games = getAllGames()
const validators = getRegisteredGameNames()
for (const game of games) {
if (!validators.includes(game.manifest.name)) {
throw new Error(`Game ${game.manifest.name} has no validator!`);
throw new Error(`Game ${game.manifest.name} has no validator!`)
}
}
}
@@ -418,16 +391,16 @@ function validateRegistries() {
## Updated Compliance Table
| Intention | Status | Notes |
| ----------------- | ---------- | ------------------------------------------------------ |
| Modularity | ⚠️ Partial | Validators unified, but database/config not modular |
| Self-registration | ✅ Pass | Two registration points (validator + game), both clear |
| Type safety | ⚠️ Partial | Types work, but database schema breaks for new games |
| No core changes | ⚠️ Partial | Must update 4 files + database schema |
| Drop-in games | ❌ Fail | Database migration required |
| Stable SDK API | ✅ Pass | SDK is excellent |
| Clear patterns | ✅ Pass | Patterns are consistent |
| Low boilerplate | ⚠️ Partial | SDK usage is clean, registration is verbose |
| Intention | Status | Notes |
|-----------|--------|-------|
| Modularity | ⚠️ Partial | Validators unified, but database/config not modular |
| Self-registration | ✅ Pass | Two registration points (validator + game), both clear |
| Type safety | ⚠️ Partial | Types work, but database schema breaks for new games |
| No core changes | ⚠️ Partial | Must update 4 files + database schema |
| Drop-in games | ❌ Fail | Database migration required |
| Stable SDK API | ✅ Pass | SDK is excellent |
| Clear patterns | ✅ Pass | Patterns are consistent |
| Low boilerplate | ⚠️ Partial | SDK usage is clean, registration is verbose |
**Overall Grade**: **B-** (Was B+, downgraded after implementation testing)
@@ -438,20 +411,17 @@ function validateRegistries() {
### What We Learned
**The Good**:
- SDK design is solid
- Unified validator registry works
- Pattern is consistent and learnable
- Number Guesser proves the concept
⚠️ **The Not-So-Good**:
- Database schema couples to game names (critical blocker)
- Config system has too much boilerplate
- 12 files touched per game is high
**The Bad**:
- Can't truly "drop in" a game without schema migration
- Config types are duplicated
- Helper switch statements are tedious
@@ -459,13 +429,11 @@ function validateRegistries() {
### Verdict
The system **works** and is **usable**, but falls short of "modular architecture" goals due to:
1. Database schema hard-coding
2. Config system boilerplate
3. Required schema migrations
**Recommendation**:
1. **Option A (Quick Fix)**: Document the 12-file checklist, live with boilerplate for now
2. **Option B (Proper Fix)**: Implement Critical recommendations 1-3 before adding Math Sprint
@@ -480,3 +448,4 @@ The system **works** and is **usable**, but falls short of "modular architecture
3. 🟡 Test Math Sprint with current architecture
4. 🟡 Evaluate if boilerplate is acceptable in practice
5. 🟢 Consider config system refactoring for later

View File

@@ -25,7 +25,6 @@ The modular game system **now meets its stated intentions** after implementing t
> "A modular, plugin-based architecture for building multiplayer arcade games"
>
> **Goals:**
>
> 1. **Modularity**: Each game is self-contained and independently deployable
> 2. Games register themselves with a central registry
> 3. No need to modify core infrastructure when adding games
@@ -48,7 +47,6 @@ The modular game system **now meets its stated intentions** after implementing t
2. **Server Validator Map** (`src/lib/arcade/validation/index.ts`)
**Impact**:
- ❌ Broke modularity - couldn't just drop in a new game
- ❌ Easy to forget one registration, causing runtime errors
- ❌ Violated DRY principle
@@ -61,17 +59,16 @@ Created unified isomorphic validator registry at `src/lib/arcade/validators.ts`:
```typescript
export const validatorRegistry = {
matching: matchingGameValidator,
"memory-quiz": memoryQuizGameValidator,
"number-guesser": numberGuesserValidator,
'memory-quiz': memoryQuizGameValidator,
'number-guesser': numberGuesserValidator,
// Add new games here - GameName type auto-updates!
} as const;
} as const
// Auto-derived type - no manual updates needed!
export type GameName = keyof typeof validatorRegistry;
export type GameName = keyof typeof validatorRegistry
```
**Changes Made**:
1. ✅ Created `src/lib/arcade/validators.ts` - Unified validator registry (isomorphic)
2. ✅ Updated `validation/index.ts` - Now re-exports from unified registry (backwards compatible)
3. ✅ Updated `validation/types.ts` - GameName now auto-derived (no more hard-coded union)
@@ -82,7 +79,6 @@ export type GameName = keyof typeof validatorRegistry;
8. ✅ Updated `game-registry.ts` - Added runtime validation check
**Benefits**:
- ✅ Single registration point for validators
- ✅ Auto-derived GameName type (no manual updates)
- ✅ Type-safe validator access
@@ -100,13 +96,11 @@ export type GameName = keyof typeof validatorRegistry;
**Resolution**: Created separate isomorphic validator registry that server can import without pulling in client-only code.
**How It Works Now**:
- `src/lib/arcade/validators.ts` - Isomorphic, server can import safely
- `src/lib/arcade/game-registry.ts` - Client-only, imports React components
- Both use the same validator instances (verified at runtime)
**Benefits**:
- ✅ Server has direct access to validators
- ✅ No need for dual validator maps
- ✅ Clear separation: validators (isomorphic) vs UI (client-only)
@@ -118,31 +112,23 @@ export type GameName = keyof typeof validatorRegistry;
**Problem**: Multiple overlapping type definitions for same concepts:
**GameValidator** has THREE definitions:
1. `validation/types.ts` - Legacy validator interface
2. `game-sdk/types.ts` - SDK validator interface (extends legacy)
3. Individual game validators - Implement one or both?
**GameMove** has TWO type systems:
1. `validation/types.ts` - Legacy move types (MatchingFlipCardMove, etc.)
2. Game-specific types in each game's `types.ts`
**GameName** is hard-coded:
```typescript
// validation/types.ts:9
export type GameName =
| "matching"
| "memory-quiz"
| "complement-race"
| "number-guesser";
export type GameName = 'matching' | 'memory-quiz' | 'complement-race' | 'number-guesser'
```
This must be manually updated for every new game!
**Impact**:
- Confusing which types to use
- Easy to use wrong import
- GameName type doesn't auto-update from registry
@@ -154,7 +140,6 @@ This must be manually updated for every new game!
**Problem**: Existing games (matching, memory-quiz) still use old structure:
**Old Pattern** (matching, memory-quiz):
```
src/app/arcade/matching/
├── context/ (Old pattern)
@@ -163,7 +148,6 @@ src/app/arcade/matching/
```
**New Pattern** (number-guesser):
```
src/arcade-games/number-guesser/
├── index.ts (New pattern)
@@ -173,14 +157,12 @@ src/arcade-games/number-guesser/
```
**Impact**:
- Inconsistent codebase structure
- Two different patterns developers must understand
- Documentation shows new pattern, but most games use old pattern
- Confusing for new developers
**Evidence**:
- `src/app/arcade/matching/` - Uses old structure
- `src/app/arcade/memory-quiz/` - Uses old structure
- `src/arcade-games/number-guesser/` - Uses new structure
@@ -197,17 +179,16 @@ src/arcade-games/number-guesser/
// src/lib/arcade/validators.ts
export const validatorRegistry = {
matching: matchingGameValidator,
"memory-quiz": memoryQuizGameValidator,
"number-guesser": numberGuesserValidator,
'memory-quiz': memoryQuizGameValidator,
'number-guesser': numberGuesserValidator,
// Add new games here...
} as const;
} as const
// Auto-derived! No manual updates needed!
export type GameName = keyof typeof validatorRegistry;
export type GameName = keyof typeof validatorRegistry
```
**Benefits**:
- ✅ GameName type updates automatically when adding to registry
- ✅ Impossible to forget type update (it's derived)
- ✅ Single registration step (just add to validatorRegistry)
@@ -222,13 +203,11 @@ export type GameName = keyof typeof validatorRegistry;
**Problem**: Server code cannot import `game-registry.ts` because it contains React components.
**Why**:
- `GameDefinition` includes `Provider` and `GameComponent` (React components)
- Server-side code runs in Node.js, can't import React components
- No way to access just the validator from registry
**Potential Solutions**:
1. Split registry into isomorphic and client-only parts
2. Separate validator registration from game registration
3. Use conditional exports in package.json
@@ -240,11 +219,9 @@ export type GameName = keyof typeof validatorRegistry;
**Problem**: Documentation describes a fully modular system, but reality requires manual edits in multiple places.
**From README.md**:
> "Step 7: Register Game - Add to src/lib/arcade/game-registry.ts"
**Missing Steps**:
- Also add to `validation/index.ts` validator map
- Also add to `GameName` type union
- Import validator in server files
@@ -258,11 +235,10 @@ export type GameName = keyof typeof validatorRegistry;
**Problem**: Registration is type-safe but has no runtime validation:
```typescript
registerGame(numberGuesserGame); // No validation that validator works
registerGame(numberGuesserGame) // No validation that validator works
```
**Missing Checks**:
- Does validator implement all required methods?
- Does manifest match expected schema?
- Are all required fields present?
@@ -280,53 +256,51 @@ registerGame(numberGuesserGame); // No validation that validator works
```typescript
// src/lib/arcade/validators.ts (NEW FILE - isomorphic)
import { numberGuesserValidator } from "@/arcade-games/number-guesser/Validator";
import { matchingGameValidator } from "@/lib/arcade/validation/MatchingGameValidator";
import { numberGuesserValidator } from '@/arcade-games/number-guesser/Validator'
import { matchingGameValidator } from '@/lib/arcade/validation/MatchingGameValidator'
// ... other validators
export const validatorRegistry = new Map([
["number-guesser", numberGuesserValidator],
["matching", matchingGameValidator],
['number-guesser', numberGuesserValidator],
['matching', matchingGameValidator],
// ...
]);
])
export function getValidator(gameName: string) {
const validator = validatorRegistry.get(gameName);
if (!validator) throw new Error(`No validator for game: ${gameName}`);
return validator;
const validator = validatorRegistry.get(gameName)
if (!validator) throw new Error(`No validator for game: ${gameName}`)
return validator
}
export type GameName = keyof typeof validatorRegistry; // Auto-derived!
export type GameName = keyof typeof validatorRegistry // Auto-derived!
```
**Update game-registry.ts** to use this:
```typescript
// src/lib/arcade/game-registry.ts
import { getValidator } from "./validators";
import { getValidator } from './validators'
export function registerGame(game: GameDefinition) {
const { name } = game.manifest;
const { name } = game.manifest
// Verify validator is registered server-side
const validator = getValidator(name);
const validator = getValidator(name)
if (validator !== game.validator) {
console.warn(`[Registry] Validator mismatch for ${name}`);
console.warn(`[Registry] Validator mismatch for ${name}`)
}
registry.set(name, game);
registry.set(name, game)
}
```
**Pros**:
- Single source of truth for validators
- Auto-derived GameName type
- Client and server use same validator
- Only one registration needed
**Cons**:
- Still requires manual import in validators.ts
- Doesn't solve "drop in a game" fully
@@ -343,13 +317,11 @@ export function registerGame(game: GameDefinition) {
```
**Pros**:
- Truly modular - just add folder, run build
- No manual registration
- Auto-derived types
**Cons**:
- Build-time complexity
- Magic (harder to understand)
- May not work with all bundlers
@@ -363,33 +335,29 @@ export function registerGame(game: GameDefinition) {
```typescript
// Isomorphic (client + server)
export interface GameValidatorDefinition {
name: string;
validator: GameValidator;
defaultConfig: GameConfig;
name: string
validator: GameValidator
defaultConfig: GameConfig
}
// Client-only
export interface GameUIDefinition {
name: string;
manifest: GameManifest;
Provider: GameProviderComponent;
GameComponent: GameComponent;
name: string
manifest: GameManifest
Provider: GameProviderComponent
GameComponent: GameComponent
}
// Combined (client-only)
export interface GameDefinition
extends GameValidatorDefinition,
GameUIDefinition {}
export interface GameDefinition extends GameValidatorDefinition, GameUIDefinition {}
```
**Pros**:
- Clear separation of concerns
- Server can import just validator definition
- Type-safe
**Cons**:
- More complexity
- Still requires two registries
@@ -464,13 +432,11 @@ export interface GameDefinition
### Migration Path
**Option A: Big Bang** (Risky)
- Migrate all games to new structure in one PR
- Update server to use unified registry
- High risk of breakage
**Option B: Incremental** (Safer)
- Document dual registration as "current reality"
- Create unified validator registry (doesn't break old games)
- Slowly migrate old games one by one
@@ -482,15 +448,15 @@ export interface GameDefinition
## Compliance with Intentions
| Intention | Status | Notes |
| ----------------- | ----------- | ------------------------------------------------------- |
| Modularity | ✅ Pass | Single registration in validators.ts + game-registry.ts |
| Self-registration | ✅ Pass | Both client and server use unified registry |
| Type safety | ✅ Pass | Good TypeScript coverage + auto-derived GameName |
| No core changes | ⚠️ Improved | Must edit validators.ts, but one central file |
| Drop-in games | ⚠️ Improved | Two registration points (validator + game def) |
| Stable SDK API | ✅ Pass | SDK is well-designed and consistent |
| Clear patterns | ⚠️ Partial | New pattern is clear, but old games don't follow it |
| Intention | Status | Notes |
|-----------|--------|-------|
| Modularity | ✅ Pass | Single registration in validators.ts + game-registry.ts |
| Self-registration | ✅ Pass | Both client and server use unified registry |
| Type safety | ✅ Pass | Good TypeScript coverage + auto-derived GameName |
| No core changes | ⚠️ Improved | Must edit validators.ts, but one central file |
| Drop-in games | ⚠️ Improved | Two registration points (validator + game def) |
| Stable SDK API | ✅ Pass | SDK is well-designed and consistent |
| Clear patterns | ⚠️ Partial | New pattern is clear, but old games don't follow it |
**Original Grade**: **D** (Failed core modularity requirement)
**Current Grade**: **B+** (Modularity achieved, some legacy migration pending)
@@ -543,8 +509,7 @@ The modular game system has a **solid foundation** but is **not truly modular**
---
_This audit was conducted by reviewing:_
*This audit was conducted by reviewing:*
- `src/lib/arcade/game-registry.ts`
- `src/lib/arcade/validation/index.ts`
- `src/lib/arcade/session-manager.ts`

View File

@@ -19,7 +19,6 @@ This playbook provides step-by-step instructions for migrating a legacy arcade g
## Prerequisites
Before starting, ensure:
- [ ] You understand the Game SDK architecture (`/src/arcade-games/README.md`)
- [ ] You've read `ARCHITECTURAL_IMPROVEMENTS.md`
- [ ] The game is currently working in production
@@ -34,7 +33,6 @@ Before starting, ensure:
Create a migration plan document (`docs/[GAME_NAME]_MIGRATION_PLAN.md`) with:
**Current File Structure**:
```
List all files the game currently uses:
- State types
@@ -46,7 +44,6 @@ List all files the game currently uses:
```
**Current State Shape**:
```typescript
// Document the current state interface
interface CurrentGameState {
@@ -57,7 +54,6 @@ interface CurrentGameState {
```
**Current Move Types**:
```typescript
// Document all move types
type CurrentGameMove =
@@ -66,7 +62,6 @@ type CurrentGameMove =
```
**Current Config**:
```typescript
// Document game configuration
interface CurrentGameConfig {
@@ -77,26 +72,22 @@ interface CurrentGameConfig {
### Step 1.2: Identify Validator Status
**Check if validator exists**:
```bash
grep -r "YourGameValidator" src/lib/arcade/validation/
```
**Status**:
-**Validator exists**: Migration is easier (skip validator creation)
-**No validator**: Need to create one (add 2-3 hours)
### Step 1.3: Assess Current Provider Pattern
**Which pattern does the game use?**
- **Reducer Pattern**: Has `reducer.ts`, uses `useReducer`, dispatches actions
- **ArcadeSession Pattern**: Uses `useArcadeSession`, sends moves
- **Hybrid**: Uses both (uh oh)
**How many providers?**
- **Single Provider**: One provider (room mode only)
- **Dual Providers**: Separate `LocalProvider.tsx` and `RoomProvider.tsx` (both will be replaced)
@@ -105,7 +96,6 @@ grep -r "YourGameValidator" src/lib/arcade/validation/
### Step 1.4: Identify Complexity Factors
Rate each factor (Low/Medium/High):
- [ ] **UI State Complexity**: Animations, keyboard state, timeouts
- [ ] **Timing Requirements**: Card sequences, turn timers, synchronization
- [ ] **Input Handling**: Real-time input, debouncing, local optimization
@@ -113,7 +103,6 @@ Rate each factor (Low/Medium/High):
- [ ] **Configuration Persistence**: Settings saved per room/game
**Complexity Score**:
- 0-2 High: **Easy** (2-4 hours)
- 3-4 High: **Medium** (4-6 hours)
- 5+ High: **Hard** (6-8+ hours)
@@ -130,7 +119,6 @@ mkdir -p src/arcade-games/[game-name]/components
```
**Directory structure**:
```
src/arcade-games/[game-name]/
├── index.ts # Game definition (will create)
@@ -148,14 +136,12 @@ src/arcade-games/[game-name]/
### Step 2.2: Copy Validator
**If validator exists**:
```bash
cp src/lib/arcade/validation/[Game]Validator.ts \
src/arcade-games/[game-name]/Validator.ts
```
**Update imports** in the validator:
```diff
- import type { SomeOldType } from '@/app/arcade/[game]'
+ import type { SomeNewType } from './types'
@@ -175,10 +161,8 @@ cp src/app/arcade/[game-name]/types.ts \
### Step 2.4: Document Migration Checklist
In your migration plan, add:
```markdown
## Migration Checklist
- [ ] Phase 1: Analysis complete
- [ ] Phase 2: Preparation complete
- [ ] Phase 3: Game definition created
@@ -197,7 +181,6 @@ In your migration plan, add:
### Step 3.1: Create Manifest
**Option A: YAML file** (recommended):
```yaml
# src/arcade-games/[game-name]/game.yaml
name: game-name
@@ -220,13 +203,12 @@ available: true
```
**Option B: Inline object**:
```typescript
const manifest: GameManifest = {
name: "game-name",
displayName: "Game Display Name",
name: 'game-name',
displayName: 'Game Display Name',
// ... all fields from above
};
}
```
### Step 3.2: Define Default Config
@@ -236,7 +218,7 @@ const defaultConfig: YourGameConfig = {
setting1: defaultValue1,
setting2: defaultValue2,
// ... all game settings with sensible defaults
};
}
```
### Step 3.3: Create Config Validator
@@ -244,18 +226,18 @@ const defaultConfig: YourGameConfig = {
```typescript
function validateYourGameConfig(config: unknown): config is YourGameConfig {
return (
typeof config === "object" &&
typeof config === 'object' &&
config !== null &&
// Check each field exists
"setting1" in config &&
"setting2" in config &&
'setting1' in config &&
'setting2' in config &&
// Validate each field's type and value
typeof (config as any).setting1 === "string" &&
typeof (config as any).setting2 === "number" &&
typeof (config as any).setting1 === 'string' &&
typeof (config as any).setting2 === 'number' &&
// Validate constraints
(config as any).setting2 >= 1 &&
(config as any).setting2 <= 100
);
)
}
```
@@ -263,80 +245,75 @@ function validateYourGameConfig(config: unknown): config is YourGameConfig {
```typescript
// src/arcade-games/[game-name]/index.ts
import { defineGame } from "@/lib/arcade/game-sdk";
import type { GameManifest } from "@/lib/arcade/game-sdk";
import { GameComponent } from "./components/GameComponent";
import { YourGameProvider } from "./Provider";
import type { YourGameConfig, YourGameMove, YourGameState } from "./types";
import { yourGameValidator } from "./Validator";
import { defineGame } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { GameComponent } from './components/GameComponent'
import { YourGameProvider } from './Provider'
import type { YourGameConfig, YourGameMove, YourGameState } from './types'
import { yourGameValidator } from './Validator'
const manifest: GameManifest = {
// ... manifest from Step 3.1
};
}
const defaultConfig: YourGameConfig = {
// ... config from Step 3.2
};
}
function validateYourGameConfig(config: unknown): config is YourGameConfig {
// ... validator from Step 3.3
}
export const yourGame = defineGame<YourGameConfig, YourGameState, YourGameMove>(
{
manifest,
Provider: YourGameProvider,
GameComponent,
validator: yourGameValidator,
defaultConfig,
validateConfig: validateYourGameConfig,
},
);
export const yourGame = defineGame<YourGameConfig, YourGameState, YourGameMove>({
manifest,
Provider: YourGameProvider,
GameComponent,
validator: yourGameValidator,
defaultConfig,
validateConfig: validateYourGameConfig,
})
```
### Step 3.5: Register Game
**In `src/lib/arcade/game-registry.ts`**:
```typescript
import { yourGame } from "@/arcade-games/[game-name]";
import { yourGame } from '@/arcade-games/[game-name]'
registerGame(yourGame);
registerGame(yourGame)
```
**In `src/lib/arcade/validators.ts`**:
```typescript
import { yourGameValidator } from "@/arcade-games/[game-name]/Validator";
import { yourGameValidator } from '@/arcade-games/[game-name]/Validator'
export const validatorRegistry = {
// ... other games
"game-name": yourGameValidator,
} as const;
'game-name': yourGameValidator,
} as const
```
### Step 3.6: Add Type Inference
**In `src/lib/arcade/game-configs.ts`**:
```typescript
// Add type-only import
import type { yourGame } from "@/arcade-games/[game-name]";
import type { yourGame } from '@/arcade-games/[game-name]'
// Infer config type
export type YourGameConfig = InferGameConfig<typeof yourGame>;
export type YourGameConfig = InferGameConfig<typeof yourGame>
// Add to GameConfigByName
export type GameConfigByName = {
// ... other games
"game-name": YourGameConfig;
};
'game-name': YourGameConfig,
}
// Add default config constant
export const DEFAULT_YOUR_GAME_CONFIG: YourGameConfig = {
setting1: defaultValue1,
setting2: defaultValue2,
};
}
```
---
@@ -347,24 +324,24 @@ export const DEFAULT_YOUR_GAME_CONFIG: YourGameConfig = {
```typescript
// src/arcade-games/[game-name]/types.ts
import type { GameConfig, GameState, GameMove } from "@/lib/arcade/game-sdk";
import type { GameConfig, GameState, GameMove } from '@/lib/arcade/game-sdk'
export interface YourGameConfig extends GameConfig {
setting1: string;
setting2: number;
setting1: string
setting2: number
// ... all game settings
}
export interface YourGameState extends GameState {
// Game-specific fields
gamePhase: "setup" | "playing" | "results";
gamePhase: 'setup' | 'playing' | 'results'
// Multiplayer fields (from GameState)
activePlayers: string[];
playerMetadata: Record<string, PlayerMetadata>;
activePlayers: string[]
playerMetadata: Record<string, PlayerMetadata>
// Your game's custom fields
score: Record<string, number>;
score: Record<string, number>
// ...
}
```
@@ -372,32 +349,30 @@ export interface YourGameState extends GameState {
### Step 4.2: Update Move Types
**Ensure all moves have required fields**:
```typescript
export type YourGameMove =
| {
type: "START_GAME";
playerId: string; // Required
userId: string; // Required
timestamp: number; // Added by SDK
type: 'START_GAME'
playerId: string // Required
userId: string // Required
timestamp: number // Added by SDK
data: {
activePlayers: string[];
playerMetadata: Record<string, any>;
};
activePlayers: string[]
playerMetadata: Record<string, any>
}
}
| {
type: "MAKE_MOVE";
playerId: string;
userId: string;
timestamp: number;
type: 'MAKE_MOVE'
playerId: string
userId: string
timestamp: number
data: {
// Move-specific data
};
};
}
}
```
**Key Requirements**:
- ✅ Every move must have: `playerId`, `userId`, `timestamp`, `data`
- ✅ Use `TEAM_MOVE` constant for moves where specific player doesn't matter
-`data` is always an object (never primitive)
@@ -405,7 +380,7 @@ export type YourGameMove =
### Step 4.3: Export Types
```typescript
export type { PlayerMetadata } from "@/lib/arcade/player-ownership.client";
export type { PlayerMetadata } from '@/lib/arcade/player-ownership.client'
// ... any other needed types
```
@@ -534,33 +509,31 @@ const setConfig = useCallback(
(field: keyof YourGameConfig, value: any) => {
// Send move to update state immediately
sendMove({
type: "SET_CONFIG",
type: 'SET_CONFIG',
playerId: TEAM_MOVE,
userId: viewerId || "",
userId: viewerId || '',
data: { field, value },
});
})
// Persist to database (always - room mode only)
if (roomData?.id) {
const currentGameConfig =
(roomData.gameConfig as Record<string, any>) || {};
const currentConfig =
(currentGameConfig["game-name"] as Record<string, any>) || {};
const currentGameConfig = (roomData.gameConfig as Record<string, any>) || {}
const currentConfig = (currentGameConfig['game-name'] as Record<string, any>) || {}
updateGameConfig({
roomId: roomData.id,
gameConfig: {
...currentGameConfig,
"game-name": {
'game-name': {
...currentConfig,
[field]: value,
},
},
});
})
}
},
[viewerId, sendMove, roomData, updateGameConfig],
);
[viewerId, sendMove, roomData, updateGameConfig]
)
```
### Step 5.3: Handle Local-Only State
@@ -569,27 +542,24 @@ For UI state that doesn't need network sync:
```typescript
// Example: Input field that updates every keystroke
const [localInput, setLocalInput] = useState("");
const [localInput, setLocalInput] = useState('')
// Merge with network state
const mergedState = {
...state,
currentInput: localInput, // Override with local value
};
}
// Only send to network when submitting
const submitAnswer = useCallback(
(answer: string) => {
sendMove({
type: "SUBMIT_ANSWER",
playerId: state.currentPlayer,
userId: viewerId || "",
data: { answer },
});
setLocalInput(""); // Clear local state
},
[state.currentPlayer, viewerId, sendMove],
);
const submitAnswer = useCallback((answer: string) => {
sendMove({
type: 'SUBMIT_ANSWER',
playerId: state.currentPlayer,
userId: viewerId || '',
data: { answer },
})
setLocalInput('') // Clear local state
}, [state.currentPlayer, viewerId, sendMove])
```
---
@@ -639,7 +609,6 @@ export function GameComponent() {
### Step 6.2: Update Phase Components
**Pattern for each component**:
```typescript
import { useYourGame } from '../Provider'
@@ -657,7 +626,6 @@ export function YourPhase() {
```
**Common Changes**:
```diff
- const { state, dispatch } = useYourGame()
+ const { state, actionName } = useYourGame()
@@ -783,7 +751,6 @@ npm run pre-commit
```
**Must pass**:
- [ ] TypeScript compilation (0 errors)
- [ ] Format check (all files formatted)
- [ ] Lint check (0 errors, 0 warnings)
@@ -817,17 +784,14 @@ rm src/lib/arcade/validation/[Game]Validator.ts
### Step 9.2: Update Documentation
**In `ARCHITECTURAL_IMPROVEMENTS.md`**:
```markdown
### Migrated Games
- ✅ Number Guesser (v4.0.0)
- ✅ Math Sprint (v4.0.1)
- ✅ Memory Quiz (v4.1.0) - Completed 2025-01-16
```
**In your migration plan**:
```markdown
## Migration Complete ✅
@@ -870,7 +834,6 @@ git push
**Symptom**: Lots of type errors in components
**Solution**:
1. Check that move types include `playerId`, `userId`, `timestamp`
2. Verify state extends `GameState` from SDK
3. Update component imports to use new provider location
@@ -880,7 +843,6 @@ git push
**Symptom**: Players see different states
**Solution**:
1. Verify `roomId` is passed to `useArcadeSession` (should always be present)
2. Check validator is handling moves correctly
3. Ensure `applyMove` returns state (doesn't mutate)
@@ -891,7 +853,6 @@ git push
**Symptom**: Settings reset on page reload
**Solution**:
1. Check `useUpdateGameConfig` is called in `setConfig`
2. Verify config is scoped by game name: `gameConfig['game-name']`
3. Ensure `initialState` useMemo depends on `roomData`
@@ -901,7 +862,6 @@ git push
**Symptom**: Validator rejects all moves
**Solution**:
1. Check move type strings match validator switch cases
2. Verify validator is registered correctly
3. Check move structure includes all required fields
@@ -911,7 +871,6 @@ git push
**Symptom**: useContext errors or null reference errors
**Solution**:
1. Verify Provider wraps all components
2. Check context exports match usage
3. Ensure hooks are called within Provider
@@ -924,70 +883,67 @@ If your game doesn't have a validator, create one:
```typescript
// src/arcade-games/[game-name]/Validator.ts
import type { GameValidator, ValidationResult } from "@/lib/arcade/game-sdk";
import type { YourGameState, YourGameMove } from "./types";
import type { GameValidator, ValidationResult } from '@/lib/arcade/game-sdk'
import type { YourGameState, YourGameMove } from './types'
export class YourGameValidator
implements GameValidator<YourGameState, YourGameMove>
{
export class YourGameValidator implements GameValidator<YourGameState, YourGameMove> {
validateMove(
state: YourGameState,
move: YourGameMove,
context?: { userId?: string },
context?: { userId?: string }
): ValidationResult {
switch (move.type) {
case "START_GAME":
return this.validateStartGame(state, move.data);
case 'START_GAME':
return this.validateStartGame(state, move.data)
case "MAKE_MOVE":
return this.validateMakeMove(state, move.playerId, move.data);
case 'MAKE_MOVE':
return this.validateMakeMove(state, move.playerId, move.data)
// ... handle each move type
default:
return { valid: false, error: "Unknown move type" };
return { valid: false, error: 'Unknown move type' }
}
}
private validateStartGame(state: YourGameState, data: any): ValidationResult {
// Check preconditions
if (state.gamePhase !== "setup") {
return { valid: false, error: "Can only start from setup phase" };
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only start from setup phase' }
}
// Create new state
const newState: YourGameState = {
...state,
gamePhase: "playing",
gamePhase: 'playing',
activePlayers: data.activePlayers || [],
playerMetadata: data.playerMetadata || {},
// ... initialize game state
};
}
return { valid: true, newState };
return { valid: true, newState }
}
// ... more validation methods
isGameComplete(state: YourGameState): boolean {
return state.gamePhase === "results";
return state.gamePhase === 'results'
}
getInitialState(config: YourGameConfig): YourGameState {
return {
gamePhase: "setup",
gamePhase: 'setup',
activePlayers: [],
playerMetadata: {},
// ... all initial state from config
};
}
}
}
export const yourGameValidator = new YourGameValidator();
export const yourGameValidator = new YourGameValidator()
```
**Key Principles**:
1. ✅ Validator is authoritative (client is display only)
2. ✅ Always return new state (never mutate)
3. ✅ Validate all preconditions before state changes
@@ -1003,15 +959,15 @@ export const yourGameValidator = new YourGameValidator();
```typescript
// Validator
if (move.playerId !== state.currentPlayer) {
return { valid: false, error: "Not your turn" };
return { valid: false, error: 'Not your turn' }
}
// After valid move, rotate turn
const nextPlayerIndex = (currentIndex + 1) % state.activePlayers.length;
const nextPlayerIndex = (currentIndex + 1) % state.activePlayers.length
const newState = {
...state,
currentPlayer: state.activePlayers[nextPlayerIndex],
};
}
```
### Pattern: Team Moves
@@ -1019,20 +975,20 @@ const newState = {
```typescript
// Use TEAM_MOVE for moves where any player can act
sendMove({
type: "SUBMIT_ANSWER",
type: 'SUBMIT_ANSWER',
playerId: TEAM_MOVE, // Not attributed to specific player
userId: viewerId || "",
userId: viewerId || '',
data: { answer },
});
})
```
### Pattern: Timed Actions
```typescript
// Store timestamp in state, check elapsed time
const elapsedTime = Date.now() - state.startTime;
const elapsedTime = Date.now() - state.startTime
if (elapsedTime > state.timeLimit) {
return { valid: false, error: "Time expired" };
return { valid: false, error: 'Time expired' }
}
```
@@ -1040,9 +996,9 @@ if (elapsedTime > state.timeLimit) {
```typescript
// Track scores by userId (not playerId) for multi-character games
const newScores = { ...state.scores };
const newScores = { ...state.scores }
if (move.userId) {
newScores[move.userId] = (newScores[move.userId] || 0) + points;
newScores[move.userId] = (newScores[move.userId] || 0) + points
}
```
@@ -1053,7 +1009,6 @@ if (move.userId) {
Use this as a final verification before declaring migration complete:
### Code Quality
- [ ] TypeScript compiles with 0 errors
- [ ] Linter passes with 0 errors, 0 warnings
- [ ] Formatter has been run
@@ -1061,7 +1016,6 @@ Use this as a final verification before declaring migration complete:
- [ ] No console warnings in terminal
### Architecture
- [ ] Game definition created with `defineGame()`
- [ ] Validator implements `GameValidator` interface
- [ ] Provider uses SDK hooks exclusively
@@ -1069,14 +1023,12 @@ Use this as a final verification before declaring migration complete:
- [ ] Config validation function exists
### Registration
- [ ] Game registered in `game-registry.ts`
- [ ] Validator registered in `validators.ts`
- [ ] Config type inferred in `game-configs.ts`
- [ ] Default config constant added
### Functionality
- [ ] All game phases work in local mode
- [ ] All game phases work in room mode
- [ ] Settings persist in room mode
@@ -1085,7 +1037,6 @@ Use this as a final verification before declaring migration complete:
- [ ] Can exit and re-enter game
### Documentation
- [ ] Migration plan created
- [ ] Breaking changes documented
- [ ] Testing results recorded
@@ -1098,7 +1049,6 @@ Use this as a final verification before declaring migration complete:
You've now successfully migrated a game to the modular platform! 🎉
The game is now:
- ✅ Self-contained in `/arcade-games/[game-name]/`
- ✅ Using the Game SDK for all functionality
- ✅ Room mode only (no local mode code)
@@ -1108,7 +1058,6 @@ The game is now:
- ✅ Easier to maintain and extend
**Next Steps**:
1. Monitor game in production for any issues
2. Consider adding game-specific settings
3. Migrate the next legacy game!
@@ -1116,7 +1065,6 @@ The game is now:
---
**Questions or issues?** Refer to:
- Game SDK Docs: `/src/arcade-games/README.md`
- Architecture Docs: `/docs/ARCHITECTURAL_IMPROVEMENTS.md`
- Example Games: Number Guesser, Math Sprint, Memory Quiz

View File

@@ -11,7 +11,6 @@
**Canonical Location**: `/src/app/arcade/matching/` is clearly the more advanced, feature-complete version.
**Key Findings**:
- Arcade version has pause/resume, networked presence, better player ownership
- Utils are **identical** between locations (can use either)
- **ResultsPhase.tsx** needs manual merge (arcade layout + games Performance Analysis)
@@ -24,91 +23,74 @@
### Components
#### 1. GameCard.tsx
**Differences**: Arcade has helper function `getPlayerIndex()` to reduce code duplication
**Decision**: ✅ Use arcade version (better code organization)
#### 2. PlayerStatusBar.tsx
**Differences**:
- Arcade: Distinguishes "Your turn" vs "Their turn" based on player ownership
- Arcade: Uses `useViewerId()` for authorization
- Games: Shows only "Your turn" for all players
**Decision**: ✅ Use arcade version (more feature-complete)
**Decision**: ✅ Use arcade version (more feature-complete)
#### 3. ResultsPhase.tsx
**Differences**:
- Arcade: Modern responsive layout, exits via `exitSession()` to `/arcade`
- Games: Has unique "Performance Analysis" section (strengths/improvements)
- Games: Simple navigation to `/games`
**Decision**: ⚠️ MERGE REQUIRED
**Decision**: ⚠️ MERGE REQUIRED
- Keep arcade's layout, navigation, responsive design
- **Add** Performance Analysis section from games version (lines 245-317)
#### 4. SetupPhase.tsx
**Differences**:
- Arcade: Full pause/resume with config change warnings
- Arcade: Uses action creators (setGameType, setDifficulty, setTurnTimer)
- Arcade: Sophisticated "Resume Game" vs "Start Game" button logic
- Games: Simple dispatch pattern, no pause/resume
**Decision**: ✅ Use arcade version (much more advanced)
**Decision**: ✅ Use arcade version (much more advanced)
#### 5. EmojiPicker.tsx
**Differences**: None (files identical)
**Decision**: ✅ Use arcade version (same as games)
#### 6. GamePhase.tsx
**Differences**:
- Arcade: Passes hoverCard, viewerId, gameMode to MemoryGrid
- Arcade: `enableMultiplayerPresence={true}`
- Games: No multiplayer presence features
**Decision**: ✅ Use arcade version (has networked presence)
**Decision**: ✅ Use arcade version (has networked presence)
#### 7. MemoryPairsGame.tsx
**Differences**:
- Arcade: Provides onExitSession, onSetup, onNewGame callbacks
- Arcade: Uses router for navigation
- Games: Simple component with just gameName prop
**Decision**: ✅ Use arcade version (better integration)
**Decision**: ✅ Use arcade version (better integration)
### Utilities
#### 1. cardGeneration.ts
**Differences**: None (files identical)
**Decision**: ✅ Use arcade version (same as games)
#### 2. matchValidation.ts
**Differences**: None (files identical)
**Decision**: ✅ Use arcade version (same as games)
#### 3. gameScoring.ts
**Differences**: None (files identical)
**Decision**: ✅ Use arcade version (same as games)
### Context/Types
#### types.ts
**Differences**:
- Arcade: PlayerMetadata properly typed (vs `any` in games)
- Arcade: Better documentation for pause/resume state
- Arcade: Hover state not optional (`playerHovers: {}` vs `playerHovers?: {}`)
- Arcade: More complete MemoryPairsContextValue interface
**Decision**: ✅ Use arcade version (better types)
**Decision**: ✅ Use arcade version (better types)
---
@@ -140,7 +122,6 @@ Found **7 imports** that reference `/games/matching/`:
## Migration Strategy
### Canonical Source
**Use**: `/src/app/arcade/matching/` as the base for all files
**Exception**: Merge Performance Analysis from `/src/app/games/matching/components/ResultsPhase.tsx`
@@ -148,7 +129,6 @@ Found **7 imports** that reference `/games/matching/`:
### Files to Move (from `/src/app/arcade/matching/`)
**Components** (7 files):
- ✅ GameCard.tsx (as-is)
- ✅ PlayerStatusBar.tsx (as-is)
- ⚠️ ResultsPhase.tsx (merge with games version)
@@ -158,39 +138,33 @@ Found **7 imports** that reference `/games/matching/`:
- ✅ MemoryPairsGame.tsx (as-is)
**Utils** (3 files):
- ✅ cardGeneration.ts (as-is)
- ✅ matchValidation.ts (as-is)
- ✅ gameScoring.ts (as-is)
**Context**:
- ✅ types.ts (as-is)
- ✅ RoomMemoryPairsProvider.tsx (convert to modular Provider)
**Tests**:
- ✅ EmojiPicker.test.tsx
- ✅ playerMetadata-userId.test.ts
### Files to Delete (after migration)
**From `/src/app/arcade/matching/`** (~13 files):
- Components: 7 files + 1 test (move, then delete old location)
- Context: LocalMemoryPairsProvider.tsx, MemoryPairsContext.tsx, index.ts
- Utils: 3 files (move, then delete old location)
- page.tsx (replace with redirect)
**From `/src/app/games/matching/`** (~14 files):
- Components: 7 files + 2 tests (delete)
- Context: 2 files (delete)
- Utils: 3 files (delete)
- page.tsx (replace with redirect)
**Validator**:
- `/src/lib/arcade/validation/MatchingGameValidator.ts` (move to modular location)
**Total files to delete**: ~27 files
@@ -200,59 +174,46 @@ Found **7 imports** that reference `/games/matching/`:
## Special Merge: ResultsPhase.tsx
### Keep from Arcade Version
- Responsive layout (padding, fontSize with base/md breakpoints)
- Modern stat cards design
- exitSession() navigation to /arcade
- Better button styling with gradients
### Add from Games Version
Lines 245-317: Performance Analysis section
```tsx
{
/* Performance Analysis */
}
<div
className={css({
background: "rgba(248, 250, 252, 0.8)",
padding: "30px",
borderRadius: "16px",
marginBottom: "40px",
border: "1px solid rgba(226, 232, 240, 0.8)",
maxWidth: "600px",
margin: "0 auto 40px auto",
})}
>
<h3
className={css({
fontSize: "24px",
marginBottom: "20px",
color: "gray.800",
})}
>
{/* Performance Analysis */}
<div className={css({
background: 'rgba(248, 250, 252, 0.8)',
padding: '30px',
borderRadius: '16px',
marginBottom: '40px',
border: '1px solid rgba(226, 232, 240, 0.8)',
maxWidth: '600px',
margin: '0 auto 40px auto',
})}>
<h3 className={css({
fontSize: '24px',
marginBottom: '20px',
color: 'gray.800',
})}>
Performance Analysis
</h3>
{analysis.strengths.length > 0 && (
<div className={css({ marginBottom: "20px" })}>
<h4
className={css({
fontSize: "18px",
color: "green.600",
marginBottom: "8px",
})}
>
<div className={css({ marginBottom: '20px' })}>
<h4 className={css({
fontSize: '18px',
color: 'green.600',
marginBottom: '8px',
})}>
Strengths:
</h4>
<ul
className={css({
textAlign: "left",
color: "gray.700",
lineHeight: "1.6",
})}
>
<ul className={css({
textAlign: 'left',
color: 'gray.700',
lineHeight: '1.6',
})}>
{analysis.strengths.map((strength, index) => (
<li key={index}>{strength}</li>
))}
@@ -262,29 +223,25 @@ Lines 245-317: Performance Analysis section
{analysis.improvements.length > 0 && (
<div>
<h4
className={css({
fontSize: "18px",
color: "orange.600",
marginBottom: "8px",
})}
>
<h4 className={css({
fontSize: '18px',
color: 'orange.600',
marginBottom: '8px',
})}>
💡 Areas for Improvement:
</h4>
<ul
className={css({
textAlign: "left",
color: "gray.700",
lineHeight: "1.6",
})}
>
<ul className={css({
textAlign: 'left',
color: 'gray.700',
lineHeight: '1.6',
})}>
{analysis.improvements.map((improvement, index) => (
<li key={index}>{improvement}</li>
))}
</ul>
</div>
)}
</div>;
</div>
```
**Note**: Need to ensure `analysis` variable is computed (may already exist in arcade version from `analyzePerformance` utility)
@@ -297,7 +254,6 @@ Lines 245-317: Performance Analysis section
**Status**: ✅ Comprehensive and complete (570 lines)
**Handles all move types**:
- FLIP_CARD (with turn validation, player ownership)
- START_GAME
- CLEAR_MISMATCH
@@ -322,15 +278,12 @@ Lines 245-317: Performance Analysis section
## Risks Identified
### Risk 1: Performance Analysis Feature Loss
**Mitigation**: Must manually merge Performance Analysis from games/ResultsPhase.tsx
### Risk 2: Import References
**Mitigation**: 7 files import from games/matching - systematic update required
### Risk 3: Test Coverage
**Mitigation**: Move tests with components, verify they still pass
---
@@ -338,7 +291,6 @@ Lines 245-317: Performance Analysis section
## Conclusion
Phase 1 audit complete. Clear path forward:
- **Arcade version is canonical** for all files
- **Utils are identical** - no conflicts
- **One manual merge required** (ResultsPhase Performance Analysis)

View File

@@ -12,7 +12,6 @@
This document outlines the migration plan for **Matching Pairs Battle** (aka Memory Pairs Challenge) from the legacy dual-location architecture to the modern modular game system using the Game SDK.
**Key Complexity Factors**:
- **Dual Location**: Game exists in BOTH `/src/app/arcade/matching/` AND `/src/app/games/matching/`
- **Partial Migration**: RoomMemoryPairsProvider already uses `useArcadeSession` but not in modular format
- **Turn-Based Multiplayer**: More complex than memory-quiz (requires turn validation, player ownership)
@@ -26,7 +25,6 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
### Location 1: `/src/app/arcade/matching/`
**Components** (4 files):
- `components/GameCard.tsx`
- `components/PlayerStatusBar.tsx`
- `components/ResultsPhase.tsx`
@@ -37,7 +35,6 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
- `components/__tests__/EmojiPicker.test.tsx`
**Context** (4 files):
- `context/MemoryPairsContext.tsx` - Context definition and hook
- `context/LocalMemoryPairsProvider.tsx` - Local mode provider (DEPRECATED)
- `context/RoomMemoryPairsProvider.tsx` - Room mode provider (PARTIALLY MIGRATED)
@@ -46,19 +43,16 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
- `context/__tests__/playerMetadata-userId.test.ts` - Test for player ownership
**Utils** (3 files):
- `utils/cardGeneration.ts` - Card generation logic
- `utils/gameScoring.ts` - Scoring calculations
- `utils/matchValidation.ts` - Match validation logic
**Page**:
- `page.tsx` - Route handler for `/arcade/matching`
### Location 2: `/src/app/games/matching/`
**Components** (6 files - DUPLICATES):
- `components/GameCard.tsx`
- `components/PlayerStatusBar.tsx`
- `components/ResultsPhase.tsx`
@@ -70,18 +64,15 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
- `components/PlayerStatusBar.stories.tsx` - Storybook story
**Context** (2 files):
- `context/MemoryPairsContext.tsx`
- `context/types.ts`
**Utils** (3 files - DUPLICATES):
- `utils/cardGeneration.ts`
- `utils/gameScoring.ts`
- `utils/matchValidation.ts`
**Page**:
- `page.tsx` - Route handler for `/games/matching` (legacy?)
### Shared Components
@@ -105,7 +96,6 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
### Complexity: **HIGH** (8/10)
**Reasons**:
1. **Dual Locations**: Must consolidate two separate implementations
2. **Partial Migration**: RoomMemoryPairsProvider uses useArcadeSession but not in modular format
3. **Turn-Based Logic**: Player ownership validation, turn switching
@@ -117,7 +107,6 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
**Similar To**: Memory Quiz migration (same pattern)
**Unique Challenges**:
- Consolidating duplicate files from two locations
- Deciding which version of duplicates is canonical
- Handling `/games/matching/` route (deprecate or redirect?)
@@ -132,7 +121,6 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
**Goal**: Understand current state and identify discrepancies
**Tasks**:
- [x] Map all files in both locations
- [ ] Compare duplicate files to identify differences (e.g., `diff /src/app/arcade/matching/components/GameCard.tsx /src/app/games/matching/components/GameCard.tsx`)
- [ ] Identify which location is canonical (likely `/src/app/arcade/matching/` based on RoomProvider)
@@ -140,7 +128,6 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
- [ ] Check for references to `/games/matching/` route
**Deliverables**:
- File comparison report
- Decision: Which duplicate files to keep
- List of files to delete
@@ -152,62 +139,54 @@ This document outlines the migration plan for **Matching Pairs Battle** (aka Mem
**Goal**: Define game in registry following SDK pattern
**Tasks**:
1. Create `/src/arcade-games/matching/index.ts` with `defineGame()`
2. Register in `/src/lib/arcade/game-registry.ts`
3. Update `/src/lib/arcade/validators.ts` to import from new location
4. Add type inference to `/src/lib/arcade/game-configs.ts`
**Template**:
```typescript
// /src/arcade-games/matching/index.ts
import type { GameManifest, GameConfig } from "@/lib/arcade/game-sdk/types";
import { defineGame } from "@/lib/arcade/game-sdk";
import { MatchingProvider } from "./Provider";
import { MemoryPairsGame } from "./components/MemoryPairsGame";
import { matchingGameValidator } from "./Validator";
import { validateMatchingConfig } from "./config-validation";
import type { MatchingConfig, MatchingState, MatchingMove } from "./types";
import type { GameManifest, GameConfig } from '@/lib/arcade/game-sdk/types'
import { defineGame } from '@/lib/arcade/game-sdk'
import { MatchingProvider } from './Provider'
import { MemoryPairsGame } from './components/MemoryPairsGame'
import { matchingGameValidator } from './Validator'
import { validateMatchingConfig } from './config-validation'
import type { MatchingConfig, MatchingState, MatchingMove } from './types'
const manifest: GameManifest = {
name: "matching",
displayName: "Matching Pairs Battle",
icon: "⚔️",
description: "Multiplayer memory battle with friends",
longDescription:
"Battle friends in epic memory challenges. Match pairs faster than your opponents in this exciting multiplayer experience.",
name: 'matching',
displayName: 'Matching Pairs Battle',
icon: '⚔️',
description: 'Multiplayer memory battle with friends',
longDescription: 'Battle friends in epic memory challenges. Match pairs faster than your opponents in this exciting multiplayer experience.',
maxPlayers: 4,
difficulty: "Intermediate",
chips: ["👥 Multiplayer", "🎯 Strategic", "🏆 Competitive"],
color: "purple",
gradient: "linear-gradient(135deg, #e9d5ff, #ddd6fe)",
borderColor: "purple.200",
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)',
borderColor: 'purple.200',
available: true,
};
}
const defaultConfig: MatchingConfig = {
gameType: "abacus-numeral",
gameType: 'abacus-numeral',
difficulty: 6,
turnTimer: 30,
};
}
export const matchingGame = defineGame<
MatchingConfig,
MatchingState,
MatchingMove
>({
export const matchingGame = defineGame<MatchingConfig, MatchingState, MatchingMove>({
manifest,
Provider: MatchingProvider,
GameComponent: MemoryPairsGame,
validator: matchingGameValidator,
defaultConfig,
validateConfig: validateMatchingConfig,
});
})
```
**Files Modified**:
- `/src/arcade-games/matching/index.ts` (new)
- `/src/lib/arcade/game-registry.ts` (add import + register)
- `/src/lib/arcade/validators.ts` (update import path)
@@ -220,7 +199,6 @@ export const matchingGame = defineGame<
**Goal**: Move validator to modular game directory
**Tasks**:
1. Move `/src/lib/arcade/validation/MatchingGameValidator.ts``/src/arcade-games/matching/Validator.ts`
2. Update imports to use local types from `./types` instead of importing from game-configs (avoid circular deps)
3. Verify all move types are handled
@@ -229,7 +207,6 @@ export const matchingGame = defineGame<
**Note**: Validator looks comprehensive already - likely minimal changes needed
**Files Modified**:
- `/src/arcade-games/matching/Validator.ts` (moved)
- Update imports in validator
@@ -240,7 +217,6 @@ export const matchingGame = defineGame<
**Goal**: Create SDK-compatible type definitions in modular location
**Tasks**:
1. Compare types from both locations:
- `/src/app/arcade/matching/context/types.ts`
- `/src/app/games/matching/context/types.ts`
@@ -252,112 +228,71 @@ export const matchingGame = defineGame<
4. Fix any `{}``Record<string, never>` warnings
**Move Types**:
```typescript
export interface MatchingConfig extends GameConfig {
gameType: "abacus-numeral" | "complement-pairs";
difficulty: 6 | 8 | 12 | 15;
turnTimer: number;
gameType: 'abacus-numeral' | 'complement-pairs'
difficulty: 6 | 8 | 12 | 15
turnTimer: number
}
export interface MatchingState {
// Core game data
cards: GameCard[];
gameCards: GameCard[];
flippedCards: GameCard[];
cards: GameCard[]
gameCards: GameCard[]
flippedCards: GameCard[]
// Config
gameType: "abacus-numeral" | "complement-pairs";
difficulty: 6 | 8 | 12 | 15;
turnTimer: number;
gameType: 'abacus-numeral' | 'complement-pairs'
difficulty: 6 | 8 | 12 | 15
turnTimer: number
// Progression
gamePhase: "setup" | "playing" | "results";
currentPlayer: string;
matchedPairs: number;
totalPairs: number;
moves: number;
scores: Record<string, number>;
activePlayers: string[];
playerMetadata: Record<string, PlayerMetadata>;
consecutiveMatches: Record<string, number>;
gamePhase: 'setup' | 'playing' | 'results'
currentPlayer: string
matchedPairs: number
totalPairs: number
moves: number
scores: Record<string, number>
activePlayers: string[]
playerMetadata: Record<string, PlayerMetadata>
consecutiveMatches: Record<string, number>
// Timing
gameStartTime: number | null;
gameEndTime: number | null;
currentMoveStartTime: number | null;
timerInterval: NodeJS.Timeout | null;
gameStartTime: number | null
gameEndTime: number | null
currentMoveStartTime: number | null
timerInterval: NodeJS.Timeout | null
// UI state
celebrationAnimations: CelebrationAnimation[];
isProcessingMove: boolean;
showMismatchFeedback: boolean;
lastMatchedPair: [string, string] | null;
celebrationAnimations: CelebrationAnimation[]
isProcessingMove: boolean
showMismatchFeedback: boolean
lastMatchedPair: [string, string] | null
// Pause/Resume
originalConfig?: {
gameType: "abacus-numeral" | "complement-pairs";
difficulty: 6 | 8 | 12 | 15;
turnTimer: number;
};
pausedGamePhase?: "setup" | "playing" | "results";
pausedGameState?: PausedGameState;
gameType: 'abacus-numeral' | 'complement-pairs'
difficulty: 6 | 8 | 12 | 15
turnTimer: number
}
pausedGamePhase?: 'setup' | 'playing' | 'results'
pausedGameState?: PausedGameState
// Hover state
playerHovers: Record<string, string | null>;
playerHovers: Record<string, string | null>
}
export type MatchingMove =
| {
type: "FLIP_CARD";
playerId: string;
userId: string;
data: { cardId: string };
}
| {
type: "START_GAME";
playerId: string;
userId: string;
data: {
cards: GameCard[];
activePlayers: string[];
playerMetadata: Record<string, PlayerMetadata>;
};
}
| {
type: "CLEAR_MISMATCH";
playerId: string;
userId: string;
data: Record<string, never>;
}
| {
type: "GO_TO_SETUP";
playerId: string;
userId: string;
data: Record<string, never>;
}
| {
type: "SET_CONFIG";
playerId: string;
userId: string;
data: { field: "gameType" | "difficulty" | "turnTimer"; value: any };
}
| {
type: "RESUME_GAME";
playerId: string;
userId: string;
data: Record<string, never>;
}
| {
type: "HOVER_CARD";
playerId: string;
userId: string;
data: { cardId: string | null };
};
| { type: 'FLIP_CARD'; playerId: string; userId: string; data: { cardId: string } }
| { type: 'START_GAME'; playerId: string; userId: string; data: { cards: GameCard[]; activePlayers: string[]; playerMetadata: Record<string, PlayerMetadata> } }
| { type: 'CLEAR_MISMATCH'; playerId: string; userId: string; data: Record<string, never> }
| { type: 'GO_TO_SETUP'; playerId: string; userId: string; data: Record<string, never> }
| { type: 'SET_CONFIG'; playerId: string; userId: string; data: { field: 'gameType' | 'difficulty' | 'turnTimer'; value: any } }
| { type: 'RESUME_GAME'; playerId: string; userId: string; data: Record<string, never> }
| { type: 'HOVER_CARD'; playerId: string; userId: string; data: { cardId: string | null } }
```
**Files Created**:
- `/src/arcade-games/matching/types.ts`
---
@@ -367,7 +302,6 @@ export type MatchingMove =
**Goal**: Convert RoomMemoryPairsProvider to modular Provider using SDK
**Tasks**:
1. Copy RoomMemoryPairsProvider as starting point (already uses useArcadeSession)
2. Create `/src/arcade-games/matching/Provider.tsx`
3. Remove dependency on MemoryPairsContext (will export its own hook)
@@ -386,13 +320,11 @@ export type MatchingMove =
7. Export `useMatching` hook
**Key Changes**:
- Import types from `./types` not from context
- Export hook: `export function useMatching() { return useContext(MatchingContext) }`
- Ensure hooks called before early returns (React rules)
**Files Created**:
- `/src/arcade-games/matching/Provider.tsx`
---
@@ -402,14 +334,12 @@ export type MatchingMove =
**Goal**: Move components to modular location, choosing canonical versions
**Decision Process** (for each component):
1. If files are identical → pick either (prefer `/src/app/arcade/matching/`)
2. If files differ → manually merge, keeping best of both
3. Update imports to use new Provider: `from '@/arcade-games/matching/Provider'`
4. Fix styled-system import paths (4 levels: `../../../../styled-system/css`)
**Components to Move**:
- GameCard.tsx
- PlayerStatusBar.tsx
- ResultsPhase.tsx
@@ -419,16 +349,13 @@ export type MatchingMove =
- MemoryPairsGame.tsx
**Shared Components** (leave in place):
- `/src/components/matching/HoverAvatar.tsx`
- `/src/components/matching/MemoryGrid.tsx`
**Tests**:
- Move test to `/src/arcade-games/matching/components/__tests__/EmojiPicker.test.tsx`
**Files Created**:
- `/src/arcade-games/matching/components/*.tsx` (7 files)
- `/src/arcade-games/matching/components/__tests__/EmojiPicker.test.tsx`
@@ -439,7 +366,6 @@ export type MatchingMove =
**Goal**: Consolidate utils in modular location
**Tasks**:
1. Compare utils from both locations (likely identical)
2. Move to `/src/arcade-games/matching/utils/`
- `cardGeneration.ts`
@@ -448,7 +374,6 @@ export type MatchingMove =
3. Update imports in components and validator
**Files Created**:
- `/src/arcade-games/matching/utils/*.ts` (3 files)
---
@@ -460,7 +385,6 @@ export type MatchingMove =
**Tasks**:
**Route Updates**:
1. `/src/app/arcade/matching/page.tsx` - Replace with redirect to `/arcade` (local mode deprecated)
2. `/src/app/games/matching/page.tsx` - Replace with redirect to `/arcade` (legacy route)
3. Remove from `GAMES_CONFIG` in `/src/components/GameSelector.tsx`
@@ -468,7 +392,6 @@ export type MatchingMove =
5. Update `/src/lib/arcade/validation/types.ts` imports (if referencing old types)
**Delete Legacy Files** (~30 files):
- `/src/app/arcade/matching/components/` (7 files + 1 test)
- `/src/app/arcade/matching/context/` (5 files + 1 test)
- `/src/app/arcade/matching/utils/` (3 files)
@@ -478,7 +401,6 @@ export type MatchingMove =
- `/src/lib/arcade/validation/MatchingGameValidator.ts` (moved)
**Files Modified**:
- `/src/app/arcade/matching/page.tsx` (redirect)
- `/src/app/games/matching/page.tsx` (redirect)
- `/src/components/GameSelector.tsx` (remove from GAMES_CONFIG)
@@ -509,7 +431,6 @@ After migration, verify:
## Migration Steps Summary
**8 Phases**:
1. ✅ Pre-Migration Audit - Compare duplicate files
2. ⏳ Create Modular Game Definition - Registry + types
3. ⏳ Move and Update Validator - Move to new location
@@ -537,22 +458,18 @@ After migration, verify:
## Risks and Mitigation
### Risk 1: File Divergence
**Risk**: Duplicate files may have different features/fixes
**Mitigation**: Manually diff each duplicate pair, merge best of both
### Risk 2: Test Breakage
**Risk**: PlayerMetadata test may break during migration
**Mitigation**: Run tests frequently, update test if needed
### Risk 3: Turn Logic Complexity
**Risk**: Player ownership and turn validation is complex
**Mitigation**: Validator already handles this - trust existing logic
### Risk 4: Unknown Dependencies
**Risk**: Other parts of codebase may depend on `/games/matching/`
**Mitigation**: Search for imports before deletion: `grep -r "from.*games/matching" src/`

View File

@@ -9,7 +9,6 @@
## Executive Summary
Migrate the Memory Lightning game from the legacy architecture to the new modular game platform. This game is unique because:
- ✅ Already has a validator (`MemoryQuizGameValidator`)
- ✅ Already uses `useArcadeSession` in room mode
- ❌ Located in `/app/arcade/memory-quiz/` instead of `/arcade-games/`
@@ -24,7 +23,6 @@ Migrate the Memory Lightning game from the legacy architecture to the new modula
## Current Architecture
### File Structure
```
src/app/arcade/memory-quiz/
├── page.tsx # Main page (local mode)
@@ -52,7 +50,6 @@ src/lib/arcade/validation/
**⚠️ Local Mode Deprecated**: This migration only supports room mode. All games must be played in a room (even solo play is a single-player room). No local/offline mode code should be included.
### Current State Type (`SorobanQuizState`)
```typescript
interface SorobanQuizState {
// Core game data
@@ -93,31 +90,26 @@ interface SorobanQuizState {
```
### Current Move Types
```typescript
type MemoryQuizGameMove =
| {
type: "START_QUIZ";
data: { numbers: number[]; activePlayers; playerMetadata };
}
| { type: "NEXT_CARD" }
| { type: "SHOW_INPUT_PHASE" }
| { type: "ACCEPT_NUMBER"; data: { number: number } }
| { type: "REJECT_NUMBER" }
| { type: "SET_INPUT"; data: { input: string } }
| { type: "SHOW_RESULTS" }
| { type: "RESET_QUIZ" }
| { type: "SET_CONFIG"; data: { field; value } };
| { type: 'START_QUIZ'; data: { numbers: number[], activePlayers, playerMetadata } }
| { type: 'NEXT_CARD' }
| { type: 'SHOW_INPUT_PHASE' }
| { type: 'ACCEPT_NUMBER'; data: { number: number } }
| { type: 'REJECT_NUMBER' }
| { type: 'SET_INPUT'; data: { input: string } }
| { type: 'SHOW_RESULTS' }
| { type: 'RESET_QUIZ' }
| { type: 'SET_CONFIG'; data: { field, value } }
```
### Current Config
```typescript
interface MemoryQuizGameConfig {
selectedCount: 2 | 5 | 8 | 12 | 15;
displayTime: number;
selectedDifficulty: "beginner" | "easy" | "medium" | "hard" | "expert";
playMode: "cooperative" | "competitive";
selectedCount: 2 | 5 | 8 | 12 | 15
displayTime: number
selectedDifficulty: 'beginner' | 'easy' | 'medium' | 'hard' | 'expert'
playMode: 'cooperative' | 'competitive'
}
```
@@ -126,7 +118,6 @@ interface MemoryQuizGameConfig {
## Target Architecture
### New File Structure
```
src/arcade-games/memory-quiz/ # NEW location
├── index.ts # Game definition (defineGame)
@@ -145,7 +136,6 @@ src/arcade-games/memory-quiz/ # NEW location
```
### New Provider Pattern
- ✅ Single provider (room mode only)
- ✅ Uses `useArcadeSession` with `roomId` (always provided)
- ✅ Uses Game SDK hooks (`useViewerId`, `useRoomData`, `useGameMode`)
@@ -157,7 +147,6 @@ src/arcade-games/memory-quiz/ # NEW location
## Migration Steps
### Phase 1: Preparation (1 hour)
**Goal**: Set up new structure without breaking existing game
1. ✅ Create `/src/arcade-games/memory-quiz/` directory
@@ -172,13 +161,10 @@ src/arcade-games/memory-quiz/ # NEW location
---
### Phase 2: Create Game Definition (1 hour)
**Goal**: Define the game using `defineGame()` helper
**Steps**:
1. Create `game.yaml` manifest (optional but recommended)
```yaml
name: memory-quiz
displayName: Memory Lightning
@@ -200,51 +186,44 @@ src/arcade-games/memory-quiz/ # NEW location
```
2. Create `index.ts` game definition:
```typescript
import { defineGame } from "@/lib/arcade/game-sdk";
import type { GameManifest } from "@/lib/arcade/game-sdk";
import { GameComponent } from "./components/GameComponent";
import { MemoryQuizProvider } from "./Provider";
import type {
MemoryQuizConfig,
MemoryQuizMove,
MemoryQuizState,
} from "./types";
import { memoryQuizValidator } from "./Validator";
import { defineGame } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { GameComponent } from './components/GameComponent'
import { MemoryQuizProvider } from './Provider'
import type { MemoryQuizConfig, MemoryQuizMove, MemoryQuizState } from './types'
import { memoryQuizValidator } from './Validator'
const manifest: GameManifest = {
name: "memory-quiz",
displayName: "Memory Lightning",
icon: "🧠",
name: 'memory-quiz',
displayName: 'Memory Lightning',
icon: '🧠',
// ... (copy from game.yaml or define inline)
};
}
const defaultConfig: MemoryQuizConfig = {
selectedCount: 5,
displayTime: 2.0,
selectedDifficulty: "easy",
playMode: "cooperative",
};
selectedDifficulty: 'easy',
playMode: 'cooperative',
}
function validateMemoryQuizConfig(
config: unknown,
): config is MemoryQuizConfig {
function validateMemoryQuizConfig(config: unknown): config is MemoryQuizConfig {
return (
typeof config === "object" &&
typeof config === 'object' &&
config !== null &&
"selectedCount" in config &&
"displayTime" in config &&
"selectedDifficulty" in config &&
"playMode" in config &&
'selectedCount' in config &&
'displayTime' in config &&
'selectedDifficulty' in config &&
'playMode' in config &&
[2, 5, 8, 12, 15].includes((config as any).selectedCount) &&
typeof (config as any).displayTime === "number" &&
typeof (config as any).displayTime === 'number' &&
(config as any).displayTime > 0 &&
["beginner", "easy", "medium", "hard", "expert"].includes(
(config as any).selectedDifficulty,
['beginner', 'easy', 'medium', 'hard', 'expert'].includes(
(config as any).selectedDifficulty
) &&
["cooperative", "competitive"].includes((config as any).playMode)
);
['cooperative', 'competitive'].includes((config as any).playMode)
)
}
export const memoryQuizGame = defineGame<
@@ -258,26 +237,24 @@ src/arcade-games/memory-quiz/ # NEW location
validator: memoryQuizValidator,
defaultConfig,
validateConfig: validateMemoryQuizConfig,
});
})
```
3. Register game in `game-registry.ts`:
```typescript
import { memoryQuizGame } from "@/arcade-games/memory-quiz";
registerGame(memoryQuizGame);
import { memoryQuizGame } from '@/arcade-games/memory-quiz'
registerGame(memoryQuizGame)
```
4. Update `validators.ts` to import from new location:
```typescript
import { memoryQuizValidator } from "@/arcade-games/memory-quiz/Validator";
import { memoryQuizValidator } from '@/arcade-games/memory-quiz/Validator'
```
5. Add type inference to `game-configs.ts`:
```typescript
import type { memoryQuizGame } from "@/arcade-games/memory-quiz";
export type MemoryQuizGameConfig = InferGameConfig<typeof memoryQuizGame>;
import type { memoryQuizGame } from '@/arcade-games/memory-quiz'
export type MemoryQuizGameConfig = InferGameConfig<typeof memoryQuizGame>
```
**Verification**: Game definition compiles, validator registered
@@ -285,18 +262,15 @@ src/arcade-games/memory-quiz/ # NEW location
---
### Phase 3: Update Types (30 minutes)
**Goal**: Ensure types match Game SDK expectations
**Changes to `types.ts`**:
1. Rename `SorobanQuizState` → `MemoryQuizState`
2. Ensure `MemoryQuizState` extends `GameState` from SDK
3. Rename move types to match SDK patterns
4. Export proper config type
**Example**:
```typescript
import type { GameConfig, GameState, GameMove } from '@/lib/arcade/game-sdk'
@@ -353,7 +327,6 @@ export type MemoryQuizMove =
```
**Key Changes**:
- All moves must have `playerId`, `userId`, `timestamp` (SDK requirement)
- State should include `activePlayers` and `playerMetadata` (SDK standard)
- Use `TEAM_MOVE` for moves where specific player doesn't matter
@@ -363,11 +336,9 @@ export type MemoryQuizMove =
---
### Phase 4: Create Provider (2 hours)
**Goal**: Single provider for room mode (only mode supported)
**Key Pattern**:
```typescript
'use client'
@@ -442,7 +413,6 @@ export function MemoryQuizProvider({ children }: { children: ReactNode }) {
```
**Key Changes from Current RoomProvider**:
1. ✅ No reducer - server handles all state
2. ✅ Uses SDK hooks exclusively
3. ✅ Simpler action creators (server does the work)
@@ -450,7 +420,6 @@ export function MemoryQuizProvider({ children }: { children: ReactNode }) {
5. ✅ Always uses roomId (no conditional logic)
**Files to Delete**:
- ❌ `reducer.ts` (no longer needed)
- ❌ `LocalMemoryQuizProvider.tsx` (local mode deprecated)
- ❌ Client-side `applyMoveOptimistically()` (server authoritative)
@@ -460,13 +429,10 @@ export function MemoryQuizProvider({ children }: { children: ReactNode }) {
---
### Phase 5: Update Components (1 hour)
**Goal**: Update components to use new provider API
**Changes Needed**:
1. **GameComponent.tsx** (new file):
```typescript
'use client'
@@ -504,21 +470,18 @@ export function MemoryQuizProvider({ children }: { children: ReactNode }) {
```
2. **SetupPhase.tsx**: Update to use action creators instead of dispatch
```diff
- dispatch({ type: 'SET_DIFFICULTY', difficulty: value })
+ setConfig('selectedDifficulty', value)
```
3. **DisplayPhase.tsx**: Update to use `nextCard` action
```diff
- dispatch({ type: 'NEXT_CARD' })
+ nextCard()
```
4. **InputPhase.tsx**: Update to use `acceptNumber`, `rejectNumber` actions
```diff
- dispatch({ type: 'ACCEPT_NUMBER', number })
+ acceptNumber(number)
@@ -531,7 +494,6 @@ export function MemoryQuizProvider({ children }: { children: ReactNode }) {
```
**Minimal Changes**:
- Components mostly stay the same
- Replace `dispatch()` calls with action creators
- No other UI changes needed
@@ -541,11 +503,9 @@ export function MemoryQuizProvider({ children }: { children: ReactNode }) {
---
### Phase 6: Update Page Route (15 minutes)
**Goal**: Update page to use new game definition
**New `/app/arcade/memory-quiz/page.tsx`**:
```typescript
'use client'
@@ -569,11 +529,9 @@ export default function MemoryQuizPage() {
---
### Phase 7: Testing (30 minutes)
**Goal**: Verify all functionality works
**Test Cases**:
1. **Solo Play** (single player in room):
- [ ] Setup phase renders
- [ ] Can change all settings (count, difficulty, display time, play mode)
@@ -611,11 +569,9 @@ export default function MemoryQuizPage() {
## Breaking Changes
### For Users
- ✅ **None** - Game should work identically
### For Developers
- ❌ Can't use `dispatch()` anymore (use action creators)
- ❌ Can't access reducer (server-driven state only)
- ❌ No local mode support (room mode only)
@@ -625,7 +581,6 @@ export default function MemoryQuizPage() {
## Rollback Plan
If migration fails:
1. Revert page to use old providers
2. Keep old files in place
3. Remove new `/arcade-games/memory-quiz/` directory
@@ -656,21 +611,18 @@ If migration fails:
## Complexity Analysis
### What Makes This Easier
- ✅ Validator already exists and works
- ✅ Already uses `useArcadeSession`
- ✅ Move types mostly match SDK requirements
- ✅ Well-tested, stable game
### What Makes This Harder
- ❌ Complex UI state (keyboard detection, animations)
- ❌ Two-phase gameplay (display, then input)
- ❌ Timing synchronization requirements
- ❌ Local input optimization (doesn't sync every keystroke)
### Estimated Time
- **Fast path** (no issues): 3-4 hours
- **Normal path** (minor fixes): 4-6 hours
- **Slow path** (major issues): 6-8 hours
@@ -695,9 +647,7 @@ If migration fails:
## Notes
### UI State Challenges
Memory Quiz has significant UI-only state:
- `wrongGuessAnimations` - visual feedback
- `hasPhysicalKeyboard` - device detection
- `showOnScreenKeyboard` - toggle state
@@ -706,13 +656,11 @@ Memory Quiz has significant UI-only state:
**Solution**: These can remain client-only (not synced). They don't affect game logic.
### Input Optimization
Current implementation doesn't sync `currentInput` over network (only final submission).
**Solution**: Keep this pattern. Use local state for input, only sync `ACCEPT_NUMBER`/`REJECT_NUMBER`.
### Timing Synchronization
Room creator controls card timing (NEXT_CARD moves).
**Solution**: Check `isRoomCreator` flag, only creator can advance cards.

View File

@@ -137,16 +137,15 @@ A game is defined by five core pieces:
```typescript
interface GameDefinition<TConfig, TState, TMove> {
manifest: GameManifest; // Display metadata
Provider: GameProviderComponent; // React context provider
GameComponent: GameComponent; // Main UI component
validator: GameValidator; // Server validation logic
defaultConfig: TConfig; // Default settings
manifest: GameManifest // Display metadata
Provider: GameProviderComponent // React context provider
GameComponent: GameComponent // Main UI component
validator: GameValidator // Server validation logic
defaultConfig: TConfig // Default settings
}
```
**Why this structure?**
- `manifest`: Declarative metadata for discovery and UI
- `Provider`: Encapsulates all game logic and state management
- `GameComponent`: Pure UI component, no business logic
@@ -159,21 +158,19 @@ The validator is the **source of truth** for game logic.
```typescript
interface GameValidator<TState, TMove> {
validateMove(state: TState, move: TMove): ValidationResult;
isGameComplete(state: TState): boolean;
getInitialState(config: unknown): TState;
validateMove(state: TState, move: TMove): ValidationResult
isGameComplete(state: TState): boolean
getInitialState(config: unknown): TState
}
```
**Key Principles:**
- **Pure functions**: No side effects, no I/O
- **Deterministic**: Same input → same output
- **Complete game logic**: All rules enforced here
- **Returns new state**: Immutable state updates
**Why server-side?**
- Prevents cheating (client can't fake moves)
- Single source of truth (no client/server divergence)
- Easier debugging (all logic in one place)
@@ -185,17 +182,16 @@ The provider manages client state and provides a clean API.
```typescript
interface GameContextValue {
state: GameState; // Current game state
lastError: string | null; // Last validation error
startGame: () => void; // Action creators
makeMove: (data) => void; // ...
clearError: () => void;
exitSession: () => void;
state: GameState // Current game state
lastError: string | null // Last validation error
startGame: () => void // Action creators
makeMove: (data) => void // ...
clearError: () => void
exitSession: () => void
}
```
**Responsibilities:**
- Wrap `useArcadeSession` with game-specific actions
- Build player metadata from game mode context
- Provide clean, typed API to components
@@ -214,19 +210,16 @@ The system uses **optimistic UI** for instant feedback:
- ✗ Invalid → Rollback and show error
**Why optimistic updates?**
- Instant feedback (no perceived latency)
- Better UX for fast-paced games
- Handles network issues gracefully
**Tradeoff:**
- More complex state management
- Need rollback logic
- Potential for flashing/jumpy UI on rollback
**When NOT to use:**
- High-stakes actions (payments, permanent changes)
- Actions with irreversible side effects
- When server latency is acceptable
@@ -243,7 +236,6 @@ Client A makes move → Server validates → Broadcast to all clients
```
**Conflict Resolution:**
- Server state is **always authoritative**
- Version numbers prevent out-of-order updates
- Pending moves are reapplied after server sync
@@ -258,14 +250,13 @@ The SDK provides a **stable API surface** that games import from:
```typescript
// ✅ GOOD: Import from SDK
import { useArcadeSession, type GameDefinition } from "@/lib/arcade/game-sdk";
import { useArcadeSession, type GameDefinition } from '@/lib/arcade/game-sdk'
// ❌ BAD: Import internal implementation
import { useArcadeSocket } from "@/hooks/useArcadeSocket";
import { useArcadeSocket } from '@/hooks/useArcadeSocket'
```
**Why?**
- **Stability**: Internal APIs can change, SDK stays stable
- **Discoverability**: One place to find all game APIs
- **Encapsulation**: Hide implementation details
@@ -290,23 +281,22 @@ Games register themselves on module load:
```typescript
// game-registry.ts
const registry = new Map<string, GameDefinition>();
const registry = new Map<string, GameDefinition>()
export function registerGame(game: GameDefinition) {
registry.set(game.manifest.name, game);
registry.set(game.manifest.name, game)
}
export function getGame(name: string) {
return registry.get(name);
return registry.get(name)
}
// At bottom of file
import { numberGuesserGame } from "@/arcade-games/number-guesser";
registerGame(numberGuesserGame);
import { numberGuesserGame } from '@/arcade-games/number-guesser'
registerGame(numberGuesserGame)
```
**Why self-registration?**
- No central "game list" to maintain
- Games are automatically discovered
- Import errors are caught at module load time
@@ -316,7 +306,7 @@ registerGame(numberGuesserGame);
```typescript
// ❌ Rejected: Magic, fragile, breaks with bundlers
const games = import.meta.glob("../arcade-games/*/index.ts");
const games = import.meta.glob('../arcade-games/*/index.ts')
```
### Player Metadata
@@ -328,19 +318,17 @@ function buildPlayerMetadata(
playerIds: string[],
existingMetadata: Record<string, unknown>,
playerMap: Map<string, Player>,
viewerId?: string,
): Record<string, PlayerMetadata>;
viewerId?: string
): Record<string, PlayerMetadata>
```
**Sources:**
1. `playerIds`: Which players are active
2. `existingMetadata`: Carry over existing data (for reconnects)
3. `playerMap`: Player details (name, emoji, color, userId)
4. `viewerId`: Current user (for ownership checks)
**Why so complex?**
- Players can be local or remote (in rooms)
- Need to preserve data across state updates
- Must map player IDs to user IDs for permissions
@@ -351,30 +339,30 @@ function buildPlayerMetadata(
```typescript
// 1. Client sends move
sendMove({
type: "MAKE_GUESS",
playerId: "player-123",
userId: "user-456",
type: 'MAKE_GUESS',
playerId: 'player-123',
userId: 'user-456',
timestamp: Date.now(),
data: { guess: 42 },
});
data: { guess: 42 }
})
// 2. Optimistic update (client-side)
const optimisticState = applyMove(currentState, move);
setOptimisticState(optimisticState);
const optimisticState = applyMove(currentState, move)
setOptimisticState(optimisticState)
// 3. Server validates
const result = validator.validateMove(serverState, move);
const result = validator.validateMove(serverState, move)
// 4a. Valid → Broadcast new state
if (result.valid) {
serverState = result.newState;
version++;
broadcastToAllClients({ gameState: serverState, version });
serverState = result.newState
version++
broadcastToAllClients({ gameState: serverState, version })
}
// 4b. Invalid → Send rejection
else {
sendToClient({ error: result.error, move });
sendToClient({ error: result.error, move })
}
// 5. Client handles response
@@ -383,7 +371,6 @@ else {
```
**Key Points:**
- Optimistic update happens **before** server response
- Server is **authoritative** (client state can be overwritten)
- Version numbers prevent stale updates
@@ -398,20 +385,17 @@ else {
**Choice:** All game logic runs on server, client is "dumb"
**Rationale:**
- Prevents cheating (client can't manipulate state)
- Single source of truth (no client/server divergence)
- Easier testing (one codebase for game logic)
- Can add server-side features (analytics, matchmaking)
**Tradeoff:**
- Secure, consistent, easier to maintain
- Network latency affects UX (mitigated by optimistic updates)
- Can't play offline
**Alternative Considered:** Client-side validation + server verification
- Rejected: Duplicate logic, potential for divergence
### Decision: Optimistic Updates
@@ -419,13 +403,11 @@ else {
**Choice:** Apply moves immediately, rollback on rejection
**Rationale:**
- Instant feedback (no perceived latency)
- Better UX for turn-based games
- Handles network issues gracefully
**Tradeoff:**
- Feels instant, smooth UX
- More complex state management
- Potential for jarring rollbacks
@@ -437,20 +419,17 @@ else {
**Choice:** Full TypeScript on client and server
**Rationale:**
- Compile-time validation catches bugs early
- Better IDE support (autocomplete, refactoring)
- Self-documenting code (types as documentation)
- Easier refactoring (compiler catches breakages)
**Tradeoff:**
- Fewer runtime errors, better DX
- Slower initial development (must define types)
- Learning curve for new developers
**Alternative Considered:** JavaScript with JSDoc
- Rejected: JSDoc is not type-safe, easy to drift
### Decision: React Context for State
@@ -458,20 +437,17 @@ else {
**Choice:** Each game has a Provider that wraps game logic
**Rationale:**
- Natural React pattern
- Easy to compose (Provider wraps GameComponent)
- No prop drilling
- Easy to test (can provide mock context)
**Tradeoff:**
- Clean component APIs, easy to understand
- Can't use context outside React tree
- Re-renders if not memoized carefully
**Alternative Considered:** Zustand/Redux
- Rejected: Overkill for game-specific state, harder to isolate per-game
### Decision: Phase-Based UI
@@ -479,14 +455,12 @@ else {
**Choice:** Each game has distinct phases (setup, playing, results)
**Rationale:**
- Clear separation of concerns
- Easy to understand game flow
- Each phase is independently testable
- Natural mapping to game states
**Tradeoff:**
- Organized, predictable
- Some duplication (multiple components)
- Can't have overlapping phases
@@ -504,19 +478,16 @@ else {
**Choice:** Don't sort player arrays, use Set iteration order
**Rationale:**
- Set order is consistent within a session
- Matches UI display order (PageWithNav uses same Set)
- Avoids alphabetical bias (first player isn't always "AAA")
**Tradeoff:**
- UI and game logic always match
- Order is not predictable across sessions
- Different players see different orders (based on join time)
**Why not sort?**
- Creates mismatch: UI shows Set order, game uses sorted order
- Causes "skipping first player" bug (discovered in Number Guesser)
@@ -526,23 +497,20 @@ else {
```typescript
const { state, sendMove } = useArcadeSession({
applyMove: (state, move) => state, // Don't apply, wait for server
});
applyMove: (state, move) => state // Don't apply, wait for server
})
```
**Rationale:**
- Keeps client logic minimal (less code to maintain)
- Prevents client/server logic divergence
- Server is authoritative (no client-side cheats)
**Tradeoff:**
- Simple, secure
- Slightly slower UX (wait for server)
**When to use client-side `applyMove`:**
- Very fast-paced games (60fps animations)
- Purely cosmetic updates (particles, sounds)
- Never for game logic (scoring, winning, etc.)
@@ -559,10 +527,10 @@ const { state, sendMove } = useArcadeSession({
```typescript
// Client sends
sendMove({ data: { guess: 42 } });
sendMove({ data: { guess: 42 } })
// Server receives
move.data.guess === "42"; // String! 😱
move.data.guess === "42" // String! 😱
```
**Solution:** Explicit coercion in validator
@@ -577,7 +545,6 @@ validateMove(state, move) {
**Lesson:** Always coerce types from `move.data` in validator.
**Symptom Observed:** User reported "first guess always rejected, second guess always correct" which was caused by:
- First guess: `"42" < 1` evaluates to `false` (string comparison)
- Validator thinks it's valid, calculates distance as `NaN`
- `NaN === 0` is false, so guess is "wrong"
@@ -591,7 +558,6 @@ validateMove(state, move) {
**Problem:** Set iteration order differed from sorted order, causing "skipped player" bug.
**Root Cause:**
- UI used `Array.from(Set)` → Set iteration order
- Game used `Array.from(Set).sort()` → Alphabetical order
- Leftmost UI player ≠ First game player
@@ -640,9 +606,8 @@ const { lastError, clearError } = useArcadeSession()
**Solution:** Check if last guess was correct:
```typescript
const roundComplete =
state.guesses.length > 0 &&
state.guesses[state.guesses.length - 1].distance === 0;
const roundComplete = state.guesses.length > 0 &&
state.guesses[state.guesses.length - 1].distance === 0
```
**Lesson:** Be precise about what "complete" means (round vs. game).
@@ -654,13 +619,13 @@ const roundComplete =
**Solution:** Add logging in validator:
```typescript
console.log("[NumberGuesser] Validating guess:", {
console.log('[NumberGuesser] Validating guess:', {
guess,
guessType: typeof guess,
secretNumber: state.secretNumber,
secretNumberType: typeof state.secretNumber,
distance: Math.abs(guess - state.secretNumber),
});
distance: Math.abs(guess - state.secretNumber)
})
```
**Lesson:** Log types and values during development.
@@ -674,7 +639,6 @@ console.log("[NumberGuesser] Validating guess:", {
**Current State:** Manual testing only
**Proposal:**
- Unit tests for validators (pure functions, easy to test)
- Integration tests for Provider + useArcadeSession
- E2E tests for full game flows (Playwright)
@@ -701,7 +665,6 @@ describe('NumberGuesserValidator', () => {
**Current State:** No move history
**Proposal:**
- Store all moves in database
- Allow "replay" of games
- Enable undo/redo (for certain games)
@@ -711,13 +674,13 @@ describe('NumberGuesserValidator', () => {
```typescript
interface GameSession {
id: string;
roomId: string;
gameType: string;
moves: GameMove[];
finalState: GameState;
startTime: number;
endTime: number;
id: string
roomId: string
gameType: string
moves: GameMove[]
finalState: GameState
startTime: number
endTime: number
}
```
@@ -726,7 +689,6 @@ interface GameSession {
**Current State:** No analytics
**Proposal:**
- Track game completions, durations, winners
- Player skill ratings (Elo, TrueSkill)
- Popular games dashboard
@@ -737,7 +699,6 @@ interface GameSession {
**Current State:** Only active players can view game
**Proposal:**
- Allow non-players to watch
- Spectators can't send moves (read-only)
- Show spectator count in room
@@ -746,8 +707,8 @@ interface GameSession {
```typescript
interface RoomMember {
userId: string;
role: "player" | "spectator" | "host";
userId: string
role: 'player' | 'spectator' | 'host'
}
```
@@ -756,7 +717,6 @@ interface RoomMember {
**Current State:** One config per game
**Proposal:**
- Preset variants (Easy, Medium, Hard)
- Custom rules per room
- "House rules" feature
@@ -768,7 +728,7 @@ const variants = {
beginner: { minNumber: 1, maxNumber: 20, roundsToWin: 1 },
standard: { minNumber: 1, maxNumber: 100, roundsToWin: 3 },
expert: { minNumber: 1, maxNumber: 1000, roundsToWin: 5 },
};
}
```
### 6. Tournaments / Brackets
@@ -776,7 +736,6 @@ const variants = {
**Current State:** Single-room games only
**Proposal:**
- Multi-round tournaments
- Bracket generation
- Leaderboards
@@ -786,13 +745,11 @@ const variants = {
**Current State:** Games are hard-coded
**Proposal:**
- Load games from external bundles
- Community-created games
- Sandboxed execution (Deno, WASM)
**Challenges:**
- Security (untrusted code)
- Type safety (dynamic loading)
- Versioning (breaking changes)
@@ -802,7 +759,6 @@ const variants = {
**Current State:** Text chat only (if implemented)
**Proposal:**
- WebRTC voice/video
- Per-room channels
- Mute/kick controls
@@ -811,17 +767,17 @@ const variants = {
## Appendix: Key Files Reference
| Path | Purpose |
| ------------------------------------- | --------------------------- |
| `src/lib/arcade/game-sdk/index.ts` | SDK exports (public API) |
| `src/lib/arcade/game-registry.ts` | Game registration |
| `src/lib/arcade/manifest-schema.ts` | Manifest validation |
| `src/hooks/useArcadeSession.ts` | Session management hook |
| `src/hooks/useArcadeSocket.ts` | WebSocket connection |
| Path | Purpose |
|------|---------|
| `src/lib/arcade/game-sdk/index.ts` | SDK exports (public API) |
| `src/lib/arcade/game-registry.ts` | Game registration |
| `src/lib/arcade/manifest-schema.ts` | Manifest validation |
| `src/hooks/useArcadeSession.ts` | Session management hook |
| `src/hooks/useArcadeSocket.ts` | WebSocket connection |
| `src/hooks/useOptimisticGameState.ts` | Optimistic state management |
| `src/contexts/GameModeContext.tsx` | Player management |
| `src/components/PageWithNav.tsx` | Game navigation wrapper |
| `src/arcade-games/number-guesser/` | Example game implementation |
| `src/contexts/GameModeContext.tsx` | Player management |
| `src/components/PageWithNav.tsx` | Game navigation wrapper |
| `src/arcade-games/number-guesser/` | Example game implementation |
---
@@ -833,4 +789,4 @@ const variants = {
---
_Last Updated: 2025-10-15_
*Last Updated: 2025-10-15*

View File

@@ -1,3 +0,0 @@
-- Custom SQL migration file, put your code below! --
-- Add native_abacus_numbers column to abacus_settings table
ALTER TABLE `abacus_settings` ADD `native_abacus_numbers` integer DEFAULT 0 NOT NULL;

View File

@@ -1,17 +0,0 @@
-- Migration: Add player_stats table
-- Per-player game statistics tracking
CREATE TABLE `player_stats` (
`player_id` text PRIMARY KEY NOT NULL,
`games_played` integer DEFAULT 0 NOT NULL,
`total_wins` integer DEFAULT 0 NOT NULL,
`total_losses` integer DEFAULT 0 NOT NULL,
`best_time` integer,
`highest_accuracy` real DEFAULT 0 NOT NULL,
`favorite_game_type` text,
`game_stats` text DEFAULT '{}' NOT NULL,
`last_played_at` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON UPDATE no action ON DELETE cascade
);

View File

@@ -1,15 +0,0 @@
-- Custom SQL migration file, put your code below! --
-- Create worksheet_settings table for persisting user worksheet generator preferences
CREATE TABLE `worksheet_settings` (
`id` TEXT PRIMARY KEY NOT NULL,
`user_id` TEXT NOT NULL,
`worksheet_type` TEXT NOT NULL,
`config` TEXT NOT NULL,
`created_at` INTEGER NOT NULL,
`updated_at` INTEGER NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
);--> statement-breakpoint
-- Create index for efficient lookups by user and worksheet type
CREATE INDEX `worksheet_settings_user_type_idx` ON `worksheet_settings` (`user_id`, `worksheet_type`);

View File

@@ -1,28 +0,0 @@
-- Custom SQL migration file, put your code below! --
-- Remove foreign key constraint from worksheet_settings to allow guest users
-- SQLite doesn't support DROP CONSTRAINT, so we need to recreate the table
-- Create new table without foreign key
CREATE TABLE `worksheet_settings_new` (
`id` TEXT PRIMARY KEY NOT NULL,
`user_id` TEXT NOT NULL,
`worksheet_type` TEXT NOT NULL,
`config` TEXT NOT NULL,
`created_at` INTEGER NOT NULL,
`updated_at` INTEGER NOT NULL
);--> statement-breakpoint
-- Copy existing data (if any)
INSERT INTO `worksheet_settings_new`
SELECT id, user_id, worksheet_type, config, created_at, updated_at
FROM `worksheet_settings`;--> statement-breakpoint
-- Drop old table
DROP TABLE `worksheet_settings`;--> statement-breakpoint
-- Rename new table to original name
ALTER TABLE `worksheet_settings_new` RENAME TO `worksheet_settings`;--> statement-breakpoint
-- Recreate index
CREATE INDEX `worksheet_settings_user_type_idx` ON `worksheet_settings` (`user_id`, `worksheet_type`);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -85,27 +85,6 @@
"when": 1760800000000,
"tag": "0011_add_room_game_configs",
"breakpoints": true
},
{
"idx": 12,
"version": "6",
"when": 1761939039939,
"tag": "0012_damp_mongoose",
"breakpoints": true
},
{
"idx": 13,
"version": "6",
"when": 1762432185673,
"tag": "0013_conscious_firebird",
"breakpoints": true
},
{
"idx": 14,
"version": "6",
"when": 1762434916279,
"tag": "0014_remarkable_master_chief",
"breakpoints": true
}
]
}

View File

@@ -1,7 +1,3 @@
const createNextIntlPlugin = require('next-intl/plugin')
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
@@ -67,4 +63,4 @@ const nextConfig = {
},
}
module.exports = withNextIntl(nextConfig)
module.exports = nextConfig

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && concurrently \"node server.js\" \"npx @pandacss/dev --watch\"",
"build": "node scripts/generate-build-info.js && npx tsx scripts/generateAllDayIcons.tsx && npx @pandacss/dev && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && next build",
"build": "node scripts/generate-build-info.js && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && next build",
"start": "NODE_ENV=production node server.js",
"lint": "npx @biomejs/biome lint . && npx eslint .",
"lint:fix": "npx @biomejs/biome lint . --write && npx eslint . --fix",
@@ -46,9 +46,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-spring/web": "^10.0.3",
"@react-three/drei": "^9.117.0",
"@react-three/fiber": "^8.17.0",
"@react-spring/web": "^10.0.2",
"@soroban/abacus-react": "workspace:*",
"@soroban/core": "workspace:*",
"@soroban/templates": "workspace:*",
@@ -59,39 +57,20 @@
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.4.1",
"drizzle-orm": "^0.44.6",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"emojibase-data": "^16.0.3",
"gray-matter": "^4.0.3",
"jose": "^6.1.0",
"js-yaml": "^4.1.0",
"lib0": "^0.2.114",
"lucide-react": "^0.294.0",
"make-plural": "^7.4.0",
"nanoid": "^5.1.6",
"next": "^14.2.32",
"next-auth": "5.0.0-beta.29",
"next-intl": "^4.4.0",
"openscad-wasm-prebuilt": "^1.2.0",
"python-bridge": "^1.1.0",
"qrcode.react": "^4.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-resizable-layout": "^0.7.3",
"react-resizable-panels": "^3.0.6",
"react-textfit": "^1.1.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-highlight": "^7.0.2",
"rehype-slug": "^6.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
"remark-html": "^16.0.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"three": "^0.169.0",
"y-protocols": "^1.0.6",
"y-websocket": "^3.0.0",
"yjs": "^13.6.27",
"zod": "^4.1.12"
},
"devDependencies": {
@@ -107,8 +86,6 @@
"@types/node": "^20.0.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-textfit": "^1.1.4",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^5.0.2",
"concurrently": "^8.0.0",
"drizzle-kit": "^0.31.5",

View File

@@ -71,122 +71,6 @@ export default defineConfig({
glow: { value: 'glow 1s ease-in-out infinite alternate' },
},
},
// Semantic color tokens for light/dark theme support
semanticTokens: {
colors: {
// Background colors
'bg.canvas': {
value: {
base: '#ffffff',
_dark: '#0f172a',
},
},
'bg.surface': {
value: {
base: '#f8fafc',
_dark: '#1e293b',
},
},
'bg.subtle': {
value: {
base: '#f1f5f9',
_dark: '#334155',
},
},
'bg.muted': {
value: {
base: '#e2e8f0',
_dark: '#475569',
},
},
// Text colors
'text.primary': {
value: {
base: '#0f172a',
_dark: '#f1f5f9',
},
},
'text.secondary': {
value: {
base: '#475569',
_dark: '#cbd5e1',
},
},
'text.muted': {
value: {
base: '#64748b',
_dark: '#94a3b8',
},
},
'text.inverse': {
value: {
base: '#ffffff',
_dark: '#0f172a',
},
},
// Border colors
'border.default': {
value: {
base: '#e2e8f0',
_dark: '#334155',
},
},
'border.muted': {
value: {
base: '#f1f5f9',
_dark: '#1e293b',
},
},
'border.emphasis': {
value: {
base: '#cbd5e1',
_dark: '#475569',
},
},
// Accent colors (purple theme)
'accent.default': {
value: {
base: '#7c3aed',
_dark: '#a78bfa',
},
},
'accent.emphasis': {
value: {
base: '#6d28d9',
_dark: '#c4b5fd',
},
},
'accent.muted': {
value: {
base: '#f5f3ff',
_dark: 'rgba(139, 92, 246, 0.15)',
},
},
'accent.subtle': {
value: {
base: '#ede9fe',
_dark: 'rgba(139, 92, 246, 0.1)',
},
},
// Interactive states
'interactive.hover': {
value: {
base: '#f8fafc',
_dark: '#334155',
},
},
'interactive.active': {
value: {
base: '#f1f5f9',
_dark: '#475569',
},
},
},
},
keyframes: {
// Shake - horizontal oscillation for errors (line 3419)
shake: {
@@ -231,12 +115,4 @@ export default defineConfig({
},
},
},
// Enable dark mode support via data-theme attribute
conditions: {
extend: {
dark: '[data-theme="dark"] &, .dark &',
light: '[data-theme="light"] &, .light &',
},
},
})

View File

@@ -1,47 +0,0 @@
// Inline version of abacus.scad that doesn't require BOSL2
// This version uses a hardcoded bounding box size instead of the bounding_box() function
// ---- USER CUSTOMIZABLE PARAMETERS ----
// These can be overridden via command line: -D 'columns=7' etc.
columns = 13; // Total number of columns (1-13, mirrored book design)
scale_factor = 1.5; // Overall size scale (preserves aspect ratio)
// -----------------------------------------
stl_path = "/3d-models/simplified.abacus.stl";
// Known bounding box dimensions of the simplified.abacus.stl file
// These were measured from the original file
bbox_size = [186, 60, 120]; // [width, depth, height] in STL units
// Calculate parameters based on column count
// The full STL has 13 columns. We want columns/2 per side (mirrored).
total_columns_in_stl = 13;
columns_per_side = columns / 2;
width_scale = columns_per_side / total_columns_in_stl;
// Column spacing: distance between mirrored halves
units_per_column = bbox_size[0] / total_columns_in_stl; // ~14.3 units per column
column_spacing = columns_per_side * units_per_column;
// --- actual model ---
module imported() {
import(stl_path, convexity = 10);
}
// Create a bounding box manually instead of using BOSL2's bounding_box()
module bounding_box_manual() {
translate([-bbox_size[0]/2, -bbox_size[1]/2, -bbox_size[2]/2])
cube(bbox_size);
}
module half_abacus() {
intersection() {
scale([width_scale, 1, 1]) bounding_box_manual();
imported();
}
}
scale([scale_factor, scale_factor, scale_factor]) {
translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus();
half_abacus();
}

View File

@@ -1,39 +0,0 @@
include <BOSL2/std.scad>; // BOSL2 v2.0 or newer
// ---- USER CUSTOMIZABLE PARAMETERS ----
// These can be overridden via command line: -D 'columns=7' etc.
columns = 13; // Total number of columns (1-13, mirrored book design)
scale_factor = 1.5; // Overall size scale (preserves aspect ratio)
// -----------------------------------------
stl_path = "./simplified.abacus.stl";
// Calculate parameters based on column count
// The full STL has 13 columns. We want columns/2 per side (mirrored).
// The original bounding box intersection: scale([35/186, 1, 1])
// 35/186 ≈ 0.188 = ~2.44 columns, so 186 units ≈ 13 columns, ~14.3 units per column
total_columns_in_stl = 13;
columns_per_side = columns / 2;
width_scale = columns_per_side / total_columns_in_stl;
// Column spacing: distance between mirrored halves
// Original spacing of 69 for ~2.4 columns/side
// Calculate proportional spacing based on columns
units_per_column = 186 / total_columns_in_stl; // ~14.3 units per column
column_spacing = columns_per_side * units_per_column;
// --- actual model ---
module imported()
import(stl_path, convexity = 10);
module half_abacus() {
intersection() {
scale([width_scale, 1, 1]) bounding_box() imported();
imported();
}
}
scale([scale_factor, scale_factor, scale_factor]) {
translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus();
half_abacus();
}

View File

@@ -1,581 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
</symbol>
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,896 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(24.400000000000023 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(24.400000000000023 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(24.400000000000023 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(24.400000000000023 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 78.8 0 L 78.8 78.8 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 78.8 78.8 L 0 78.8 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -1,578 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE52AD68C7B06D7D319E57C0DFEC4A716" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
</symbol>
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
<symbol id="gE52AD68C7B06D7D319E57C0DFEC4A716" overflow="visible">
<path d="M 28.791 10.584001 C 28.791 12.852 28.098 15.687 25.704 18.333 C 24.507 19.656 23.499 20.286001 19.467001 22.806 C 24.003 25.137001 27.09 28.413 27.09 32.571 C 27.09 38.367 21.483 41.958 15.75 41.958 C 9.45 41.958 4.347 37.296 4.347 31.437 C 4.347 30.303001 4.473 27.468 7.119 24.507 C 7.8120003 23.751 10.143001 22.176 11.718 21.105 C 8.064 19.278 2.6460001 15.75 2.6460001 9.5130005 C 2.6460001 2.835 9.0720005 -1.386 15.687 -1.386 C 22.806 -1.386 28.791 3.8430002 28.791 10.584001 Z M 24.318 32.571 C 24.318 28.98 21.861 25.956001 18.081 23.751 L 10.269 28.791 C 7.3710003 30.681 7.119 32.823 7.119 33.894 C 7.119 37.737 11.214 40.383 15.687 40.383 C 20.286001 40.383 24.318 37.107002 24.318 32.571 Z M 25.641 8.316 C 25.641 3.654 20.916 0.37800002 15.75 0.37800002 C 10.332 0.37800002 5.796 4.284 5.796 9.5130005 C 5.796 13.167 7.8120003 17.199 13.167 20.16 L 20.916 15.246 C 22.68 14.049 25.641 12.159 25.641 8.316 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,899 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 27.846500000000002)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g7C2A22CF0123BA7341054F23FB3AEE5D" x="34.009" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 73.02324999999998)">
<g class="typst-group">
<g>
<g transform="translate(53.949999999999996 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(20.4 118.2)">
<g class="typst-group">
<g>
<g transform="translate(11.1503 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6BF4B3C6EAE864CD7BB10E0691834549" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g5DFA18D209ACD12A0DB62DA18B290FC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(113.05 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 27.846500000000002)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3EB0DD94D29D91EA51A9F1A322CF0975" x="34.009" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 73.02324999999998)">
<g class="typst-group">
<g>
<g transform="translate(53.949999999999996 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g2FBAD5FC5745664B6ADE5AD1F11D2E13" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g13CC2BF7150CAD2A56D6E42C1E1AA71A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(20.4 118.2)">
<g class="typst-group">
<g>
<g transform="translate(11.1503 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g2FBAD5FC5745664B6ADE5AD1F11D2E13" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6BF4B3C6EAE864CD7BB10E0691834549" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(113.05 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 27.846500000000002)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA12A273C0EA9CCB93CDAAF5C3476B99" x="34.009" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 73.02324999999998)">
<g class="typst-group">
<g>
<g transform="translate(53.949999999999996 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g2FBAD5FC5745664B6ADE5AD1F11D2E13" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g13CC2BF7150CAD2A56D6E42C1E1AA71A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(20.4 118.2)">
<g class="typst-group">
<g>
<g transform="translate(11.1503 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F9EB82CA73B0CEFA6A6BBB3DF89DD91" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g5DFA18D209ACD12A0DB62DA18B290FC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(113.05 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 27.846500000000002)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gB0D702D3F5E8AA5BEC3D8B58F45868E0" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB3094794D0628B160CB8E5B1E6D90854" x="34.009" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gD9DD5739D64F483B0A1931A9965DFBC1" x="54.421499999999995" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 73.02324999999998)">
<g class="typst-group">
<g>
<g transform="translate(53.949999999999996 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 59.1 0 L 59.1 59.1 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 59.1 59.1 L 0 59.1 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 59.1)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F9EB82CA73B0CEFA6A6BBB3DF89DD91" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(20.4 118.2)">
<g class="typst-group">
<g>
<g transform="translate(11.1503 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g702B42440DB7C8CF270F99AC35604E" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEBD20B78B8E26035E6B0027C46547B5B" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 118.2)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
<g transform="translate(17.724999999999998 45.702949999999994)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g13CC2BF7150CAD2A56D6E42C1E1AA71A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(113.05 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 59.1 0 "/>
</g>
<g transform="translate(53.949999999999996 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#fff9c4" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(113.05 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e8f5e9" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(172.15000000000003 177.29999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#e3f2fd" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 59.1 L 59.1 59.1 L 59.1 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gB0D702D3F5E8AA5BEC3D8B58F45868E0" overflow="visible">
<path d="M 31.701502 4.9345 C 31.701502 5.9995003 30.672 5.9995003 30.246 5.9995003 L 21.797 5.9995003 L 23.5365 11.786 L 30.246 11.786 C 30.672 11.786 31.701502 11.786 31.701502 12.851001 C 31.701502 13.951501 30.601002 13.951501 30.0685 13.951501 L 24.2465 13.951501 L 27.015501 22.862001 C 27.157501 23.288 27.157501 23.323502 27.157501 23.572 C 27.157501 24.104502 26.7315 24.637001 26.057001 24.637001 C 25.276001 24.637001 25.098501 24.0335 24.956501 23.572 L 21.9745 13.951501 L 15.158501 13.951501 L 17.9275 22.862001 C 18.0695 23.288 18.0695 23.323502 18.0695 23.572 C 18.0695 24.104502 17.643501 24.637001 16.969 24.637001 C 16.188 24.637001 16.0105 24.0335 15.868501 23.572 L 12.8865 13.951501 L 3.9050002 13.951501 C 3.3725002 13.951501 2.272 13.951501 2.272 12.851001 C 2.272 11.786 3.3015 11.786 3.7275002 11.786 L 12.1765 11.786 L 10.437 5.9995003 L 3.7275002 5.9995003 C 3.3015 5.9995003 2.272 5.9995003 2.272 4.9345 C 2.272 3.834 3.3725002 3.834 3.9050002 3.834 L 9.727 3.834 L 6.958 -5.112 C 6.887 -5.3250003 6.816 -5.538 6.816 -5.822 C 6.816 -6.3545003 7.242 -6.887 7.9165 -6.887 C 8.662001 -6.887 8.8395 -6.3190002 8.981501 -5.893 L 11.999001 3.834 L 18.815 3.834 L 16.046001 -5.112 C 15.975 -5.3250003 15.904 -5.538 15.904 -5.822 C 15.904 -6.3545003 16.33 -6.887 17.004501 -6.887 C 17.75 -6.887 17.9275 -6.3190002 18.0695 -5.893 L 21.087 3.834 L 30.0685 3.834 C 30.601002 3.834 31.701502 3.834 31.701502 4.9345 Z M 21.264502 11.786 L 19.525002 5.9995003 L 12.709001 5.9995003 L 14.448501 11.786 Z "/>
</symbol>
<symbol id="g7C2A22CF0123BA7341054F23FB3AEE5D" overflow="visible">
<path d="M 17.537 0 L 17.537 1.6685001 L 12.567 1.6685001 L 12.567 22.1165 C 12.567 22.897501 12.567 23.2525 11.644 23.2525 C 11.2535 23.2525 11.182501 23.2525 10.863 23.004002 C 8.1295 20.980501 4.473 20.980501 3.7275002 20.980501 L 3.0175002 20.980501 L 3.0175002 19.312 L 3.7275002 19.312 C 4.2955003 19.312 6.248 19.3475 8.342501 20.022001 L 8.342501 1.6685001 L 3.408 1.6685001 L 3.408 0 C 4.9700003 0.1065 8.733001 0.1065 10.472501 0.1065 C 12.212001 0.1065 15.975 0.1065 17.537 0 Z "/>
</symbol>
<symbol id="gD9DD5739D64F483B0A1931A9965DFBC1" overflow="visible">
<path d="M 8.4135 2.769 C 8.4135 4.2955003 7.171 5.538 5.6445003 5.538 C 4.118 5.538 2.8755002 4.2955003 2.8755002 2.769 C 2.8755002 1.2425001 4.118 0 5.6445003 0 C 7.171 0 8.4135 1.2425001 8.4135 2.769 Z "/>
</symbol>
<symbol id="gEBD20B78B8E26035E6B0027C46547B5B" overflow="visible">
<path d="M 21.2377 8.2302 L 20.0552 8.2302 C 19.8187 6.8112 19.4876 4.73 19.0146 4.0205 C 18.6835 3.6421 15.5617 3.6421 14.5211 3.6421 L 6.0071 3.6421 L 11.0209 8.514 C 18.3997 15.0414 21.2377 17.5956 21.2377 22.3256 C 21.2377 27.7178 16.9807 31.5018 11.2101 31.5018 C 5.8652 31.5018 2.365 27.1502 2.365 22.9405 C 2.365 20.2917 4.73 20.2917 4.8719 20.2917 C 5.676 20.2917 7.3315 20.8593 7.3315 22.7986 C 7.3315 24.0284 6.4801 25.2582 4.8245997 25.2582 C 4.4462 25.2582 4.3516 25.2582 4.2097 25.2109 C 5.2976 28.2854 7.8518 30.0355 10.5952 30.0355 C 14.8995 30.0355 16.9334 26.2042 16.9334 22.3256 C 16.9334 18.5416 14.5684 14.8049 11.9669 11.8723 L 2.8853 1.7501 C 2.365 1.2298 2.365 1.1352 2.365 0 L 19.9133 0 Z "/>
</symbol>
<symbol id="g702B42440DB7C8CF270F99AC35604E" overflow="visible">
<path d="M 34.1506 11.825 C 34.1506 12.3453 33.7249 12.771 33.2046 12.771 L 19.3457 12.771 L 19.3457 26.6299 C 19.3457 27.1502 18.92 27.5759 18.3997 27.5759 C 17.8794 27.5759 17.4537 27.1502 17.4537 26.6299 L 17.4537 12.771 L 3.5948 12.771 C 3.0745 12.771 2.6488 12.3453 2.6488 11.825 C 2.6488 11.3047 3.0745 10.879 3.5948 10.879 L 17.4537 10.879 L 17.4537 -2.9799 C 17.4537 -3.5002 17.8794 -3.9259 18.3997 -3.9259 C 18.92 -3.9259 19.3457 -3.5002 19.3457 -2.9799 L 19.3457 10.879 L 33.2046 10.879 C 33.7249 10.879 34.1506 11.3047 34.1506 11.825 Z "/>
</symbol>
<symbol id="g6BF4B3C6EAE864CD7BB10E0691834549" overflow="visible">
<path d="M 22.2783 7.8045 L 22.2783 9.2708 L 17.5483 9.2708 L 17.5483 30.7923 C 17.5483 31.7383 17.5483 32.0221 16.7915 32.0221 C 16.3658 32.0221 16.2239 32.0221 15.8455 31.4545 L 1.3244 9.2708 L 1.3244 7.8045 L 13.906199 7.8045 L 13.906199 3.6894 C 13.906199 1.9866 13.8116 1.4663 10.3114 1.4663 L 9.3181 1.4663 L 9.3181 0 C 11.2574 0.1419 13.717 0.1419 15.7036 0.1419 C 17.6902 0.1419 20.1971 0.1419 22.1364 0 L 22.1364 1.4663 L 21.1431 1.4663 C 17.6429 1.4663 17.5483 1.9866 17.5483 3.6894 L 17.5483 7.8045 Z M 14.19 9.2708 L 2.6488 9.2708 L 14.19 26.9137 Z "/>
</symbol>
<symbol id="g5DFA18D209ACD12A0DB62DA18B290FC" overflow="visible">
<path d="M 21.6161 9.6491995 C 21.6161 15.6563 17.4064 20.1971 12.1561 20.1971 C 8.9397 20.1971 7.1896 17.7848 6.2436 15.5144 L 6.2436 16.6496 C 6.2436 28.616499 12.1088 30.3193 14.5211 30.3193 C 15.6563 30.3193 17.6429 30.0355 18.6835 28.4273 C 17.973999 28.4273 16.082 28.4273 16.082 26.2988 C 16.082 24.8325 17.2172 24.123 18.2578 24.123 C 19.0146 24.123 20.4336 24.5487 20.4336 26.3934 C 20.4336 29.2314 18.3524 31.5018 14.4265 31.5018 C 8.3721 31.5018 1.9866 25.4001 1.9866 14.9468 C 1.9866 2.3177 7.4734 -1.0406 11.8723 -1.0406 C 17.1226 -1.0406 21.6161 3.4056 21.6161 9.6491995 Z M 17.3591 9.6965 C 17.3591 7.4261 17.3591 5.0611 16.555 3.3583 C 15.136 0.5203 12.9602 0.2838 11.8723 0.2838 C 8.8924 0.2838 7.4734 3.1218 7.1896 3.8313 C 6.3382 6.0544 6.3382 9.8384 6.3382 10.6898 C 6.3382 14.3792 7.8518 19.1092 12.1088 19.1092 C 12.8656 19.1092 15.0414 19.1092 16.5077 16.1766 C 17.3591 14.4265 17.3591 12.0142 17.3591 9.6965 Z "/>
</symbol>
<symbol id="g3EB0DD94D29D91EA51A9F1A322CF0975" overflow="visible">
<path d="M 18.3535 7.881 L 16.685001 7.881 C 16.5785 7.171 16.259 4.899 15.762 4.6505003 C 15.371501 4.4375 12.709001 4.4375 12.141001 4.4375 L 6.9225 4.4375 C 8.591001 5.822 10.437 7.3485003 12.0345 8.52 C 16.081501 11.502001 18.3535 13.170501 18.3535 16.543001 C 18.3535 20.6255 14.661501 23.2525 9.656 23.2525 C 5.3605003 23.2525 2.0235 21.0515 2.0235 17.8565 C 2.0235 15.762 3.7275002 15.229501 4.5795 15.229501 C 5.7155004 15.229501 7.1355004 16.0105 7.1355004 17.785501 C 7.1355004 19.6315 5.6445003 20.199501 5.112 20.341501 C 6.1415 21.1935 7.4905005 21.584002 8.7685 21.584002 C 11.821501 21.584002 13.4545 19.17 13.4545 16.5075 C 13.4545 14.058001 12.1055 11.644 9.620501 9.1235 L 2.4850001 1.8460001 C 2.0235 1.4200001 2.0235 1.3490001 2.0235 0.639 L 2.0235 0 L 17.253 0 Z "/>
</symbol>
<symbol id="g2FBAD5FC5745664B6ADE5AD1F11D2E13" overflow="visible">
<path d="M 19.8187 0 L 19.8187 1.4663 L 18.3051 1.4663 C 14.0480995 1.4663 13.906199 1.9866 13.906199 3.7367 L 13.906199 30.272 C 13.906199 31.4072 13.906199 31.5018 12.8183 31.5018 C 9.8857 28.4746 5.7233 28.4746 4.2097 28.4746 L 4.2097 27.0083 C 5.1557 27.0083 7.9464 27.0083 10.406 28.2381 L 10.406 3.7367 C 10.406 2.0339 10.2641 1.4663 6.0071 1.4663 L 4.4934998 1.4663 L 4.4934998 0 C 6.149 0.1419 10.2641 0.1419 12.1561 0.1419 C 14.0480995 0.1419 18.1632 0.1419 19.8187 0 Z "/>
</symbol>
<symbol id="g13CC2BF7150CAD2A56D6E42C1E1AA71A" overflow="visible">
<path d="M 21.758 15.136 C 21.758 18.92 21.5215 22.704 19.866 26.2042 C 17.6902 30.744999 13.8116 31.5018 11.825 31.5018 C 8.9869995 31.5018 5.5341 30.272 3.5948 25.8731 C 2.0812 22.6094 1.8447 18.92 1.8447 15.136 C 1.8447 11.5885 2.0339 7.3315 3.9732 3.7367 C 6.0071 -0.0946 9.46 -1.0406 11.777699 -1.0406 C 14.3319 -1.0406 17.9267 -0.0473 20.0079 4.4462 C 21.5215 7.7099 21.758 11.3993 21.758 15.136 Z M 17.8321 15.7036 C 17.8321 12.1561 17.8321 8.9397 17.3118 5.9125 C 16.6023 1.419 13.906199 0 11.777699 0 C 9.933 0 7.1423 1.1825 6.2908998 5.7233 C 5.7706 8.5613 5.7706 12.9129 5.7706 15.7036 C 5.7706 18.7308 5.7706 21.8526 6.149 24.4068 C 7.0477 30.0355 10.5952 30.4612 11.777699 30.4612 C 13.3386 30.4612 16.4604 29.6098 17.3591 24.9271 C 17.8321 22.2783 17.8321 18.6835 17.8321 15.7036 Z "/>
</symbol>
<symbol id="gAA12A273C0EA9CCB93CDAAF5C3476B99" overflow="visible">
<path d="M 18.673 6.3900003 C 18.673 8.342501 17.643501 11.3955 12.496 12.496 C 14.9455 13.241501 17.537 15.265 17.537 18.318 C 17.537 21.0515 14.839001 23.2525 9.8335 23.2525 C 5.609 23.2525 2.8400002 20.980501 2.8400002 18.140501 C 2.8400002 16.614 3.9405 15.6555 5.2895 15.6555 C 6.887 15.6555 7.7745004 16.7915 7.7745004 18.105001 C 7.7745004 20.164001 5.8575 20.5545 5.7155004 20.59 C 6.958 21.584002 8.52 21.868 9.620501 21.868 C 12.567 21.868 12.6735 19.596 12.6735 18.424501 C 12.6735 17.963001 12.6380005 13.3125 8.946 13.099501 C 7.4905005 13.028501 7.4195004 12.993 7.242 12.9575 C 6.887 12.922001 6.816 12.567 6.816 12.354 C 6.816 11.715 7.171 11.715 7.8100004 11.715 L 9.372001 11.715 C 13.241501 11.715 13.241501 8.236 13.241501 6.4255004 C 13.241501 4.757 13.241501 1.136 9.5495 1.136 C 8.6265 1.136 6.7805004 1.278 5.0765 2.3430002 C 6.248 2.6625001 7.1355004 3.5500002 7.1355004 5.0055003 C 7.1355004 6.603 5.9995003 7.7035003 4.4375 7.7035003 C 2.9465 7.7035003 1.704 6.7450004 1.704 4.9345 C 1.704 1.7395 5.1475 -0.3905 9.727 -0.3905 C 16.081501 -0.3905 18.673 3.1595001 18.673 6.3900003 Z "/>
</symbol>
<symbol id="g3F9EB82CA73B0CEFA6A6BBB3DF89DD91" overflow="visible">
<path d="M 21.6161 7.9464 C 21.6161 9.6491995 21.0958 11.777699 19.298399 13.7643 C 18.3997 14.7576 17.6429 15.2306 14.6157 17.1226 C 18.0213 18.8727 20.339 21.3323 20.339 24.4541 C 20.339 28.8057 16.1293 31.5018 11.825 31.5018 C 7.095 31.5018 3.2637 28.0016 3.2637 23.6027 C 3.2637 22.7513 3.3583 20.6228 5.3449 18.3997 C 5.8652 17.8321 7.6153 16.6496 8.7978 15.8455 C 6.0544 14.4738 1.9866 11.825 1.9866 7.1423 C 1.9866 2.1285 6.8112 -1.0406 11.777699 -1.0406 C 17.1226 -1.0406 21.6161 2.8853 21.6161 7.9464 Z M 18.2578 24.4541 C 18.2578 21.758 16.4131 19.4876 13.5751 17.8321 L 7.7099 21.6161 C 5.5341 23.035099 5.3449 24.6433 5.3449 25.4474 C 5.3449 28.3327 8.4194 30.3193 11.777699 30.3193 C 15.2306 30.3193 18.2578 27.8597 18.2578 24.4541 Z M 19.2511 6.2436 C 19.2511 2.7434 15.7036 0.2838 11.825 0.2838 C 7.7572 0.2838 4.3516 3.2164 4.3516 7.1423 C 4.3516 9.8857 5.8652 12.9129 9.8857 15.136 L 15.7036 11.4466 C 17.028 10.5479 19.2511 9.1289 19.2511 6.2436 Z "/>
</symbol>
<symbol id="gB3094794D0628B160CB8E5B1E6D90854" overflow="visible">
<path d="M 19.241001 0 L 19.241001 1.6685001 L 15.797501 1.6685001 L 15.797501 5.538 L 19.241001 5.538 L 19.241001 7.2065 L 15.797501 7.2065 L 15.797501 22.152 C 15.797501 23.1105 15.7265005 23.288 14.7325 23.288 C 13.987 23.288 13.951501 23.2525 13.5255 22.720001 L 1.136 7.2065 L 1.136 5.538 L 11.360001 5.538 L 11.360001 1.6685001 L 7.3840003 1.6685001 L 7.3840003 0 C 8.733001 0.1065 11.9635 0.1065 13.490001 0.1065 C 14.910001 0.1065 17.9985 0.1065 19.241001 0 Z M 11.750501 7.2065 L 2.982 7.2065 L 11.750501 18.2115 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -1,578 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE52AD68C7B06D7D319E57C0DFEC4A716" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
<symbol id="gE52AD68C7B06D7D319E57C0DFEC4A716" overflow="visible">
<path d="M 28.791 10.584001 C 28.791 12.852 28.098 15.687 25.704 18.333 C 24.507 19.656 23.499 20.286001 19.467001 22.806 C 24.003 25.137001 27.09 28.413 27.09 32.571 C 27.09 38.367 21.483 41.958 15.75 41.958 C 9.45 41.958 4.347 37.296 4.347 31.437 C 4.347 30.303001 4.473 27.468 7.119 24.507 C 7.8120003 23.751 10.143001 22.176 11.718 21.105 C 8.064 19.278 2.6460001 15.75 2.6460001 9.5130005 C 2.6460001 2.835 9.0720005 -1.386 15.687 -1.386 C 22.806 -1.386 28.791 3.8430002 28.791 10.584001 Z M 24.318 32.571 C 24.318 28.98 21.861 25.956001 18.081 23.751 L 10.269 28.791 C 7.3710003 30.681 7.119 32.823 7.119 33.894 C 7.119 37.737 11.214 40.383 15.687 40.383 C 20.286001 40.383 24.318 37.107002 24.318 32.571 Z M 25.641 8.316 C 25.641 3.654 20.916 0.37800002 15.75 0.37800002 C 10.332 0.37800002 5.796 4.284 5.796 9.5130005 C 5.796 13.167 7.8120003 17.199 13.167 20.16 L 20.916 15.246 C 22.68 14.049 25.641 12.159 25.641 8.316 Z "/>
</symbol>
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
</symbol>
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,686 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g445DAEA1F6DE410BE2757A1979F4607A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(182 236.39999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="#000000" stroke-width="0.5" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="g445DAEA1F6DE410BE2757A1979F4607A" overflow="visible">
<path d="M 30.555 40.572002 L 15.246 40.572002 C 7.56 40.572002 7.434 41.391 7.182 42.588 L 5.607 42.588 L 3.528 29.61 L 5.103 29.61 C 5.2920003 30.618 5.859 34.587 6.678 35.343002 C 7.119 35.721 12.033 35.721 12.852 35.721 L 25.893 35.721 L 18.837 25.767 C 13.167 17.262001 11.088 8.505 11.088 2.079 C 11.088 1.449 11.088 -1.386 13.986 -1.386 C 16.884 -1.386 16.884 1.449 16.884 2.079 L 16.884 5.2920003 C 16.884 8.757 17.073 12.222 17.577 15.624001 C 17.829 17.073 18.711 22.491001 21.483 26.397001 L 29.988 38.367 C 30.555 39.123 30.555 39.249 30.555 40.572002 Z "/>
</symbol>
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -1,575 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6ACD2AFE5A142413658FF72A74B119FA" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
<symbol id="g6ACD2AFE5A142413658FF72A74B119FA" overflow="visible">
<path d="M 28.791 12.852 C 28.791 20.853 23.184 26.901001 16.191 26.901001 C 11.907001 26.901001 9.576 23.688 8.316 20.664 L 8.316 22.176 C 8.316 38.115 16.128 40.383 19.341 40.383 C 20.853 40.383 23.499 40.005 24.885 37.863 C 23.94 37.863 21.42 37.863 21.42 35.028 C 21.42 33.075 22.932001 32.13 24.318 32.13 C 25.326 32.13 27.216 32.697002 27.216 35.154 C 27.216 38.934002 24.444 41.958 19.215 41.958 C 11.151 41.958 2.6460001 33.831 2.6460001 19.908 C 2.6460001 3.0870001 9.954 -1.386 15.813001 -1.386 C 22.806 -1.386 28.791 4.5360003 28.791 12.852 Z M 23.121 12.915 C 23.121 9.891 23.121 6.741 22.050001 4.473 C 20.16 0.693 17.262001 0.37800002 15.813001 0.37800002 C 11.844 0.37800002 9.954 4.158 9.576 5.103 C 8.442 8.064 8.442 13.104 8.442 14.238 C 8.442 19.152 10.458 25.452 16.128 25.452 C 17.136 25.452 20.034 25.452 21.987 21.546 C 23.121 19.215 23.121 16.002 23.121 12.915 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,578 +0,0 @@
<svg class="typst-doc" viewBox="0 0 612 792" width="612pt" height="792pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 792 L 612 792 L 612 0 Z "/>
<g>
<g transform="translate(28.800000000000004 28.800000000000004)">
<g class="typst-group">
<g>
<g transform="translate(0 8.196)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g6A26343CCF6197143E5E85002E6F4A7F" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="7.668" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB2CD2AF1A15A18C21044116735E439FA" x="13.032" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE18CD5E7B4B73FBDC01CD83F41E4944" x="20.7" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D97D0584DA925FD6772C8239C133337" x="28.368" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gAA925F3DC31586D477A84606A5396DB1" x="34.692" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g6D7C89EE23EB52047E1CF3EC7BD6584D" x="42.36" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(472.3544 7.1032)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g61BFD1E59A0EA46D23DE3D3531CF6BB" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE15E804018FC330B909226FC3C2A39F" x="7.799999999999999" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g17F221B61A8A9C38D306F63946E0648C" x="13" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="18.4912" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g16DCF5BD84073BD85AAEA4AEB890040C" x="23.1088" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g195FB46CF1F0D64D13ABD034CB02F9FA" x="31.772" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g15A35E6942E714BAE3FF6D27DBABBD3F" x="37.5544" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gADC3471E6715FB83C2C8FB541E04CC53" x="42.172000000000004" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g72F3DE5A12C199E62701347AC33B2BD4" x="49.701600000000006" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g125F7016E572FCBFF0F7D1272831D0BB" x="54.90160000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="61.24560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g22E8FEEB8A09F4E7A02BA29DD5638F92" x="66.44560000000001" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gF37BF10C38718313429FD9558CC0AC07" x="71.64560000000002" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g26DE8D7E84970EBC6DE3F861A3592734" x="76.84560000000002" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
<g transform="translate(0 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gB9B3B536283EFED4367465648CB47304" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE52AD68C7B06D7D319E57C0DFEC4A716" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 34.596)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gBBD3658F993256FB14CAE74CD19D9559" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC09EAD757457326F10709AC2E369AE7F" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#g3113712160E3A2B5B8B27AA52868E5B5" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9F43054950194F5A90237C32918D5B7" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(277.2 389.196)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="#888888" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" stroke-dashoffset="0" stroke-dasharray="3 3" d="M 0 0 L 0 354.6 L 277.2 354.6 L 277.2 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(5.76 35.905899999999995)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE04FE3F0A0616330E6EC92F519041402" x="0" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gE53B4361DF76084EEECF5E79CA66DB66" x="45.313399999999994" fill="#000000" fill-rule="nonzero"/>
<use xlink:href="#gC5B21EAC21BDA69F66A33CFBF1F9F281" x="72.51089999999999" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 37.65295000000001)">
<g class="typst-group">
<g>
<g transform="translate(103.20000000000002 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g54070CA86B054030C4E72E2BCD3E257C" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 78.79999999999998)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gE7DD47BEFFE2190835AC6B12E1E487ED" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-18.99999999999997 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(14.892999999999994 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gEAE4828ADF9944B502E8283DA1B392CB" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(103.20000000000002 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g3F6E68284F2A6689C7073689FF206CEC" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(182 157.59999999999997)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 78.8 L 78.8 78.8 L 78.8 0 Z "/>
</g>
<g transform="translate(23.649999999999995 60.9145)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g8DFC31EF140D835FC5E5720919E30CDE" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(24.400000000000023 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(103.20000000000002 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
<g transform="translate(182 236.39999999999998)">
<path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 78.8 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g6A26343CCF6197143E5E85002E6F4A7F" overflow="visible">
<path d="M 6.888 2.436 C 6.888 3.7680001 5.916 4.7400002 4.824 4.968 L 3.084 5.34 C 2.604 5.448 1.932 5.856 1.932 6.588 C 1.932 7.104 2.2680001 7.848 3.468 7.848 C 4.428 7.848 5.64 7.44 5.916 5.808 C 5.964 5.52 5.964 5.496 6.216 5.496 C 6.504 5.496 6.504 5.556 6.504 5.8320003 L 6.504 8.028 C 6.504 8.2560005 6.504 8.364 6.288 8.364 C 6.192 8.364 6.18 8.352 6.048 8.232 L 5.508 7.704 C 4.8120003 8.2560005 4.032 8.364 3.456 8.364 C 1.632 8.364 0.768 7.212 0.768 5.952 C 0.768 5.172 1.164 4.62 1.416 4.356 C 2.004 3.7680001 2.412 3.684 3.72 3.3960001 C 4.776 3.168 4.98 3.132 5.244 2.88 C 5.4240003 2.7 5.724 2.388 5.724 1.836 C 5.724 1.26 5.412 0.432 4.164 0.432 C 3.252 0.432 1.428 0.672 1.332 2.46 C 1.32 2.676 1.32 2.736 1.056 2.736 C 0.768 2.736 0.768 2.664 0.768 2.388 L 0.768 0.204 C 0.768 -0.024 0.768 -0.132 0.984 -0.132 C 1.092 -0.132 1.116 -0.108 1.212 -0.024 L 1.764 0.528 C 2.556 -0.060000002 3.672 -0.132 4.164 -0.132 C 6.144 -0.132 6.888 1.224 6.888 2.436 Z "/>
</symbol>
<symbol id="g6D7C89EE23EB52047E1CF3EC7BD6584D" overflow="visible">
<path d="M 4.584 1.488 L 4.584 2.124 L 4.02 2.124 L 4.02 1.512 C 4.02 0.696 3.636 0.408 3.3 0.408 C 2.604 0.408 2.604 1.176 2.604 1.452 L 2.604 4.764 L 4.356 4.764 L 4.356 5.328 L 2.604 5.328 L 2.604 7.62 L 2.04 7.62 C 2.028 6.42 1.44 5.232 0.252 5.196 L 0.252 4.764 L 1.2360001 4.764 L 1.2360001 1.4760001 C 1.2360001 0.192 2.28 -0.072 3.132 -0.072 C 4.044 -0.072 4.584 0.612 4.584 1.488 Z "/>
</symbol>
<symbol id="gB2CD2AF1A15A18C21044116735E439FA" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 C 6.636 0.564 6.552 0.564 6.552 1.0320001 L 6.552 5.4 L 4.356 5.304 L 4.356 4.7400002 C 5.1 4.7400002 5.184 4.7400002 5.184 4.272 L 5.184 1.98 C 5.184 0.996 4.572 0.36 3.696 0.36 C 2.772 0.36 2.736 0.66 2.736 1.308 L 2.736 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 1.4760001 C 1.368 0.192 2.34 -0.072 3.528 -0.072 C 3.8400002 -0.072 4.704 -0.072 5.256 0.864 L 5.256 -0.072 Z "/>
</symbol>
<symbol id="gE18CD5E7B4B73FBDC01CD83F41E4944" overflow="visible">
<path d="M 7.212 0 L 7.212 0.564 C 6.468 0.564 6.384 0.564 6.384 1.0320001 L 6.384 8.328 L 4.26 8.232 L 4.26 7.668 C 5.004 7.668 5.088 7.668 5.088 7.2000003 L 5.088 4.86 C 4.488 5.328 3.864 5.4 3.468 5.4 C 1.716 5.4 0.456 4.344 0.456 2.652 C 0.456 1.068 1.5600001 -0.072 3.336 -0.072 C 4.068 -0.072 4.644 0.216 5.0160003 0.51600003 L 5.0160003 -0.072 Z M 5.0160003 1.2360001 C 4.86 1.02 4.368 0.36 3.456 0.36 C 1.992 0.36 1.992 1.812 1.992 2.652 C 1.992 3.228 1.992 3.876 2.304 4.344 C 2.652 4.848 3.216 4.968 3.588 4.968 C 4.272 4.968 4.752 4.584 5.0160003 4.236 Z "/>
</symbol>
<symbol id="g6D97D0584DA925FD6772C8239C133337" overflow="visible">
<path d="M 5.928 1.404 C 5.928 1.62 5.7000003 1.62 5.64 1.62 C 5.436 1.62 5.412 1.5600001 5.34 1.368 C 5.088 0.792 4.404 0.408 3.624 0.408 C 1.932 0.408 1.9200001 2.004 1.9200001 2.616 L 5.544 2.616 C 5.808 2.616 5.928 2.616 5.928 2.94 C 5.928 3.312 5.856 4.188 5.256 4.788 C 4.8120003 5.2200003 4.176 5.436 3.348 5.436 C 1.428 5.436 0.384 4.2 0.384 2.7 C 0.384 1.092 1.584 -0.072 3.516 -0.072 C 5.412 -0.072 5.928 1.2 5.928 1.404 Z M 4.788 3.012 L 1.9200001 3.012 C 1.944 3.48 1.956 3.984 2.208 4.38 C 2.52 4.86 3 5.004 3.348 5.004 C 4.752 5.004 4.776 3.432 4.788 3.012 Z "/>
</symbol>
<symbol id="gAA925F3DC31586D477A84606A5396DB1" overflow="visible">
<path d="M 7.38 0 L 7.38 0.564 L 6.552 0.564 L 6.552 3.672 C 6.552 4.932 5.9040003 5.4 4.704 5.4 C 3.552 5.4 2.9160001 4.716 2.604 4.104 L 2.604 5.4 L 0.54 5.304 L 0.54 4.7400002 C 1.284 4.7400002 1.368 4.7400002 1.368 4.272 L 1.368 0.564 L 0.54 0.564 L 0.54 0 L 2.052 0.036 L 3.5640001 0 L 3.5640001 0.564 L 2.736 0.564 L 2.736 3.072 C 2.736 4.38 3.7680001 4.968 4.524 4.968 C 4.932 4.968 5.184 4.716 5.184 3.8040001 L 5.184 0.564 L 4.356 0.564 L 4.356 0 L 5.868 0.036 Z "/>
</symbol>
<symbol id="g61BFD1E59A0EA46D23DE3D3531CF6BB" overflow="visible">
<path d="M 7.4464 6.7808 L 7.4464 7.1032 L 6.2296 7.072 L 5.0128 7.1032 L 5.0128 6.7808 C 6.084 6.7808 6.084 6.292 6.084 6.0112 L 6.084 1.5704 L 2.4128 6.968 C 2.3192 7.0928 2.3088 7.1032 2.1112 7.1032 L 0.3432 7.1032 L 0.3432 6.7808 L 0.6448 6.7808 C 0.8008 6.7808 1.0088 6.7704 1.1648 6.76 C 1.404 6.7288 1.4144 6.7184 1.4144 6.5208 L 1.4144 1.092 C 1.4144 0.8112 1.4144 0.3224 0.3432 0.3224 L 0.3432 0 L 1.5600001 0.0312 L 2.7768 0 L 2.7768 0.3224 C 1.7056 0.3224 1.7056 0.8112 1.7056 1.092 L 1.7056 6.5 C 1.7576 6.448 1.768 6.4376 1.8096 6.3752 L 6.0528 0.1352 C 6.1464 0.0104 6.1568 0 6.2296 0 C 6.3752 0 6.3752 0.0728 6.3752 0.2704 L 6.3752 6.0112 C 6.3752 6.292 6.3752 6.7808 7.4464 6.7808 Z "/>
</symbol>
<symbol id="gE15E804018FC330B909226FC3C2A39F" overflow="visible">
<path d="M 4.8984 2.2256 C 4.8984 3.5568001 3.8584 4.6592 2.6 4.6592 C 1.3 4.6592 0.2912 3.5256 0.2912 2.2256 C 0.2912 0.884 1.3728 -0.1144 2.5896 -0.1144 C 3.848 -0.1144 4.8984 0.9048 4.8984 2.2256 Z M 4.0352 2.3088 C 4.0352 1.9344 4.0352 1.3728 3.8064 0.9152 C 3.5776 0.4472 3.1200001 0.1456 2.6 0.1456 C 2.1528 0.1456 1.6952 0.364 1.4144 0.8424 C 1.1544 1.3 1.1544 1.9344 1.1544 2.3088 C 1.1544 2.7144 1.1544 3.276 1.404 3.7336 C 1.6848 4.212 2.1736 4.4304 2.5896 4.4304 C 3.0472 4.4304 3.4944 4.2016 3.7648 3.7544 C 4.0352 3.3072 4.0352 2.704 4.0352 2.3088 Z "/>
</symbol>
<symbol id="g17F221B61A8A9C38D306F63946E0648C" overflow="visible">
<path d="M 5.2832 4.16 L 5.2832 4.4824 C 5.044 4.4616 4.7424 4.4512 4.5032 4.4512 L 3.5984 4.4824 L 3.5984 4.16 C 3.9832 4.1496 4.0976 3.9104 4.0976 3.7128 C 4.0976 3.6192 4.0768 3.5776 4.0352 3.4632 L 2.9744 0.8112 L 1.8096 3.7128 C 1.7472 3.848 1.7472 3.8896 1.7472 3.8896 C 1.7472 4.16 2.1528 4.16 2.34 4.16 L 2.34 4.4824 L 1.2064 4.4512 C 0.9256 4.4512 0.5096 4.4616 0.1976 4.4824 L 0.1976 4.16 C 0.8528 4.16 0.8944 4.0976 1.0296 3.7752001 L 2.5272 0.0832 C 2.5896 -0.0624 2.6104 -0.1144 2.7456 -0.1144 C 2.8808 -0.1144 2.9224 -0.0208 2.964 0.0832 L 4.3264 3.4632 C 4.42 3.7024 4.5968 4.1496 5.2832 4.16 Z "/>
</symbol>
<symbol id="g15A35E6942E714BAE3FF6D27DBABBD3F" overflow="visible">
<path d="M 4.316 1.2376 C 4.316 1.3416001 4.2328 1.3624 4.1808 1.3624 C 4.0872 1.3624 4.0664 1.3 4.0456 1.2168 C 3.6816 0.1456 2.7456 0.1456 2.6416 0.1456 C 2.1216 0.1456 1.7056 0.4576 1.4664 0.8424 C 1.1544 1.3416001 1.1544 2.028 1.1544 2.4024 L 4.056 2.4024 C 4.2848 2.4024 4.316 2.4024 4.316 2.6208 C 4.316 3.6504 3.7544 4.6592 2.4544 4.6592 C 1.248 4.6592 0.2912 3.588 0.2912 2.288 C 0.2912 0.8944 1.3832 -0.1144 2.5792 -0.1144 C 3.848 -0.1144 4.316 1.04 4.316 1.2376 Z M 3.6296 2.6208 L 1.1648 2.6208 C 1.2272 4.1704 2.1008 4.4304 2.4544 4.4304 C 3.5256 4.4304 3.6296 3.0264 3.6296 2.6208 Z "/>
</symbol>
<symbol id="g16DCF5BD84073BD85AAEA4AEB890040C" overflow="visible">
<path d="M 8.4552 0 L 8.4552 0.3224 C 7.9144 0.3224 7.6544 0.3224 7.644 0.6344 L 7.644 2.6208 C 7.644 3.5152 7.644 3.8376 7.3216 4.212 C 7.176 4.3888 6.8328 4.5968 6.2296 4.5968 C 5.356 4.5968 4.8984 3.9728 4.7216 3.5776 C 4.576 4.4824 3.8064 4.5968 3.3384001 4.5968 C 2.5792 4.5968 2.0904 4.1496 1.7992 3.5048 L 1.7992 4.5968 L 0.3328 4.4824 L 0.3328 4.16 C 1.0608 4.16 1.144 4.0872 1.144 3.5776 L 1.144 0.7904 C 1.144 0.3224 1.0296 0.3224 0.3328 0.3224 L 0.3328 0 L 1.508 0.0312 L 2.6728 0 L 2.6728 0.3224 C 1.976 0.3224 1.8616 0.3224 1.8616 0.7904 L 1.8616 2.704 C 1.8616 3.7856 2.6 4.368 3.2656 4.368 C 3.9208 4.368 4.0352 3.8064 4.0352 3.2136 L 4.0352 0.7904 C 4.0352 0.3224 3.9208 0.3224 3.224 0.3224 L 3.224 0 L 4.3992 0.0312 L 5.564 0 L 5.564 0.3224 C 4.8672 0.3224 4.7528 0.3224 4.7528 0.7904 L 4.7528 2.704 C 4.7528 3.7856 5.4912 4.368 6.1568 4.368 C 6.812 4.368 6.9264 3.8064 6.9264 3.2136 L 6.9264 0.7904 C 6.9264 0.3224 6.812 0.3224 6.1152 0.3224 L 6.1152 0 L 7.2904 0.0312 Z "/>
</symbol>
<symbol id="g195FB46CF1F0D64D13ABD034CB02F9FA" overflow="visible">
<path d="M 5.4184 2.2464 C 5.4184 3.5672 4.3992 4.5968 3.2136 4.5968 C 2.4024 4.5968 1.9552 4.108 1.7888 3.9208 L 1.7888 7.2176 L 0.2912 7.1032 L 0.2912 6.7808 C 1.0192 6.7808 1.1024 6.708 1.1024 6.1984 L 1.1024 0 L 1.3624 0 L 1.7368 0.6448 C 1.8928 0.4056 2.3296 -0.1144 3.0992 -0.1144 C 4.3368 -0.1144 5.4184 0.9048 5.4184 2.2464 Z M 4.5552 2.2568 C 4.5552 1.872 4.5344 1.248 4.2328 0.78000003 C 4.0144 0.4576 3.6192 0.1144 3.0576 0.1144 C 2.5896 0.1144 2.2152 0.364 1.9656 0.7488 C 1.82 0.9672 1.82 0.9984 1.82 1.1856 L 1.82 3.328 C 1.82 3.5256 1.82 3.536 1.9344 3.7024 C 2.34 4.2848 2.912 4.368 3.1616 4.368 C 3.6296 4.368 4.004 4.0976 4.2536 3.7024 C 4.524 3.276 4.5552 2.6832001 4.5552 2.2568 Z "/>
</symbol>
<symbol id="gADC3471E6715FB83C2C8FB541E04CC53" overflow="visible">
<path d="M 3.7856 3.9624 C 3.7856 4.2952 3.4632 4.5968 3.016 4.5968 C 2.2568 4.5968 1.8824 3.9 1.7368 3.4528 L 1.7368 4.5968 L 0.2912 4.4824 L 0.2912 4.16 C 1.0192 4.16 1.1024 4.0872 1.1024 3.5776 L 1.1024 0.7904 C 1.1024 0.3224 0.988 0.3224 0.2912 0.3224 L 0.2912 0 L 1.4768 0.0312 C 1.8928 0.0312 2.3816 0.0312 2.7976 0 L 2.7976 0.3224 L 2.5792 0.3224 C 1.8096 0.3224 1.7888 0.4368 1.7888 0.8112 L 1.7888 2.4128 C 1.7888 3.4424 2.2256 4.368 3.016 4.368 C 3.0888 4.368 3.1096 4.368 3.1304 4.3576 C 3.0992 4.3472 2.8912 4.2224 2.8912 3.952 C 2.8912 3.6608 3.1096 3.5048 3.3384001 3.5048 C 3.5256 3.5048 3.7856 3.6296 3.7856 3.9624 Z "/>
</symbol>
<symbol id="g72F3DE5A12C199E62701347AC33B2BD4" overflow="visible">
<path d="M 5.044 6.6976 L 2.5168 6.6976 C 1.248 6.6976 1.2272 6.8328 1.1856 7.0304 L 0.9256 7.0304 L 0.5824 4.888 L 0.8424 4.888 C 0.8736 5.0544 0.9672 5.7096 1.1024 5.8344 C 1.1752 5.8968 1.9864 5.8968 2.1216 5.8968 L 4.2744 5.8968 L 3.1096 4.2536 C 2.1736 2.8496 1.8304 1.404 1.8304 0.3432 C 1.8304 0.2392 1.8304 -0.2288 2.3088 -0.2288 C 2.7872 -0.2288 2.7872 0.2392 2.7872 0.3432 L 2.7872 0.8736 C 2.7872 1.4456 2.8184 2.0176 2.9016001 2.5792 C 2.9432 2.8184 3.0888 3.7128 3.5464 4.3576 L 4.9504 6.3336 C 5.044 6.4584002 5.044 6.4792 5.044 6.6976 Z "/>
</symbol>
<symbol id="g125F7016E572FCBFF0F7D1272831D0BB" overflow="visible">
<path d="M 2.1112 0.0104 C 2.1112 0.6968 1.8512 1.1024 1.4456 1.1024 C 1.1024 1.1024 0.8944 0.8424 0.8944 0.5512 C 0.8944 0.2704 1.1024 0 1.4456 0 C 1.5704 0 1.7056 0.0416 1.8096 0.1352 C 1.8408 0.156 1.8616 0.1664 1.8616 0.1664 C 1.8616 0.1664 1.8824 0.156 1.8824 0.0104 C 1.8824 -0.7592 1.5184 -1.3832 1.1752 -1.7264 C 1.0608 -1.8408 1.0608 -1.8616 1.0608 -1.8928 C 1.0608 -1.9656 1.1128 -2.0072 1.1648 -2.0072 C 1.2792 -2.0072 2.1112 -1.2064 2.1112 0.0104 Z "/>
</symbol>
<symbol id="gF37BF10C38718313429FD9558CC0AC07" overflow="visible">
<path d="M 4.6696 1.8096 L 4.4096 1.8096 C 4.3576 1.4976 4.2848 1.04 4.1808 0.884 C 4.108 0.8008 3.4216 0.8008 3.1928 0.8008 L 1.3208 0.8008 L 2.4232 1.872 C 4.0456 3.3072 4.6696 3.8688 4.6696 4.9088 C 4.6696 6.0944 3.7336 6.9264 2.4648001 6.9264 C 1.2896 6.9264 0.52 5.9696 0.52 5.044 C 0.52 4.4616 1.04 4.4616 1.0712 4.4616 C 1.248 4.4616 1.612 4.5864 1.612 5.0128 C 1.612 5.2832 1.4248 5.5536 1.0608 5.5536 C 0.9776 5.5536 0.9568 5.5536 0.9256 5.5432 C 1.1648 6.2192 1.7264 6.604 2.3296 6.604 C 3.276 6.604 3.7232 5.7616 3.7232 4.9088 C 3.7232 4.0768 3.2032 3.2552 2.6312 2.6104 L 0.6344 0.3848 C 0.52 0.2704 0.52 0.2496 0.52 0 L 4.3784 0 Z "/>
</symbol>
<symbol id="g22E8FEEB8A09F4E7A02BA29DD5638F92" overflow="visible">
<path d="M 4.784 3.328 C 4.784 4.16 4.732 4.992 4.368 5.7616 C 3.8896 6.76 3.0368 6.9264 2.6 6.9264 C 1.976 6.9264 1.2168 6.656 0.7904 5.6888 C 0.4576 4.9712 0.4056 4.16 0.4056 3.328 C 0.4056 2.548 0.4472 1.612 0.8736 0.8216 C 1.3208 -0.0208 2.08 -0.2288 2.5896 -0.2288 C 3.1512 -0.2288 3.9416 -0.0104 4.3992 0.9776 C 4.732 1.6952 4.784 2.5064 4.784 3.328 Z M 3.9208 3.4528 C 3.9208 2.6728 3.9208 1.9656 3.8064 1.3 C 3.6504 0.312 3.0576 0 2.5896 0 C 2.184 0 1.5704 0.26 1.3832 1.2584 C 1.2688 1.8824 1.2688 2.8392 1.2688 3.4528 C 1.2688 4.1184 1.2688 4.8048 1.352 5.3664002 C 1.5496 6.604 2.3296 6.6976 2.5896 6.6976 C 2.9328 6.6976 3.6192 6.5104 3.8168 5.4808 C 3.9208 4.8984 3.9208 4.108 3.9208 3.4528 Z "/>
</symbol>
<symbol id="g26DE8D7E84970EBC6DE3F861A3592734" overflow="visible">
<path d="M 4.6696 2.0904 C 4.6696 3.328 3.8168 4.368 2.6936 4.368 C 2.1944 4.368 1.7472 4.2016 1.3728 3.8376 L 1.3728 5.8656 C 1.5808 5.8032002 1.924 5.7304 2.2568 5.7304 C 3.536 5.7304 4.264 6.6768003 4.264 6.812 C 4.264 6.8744 4.2328 6.9264 4.16 6.9264 C 4.16 6.9264 4.1288 6.9264 4.0768 6.8952003 C 3.8688 6.8016 3.3592 6.5936 2.6624 6.5936 C 2.2464 6.5936 1.768 6.6664 1.2792 6.8848 C 1.196 6.916 1.1544 6.916 1.1544 6.916 C 1.0504 6.916 1.0504 6.8328 1.0504 6.6664 L 1.0504 3.588 C 1.0504 3.4008 1.0504 3.3176 1.196 3.3176 C 1.2688 3.3176 1.2896 3.3488 1.3312 3.4112 C 1.4456 3.5776 1.8304 4.1392 2.6728 4.1392 C 3.2136 4.1392 3.4736 3.6608 3.5568001 3.4736 C 3.7232 3.0888 3.744 2.6832001 3.744 2.1632 C 3.744 1.7992 3.744 1.1752 3.4944 0.7384 C 3.2448 0.3328 2.86 0.0624 2.3816 0.0624 C 1.6224 0.0624 1.0296 0.6136 0.8528 1.2272 C 0.884 1.2168 0.9152 1.2064 1.0296 1.2064 C 1.3728 1.2064 1.5496 1.4664 1.5496 1.716 C 1.5496 1.9656 1.3728 2.2256 1.0296 2.2256 C 0.884 2.2256 0.52 2.1528 0.52 1.6744 C 0.52 0.78000003 1.2376 -0.2288 2.4024 -0.2288 C 3.6088 -0.2288 4.6696 0.7696 4.6696 2.0904 Z "/>
</symbol>
<symbol id="gE04FE3F0A0616330E6EC92F519041402" overflow="visible">
<path d="M 42.2389 6.5747 C 42.2389 7.9937 40.8672 7.9937 40.2996 7.9937 L 29.0422 7.9937 L 31.3599 15.7036 L 40.2996 15.7036 C 40.8672 15.7036 42.2389 15.7036 42.2389 17.1226 C 42.2389 18.5889 40.7726 18.5889 40.0631 18.5889 L 32.3059 18.5889 L 35.9953 30.4612 C 36.1845 31.0288 36.1845 31.0761 36.1845 31.4072 C 36.1845 32.1167 35.6169 32.8262 34.7182 32.8262 C 33.6776 32.8262 33.4411 32.0221 33.2519 31.4072 L 29.2787 18.5889 L 20.1971 18.5889 L 23.8865 30.4612 C 24.0757 31.0288 24.0757 31.0761 24.0757 31.4072 C 24.0757 32.1167 23.5081 32.8262 22.6094 32.8262 C 21.5688 32.8262 21.3323 32.0221 21.1431 31.4072 L 17.169899 18.5889 L 5.203 18.5889 C 4.4934998 18.5889 3.0272 18.5889 3.0272 17.1226 C 3.0272 15.7036 4.3989 15.7036 4.9665 15.7036 L 16.2239 15.7036 L 13.906199 7.9937 L 4.9665 7.9937 C 4.3989 7.9937 3.0272 7.9937 3.0272 6.5747 C 3.0272 5.1084 4.4934998 5.1084 5.203 5.1084 L 12.9602 5.1084 L 9.2708 -6.8112 C 9.1762 -7.095 9.0816 -7.3788 9.0816 -7.7572 C 9.0816 -8.4667 9.6491995 -9.1762 10.5479 -9.1762 C 11.5412 -9.1762 11.777699 -8.4194 11.9669 -7.8518 L 15.9874 5.1084 L 25.069 5.1084 L 21.3796 -6.8112 C 21.285 -7.095 21.1904 -7.3788 21.1904 -7.7572 C 21.1904 -8.4667 21.758 -9.1762 22.6567 -9.1762 C 23.65 -9.1762 23.8865 -8.4194 24.0757 -7.8518 L 28.096199 5.1084 L 40.0631 5.1084 C 40.7726 5.1084 42.2389 5.1084 42.2389 6.5747 Z M 28.3327 15.7036 L 26.015 7.9937 L 16.9334 7.9937 L 19.2511 15.7036 Z "/>
</symbol>
<symbol id="gB9B3B536283EFED4367465648CB47304" overflow="visible">
<path d="M 23.3662 0 L 23.3662 2.2231 L 16.7442 2.2231 L 16.7442 29.4679 C 16.7442 30.5085 16.7442 30.9815 15.5144 30.9815 C 14.9941 30.9815 14.8995 30.9815 14.4738 30.6504 C 10.8317 27.9543 5.9598 27.9543 4.9665 27.9543 L 4.0205 27.9543 L 4.0205 25.7312 L 4.9665 25.7312 C 5.7233 25.7312 8.3248 25.7785 11.1154995 26.6772 L 11.1154995 2.2231 L 4.5408 2.2231 L 4.5408 0 C 6.6219997 0.1419 11.6358 0.1419 13.9535 0.1419 C 16.2712 0.1419 21.285 0.1419 23.3662 0 Z "/>
</symbol>
<symbol id="gC5B21EAC21BDA69F66A33CFBF1F9F281" overflow="visible">
<path d="M 11.2101 3.6894 C 11.2101 5.7233 9.5546 7.3788 7.5207 7.3788 C 5.4868 7.3788 3.8313 5.7233 3.8313 3.6894 C 3.8313 1.6554999 5.4868 0 7.5207 0 C 9.5546 0 11.2101 1.6554999 11.2101 3.6894 Z "/>
</symbol>
<symbol id="g8DFC31EF140D835FC5E5720919E30CDE" overflow="visible">
<path d="M 28.791 10.773 C 28.791 15.939 24.822 20.853 18.27 22.176 C 23.436 23.877 27.09 28.287 27.09 33.264 C 27.09 38.43 21.546 41.958 15.498 41.958 C 9.135 41.958 4.347 38.178 4.347 33.39 C 4.347 31.311 5.7330003 30.114 7.56 30.114 C 9.5130005 30.114 10.773 31.5 10.773 33.327 C 10.773 36.477 7.8120003 36.477 6.867 36.477 C 8.82 39.564 12.978001 40.383 15.246 40.383 C 17.829 40.383 21.294 38.997 21.294 33.327 C 21.294 32.571 21.168001 28.917 19.53 26.145 C 17.64 23.121 15.498 22.932001 13.923 22.869 C 13.419001 22.806 11.907001 22.68 11.466001 22.68 C 10.962 22.617 10.521 22.554 10.521 21.924 C 10.521 21.231 10.962 21.231 12.033 21.231 L 14.805 21.231 C 19.971 21.231 22.302 16.947 22.302 10.773 C 22.302 2.205 17.955 0.37800002 15.183001 0.37800002 C 12.474 0.37800002 7.749 1.449 5.544 5.166 C 7.749 4.8510003 9.702001 6.237 9.702001 8.6310005 C 9.702001 10.899 8.001 12.159 6.1740003 12.159 C 4.662 12.159 2.6460001 11.277 2.6460001 8.505 C 2.6460001 2.772 8.505 -1.386 15.372001 -1.386 C 23.058 -1.386 28.791 4.347 28.791 10.773 Z "/>
</symbol>
<symbol id="gE52AD68C7B06D7D319E57C0DFEC4A716" overflow="visible">
<path d="M 28.791 10.584001 C 28.791 12.852 28.098 15.687 25.704 18.333 C 24.507 19.656 23.499 20.286001 19.467001 22.806 C 24.003 25.137001 27.09 28.413 27.09 32.571 C 27.09 38.367 21.483 41.958 15.75 41.958 C 9.45 41.958 4.347 37.296 4.347 31.437 C 4.347 30.303001 4.473 27.468 7.119 24.507 C 7.8120003 23.751 10.143001 22.176 11.718 21.105 C 8.064 19.278 2.6460001 15.75 2.6460001 9.5130005 C 2.6460001 2.835 9.0720005 -1.386 15.687 -1.386 C 22.806 -1.386 28.791 3.8430002 28.791 10.584001 Z M 24.318 32.571 C 24.318 28.98 21.861 25.956001 18.081 23.751 L 10.269 28.791 C 7.3710003 30.681 7.119 32.823 7.119 33.894 C 7.119 37.737 11.214 40.383 15.687 40.383 C 20.286001 40.383 24.318 37.107002 24.318 32.571 Z M 25.641 8.316 C 25.641 3.654 20.916 0.37800002 15.75 0.37800002 C 10.332 0.37800002 5.796 4.284 5.796 9.5130005 C 5.796 13.167 7.8120003 17.199 13.167 20.16 L 20.916 15.246 C 22.68 14.049 25.641 12.159 25.641 8.316 Z "/>
</symbol>
<symbol id="gEAE4828ADF9944B502E8283DA1B392CB" overflow="visible">
<path d="M 45.486 15.75 C 45.486 16.443 44.919003 17.01 44.226 17.01 L 25.767 17.01 L 25.767 35.469 C 25.767 36.162 25.2 36.729 24.507 36.729 C 23.814001 36.729 23.247 36.162 23.247 35.469 L 23.247 17.01 L 4.788 17.01 C 4.0950003 17.01 3.528 16.443 3.528 15.75 C 3.528 15.057 4.0950003 14.49 4.788 14.49 L 23.247 14.49 L 23.247 -3.969 C 23.247 -4.662 23.814001 -5.229 24.507 -5.229 C 25.2 -5.229 25.767 -4.662 25.767 -3.969 L 25.767 14.49 L 44.226 14.49 C 44.919003 14.49 45.486 15.057 45.486 15.75 Z "/>
</symbol>
<symbol id="gC09EAD757457326F10709AC2E369AE7F" overflow="visible">
<path d="M 26.397001 0 L 26.397001 1.9530001 L 24.381 1.9530001 C 18.711 1.9530001 18.522 2.6460001 18.522 4.977 L 18.522 40.32 C 18.522 41.832 18.522 41.958 17.073 41.958 C 13.167 37.926003 7.623 37.926003 5.607 37.926003 L 5.607 35.973 C 6.867 35.973 10.584001 35.973 13.860001 37.611 L 13.860001 4.977 C 13.860001 2.709 13.6710005 1.9530001 8.001 1.9530001 L 5.985 1.9530001 L 5.985 0 C 8.190001 0.18900001 13.6710005 0.18900001 16.191 0.18900001 C 18.711 0.18900001 24.192001 0.18900001 26.397001 0 Z "/>
</symbol>
<symbol id="gBBD3658F993256FB14CAE74CD19D9559" overflow="visible">
<path d="M 24.4541 10.5006 L 22.230999 10.5006 C 22.0891 9.5546 21.6634 6.5274 21.0012 6.1963 C 20.4809 5.9125 16.9334 5.9125 16.1766 5.9125 L 9.2235 5.9125 C 11.4466 7.7572 13.906199 9.7911 16.0347 11.352 C 21.426899 15.3252 24.4541 17.5483 24.4541 22.0418 C 24.4541 27.4813 19.5349 30.9815 12.8656 30.9815 C 7.1423 30.9815 2.6961 28.0489 2.6961 23.7919 C 2.6961 21.0012 4.9665 20.2917 6.1017 20.2917 C 7.6153 20.2917 9.5073 21.3323 9.5073 23.6973 C 9.5073 26.1569 7.5207 26.9137 6.8112 27.1029 C 8.1829 28.2381 9.9803 28.7584 11.6831 28.7584 C 15.7509 28.7584 17.9267 25.542 17.9267 21.9945 C 17.9267 18.7308 16.1293 15.5144 12.8183 12.1561 L 3.3109999 2.4596 C 2.6961 1.892 2.6961 1.7974 2.6961 0.8514 L 2.6961 0 L 22.9878 0 Z "/>
</symbol>
<symbol id="gE7DD47BEFFE2190835AC6B12E1E487ED" overflow="visible">
<path d="M 28.98 20.16 C 28.98 25.2 28.665 30.24 26.460001 34.902 C 23.562 40.95 18.396 41.958 15.75 41.958 C 11.97 41.958 7.3710003 40.32 4.788 34.461002 C 2.772 30.114 2.457 25.2 2.457 20.16 C 2.457 15.435 2.709 9.765 5.2920003 4.977 C 8.001 -0.126 12.6 -1.386 15.687 -1.386 C 19.089 -1.386 23.877 -0.063 26.649 5.922 C 28.665 10.269 28.98 15.183001 28.98 20.16 Z M 23.751 20.916 C 23.751 16.191 23.751 11.907001 23.058 7.875 C 22.113 1.89 18.522 0 15.687 0 C 13.2300005 0 9.5130005 1.575 8.379 7.623 C 7.6860003 11.403 7.6860003 17.199 7.6860003 20.916 C 7.6860003 24.948 7.6860003 29.106 8.190001 32.508 C 9.387 40.005 14.112 40.572002 15.687 40.572002 C 17.766 40.572002 21.924 39.438 23.121 33.201 C 23.751 29.673 23.751 24.885 23.751 20.916 Z "/>
</symbol>
<symbol id="g3113712160E3A2B5B8B27AA52868E5B5" overflow="visible">
<path d="M 24.8798 8.514 C 24.8798 11.1154995 23.5081 15.1833 16.6496 16.6496 C 19.9133 17.6429 23.3662 20.339 23.3662 24.4068 C 23.3662 28.0489 19.7714 30.9815 13.1021 30.9815 C 7.4734 30.9815 3.784 27.9543 3.784 24.1703 C 3.784 22.1364 5.2503 20.8593 7.0477 20.8593 C 9.1762 20.8593 10.3587 22.3729 10.3587 24.123 C 10.3587 26.8664 7.8045 27.3867 7.6153 27.434 C 9.2708 28.7584 11.352 29.1368 12.8183 29.1368 C 16.7442 29.1368 16.8861 26.1096 16.8861 24.5487 C 16.8861 23.9338 16.8388 17.7375 11.9196 17.4537 C 9.9803 17.3591 9.8857 17.3118 9.6491995 17.2645 C 9.1762 17.2172 9.0816 16.7442 9.0816 16.4604 C 9.0816 15.609 9.5546 15.609 10.406 15.609 L 12.4872 15.609 C 17.6429 15.609 17.6429 10.9736 17.6429 8.5613 C 17.6429 6.3382 17.6429 1.5136 12.7237 1.5136 C 11.4939 1.5136 9.0343 1.7028 6.7639 3.1218 C 8.3248 3.5475 9.5073 4.73 9.5073 6.6693 C 9.5073 8.7978 7.9937 10.2641 5.9125 10.2641 C 3.9259 10.2641 2.2704 8.9869995 2.2704 6.5747 C 2.2704 2.3177 6.8585 -0.5203 12.9602 -0.5203 C 21.426899 -0.5203 24.8798 4.2097 24.8798 8.514 Z "/>
</symbol>
<symbol id="g9F43054950194F5A90237C32918D5B7" overflow="visible">
<path d="M 28.287 10.962 L 26.712 10.962 C 26.397001 9.0720005 25.956001 6.3 25.326 5.355 C 24.885 4.8510003 20.727001 4.8510003 19.341 4.8510003 L 8.001 4.8510003 L 14.679 11.34 C 24.507 20.034 28.287 23.436 28.287 29.736 C 28.287 36.918 22.617 41.958 14.931001 41.958 C 7.8120003 41.958 3.15 36.162 3.15 30.555 C 3.15 27.027 6.3 27.027 6.4890003 27.027 C 7.56 27.027 9.765 27.783 9.765 30.366001 C 9.765 32.004 8.6310005 33.642002 6.426 33.642002 C 5.922 33.642002 5.796 33.642002 5.607 33.579002 C 7.056 37.674 10.458 40.005 14.112 40.005 C 19.845001 40.005 22.554 34.902 22.554 29.736 C 22.554 24.696001 19.404001 19.719 15.939 15.813001 L 3.8430002 2.331 C 3.15 1.638 3.15 1.5120001 3.15 0 L 26.523 0 Z "/>
</symbol>
<symbol id="g54070CA86B054030C4E72E2BCD3E257C" overflow="visible">
<path d="M 28.287 12.663 C 28.287 20.16 23.121 26.460001 16.317 26.460001 C 13.293 26.460001 10.584001 25.452 8.316 23.247 L 8.316 35.532 C 9.576 35.154 11.655 34.713 13.6710005 34.713 C 21.42 34.713 25.83 40.446 25.83 41.265 C 25.83 41.643 25.641 41.958 25.2 41.958 C 25.2 41.958 25.011 41.958 24.696001 41.769 C 23.436 41.202 20.349 39.942 16.128 39.942 C 13.608 39.942 10.71 40.383 7.749 41.706 C 7.245 41.895 6.993 41.895 6.993 41.895 C 6.363 41.895 6.363 41.391 6.363 40.383 L 6.363 21.735 C 6.363 20.601 6.363 20.097 7.245 20.097 C 7.6860003 20.097 7.8120003 20.286001 8.064 20.664 C 8.757 21.672 11.088 25.074001 16.191 25.074001 C 19.467001 25.074001 21.042 22.176 21.546 21.042 C 22.554 18.711 22.68 16.254 22.68 13.104 C 22.68 10.899 22.68 7.119 21.168001 4.473 C 19.656 2.016 17.325 0.37800002 14.427 0.37800002 C 9.828 0.37800002 6.237 3.717 5.166 7.434 C 5.355 7.3710003 5.544 7.308 6.237 7.308 C 8.316 7.308 9.387 8.883 9.387 10.395 C 9.387 11.907001 8.316 13.482 6.237 13.482 C 5.355 13.482 3.15 13.041 3.15 10.143001 C 3.15 4.725 7.497 -1.386 14.553 -1.386 C 21.861 -1.386 28.287 4.662 28.287 12.663 Z "/>
</symbol>
<symbol id="gE53B4361DF76084EEECF5E79CA66DB66" overflow="visible">
<path d="M 25.6366 0 L 25.6366 2.2231 L 21.0485 2.2231 L 21.0485 7.3788 L 25.6366 7.3788 L 25.6366 9.6019 L 21.0485 9.6019 L 21.0485 29.5152 C 21.0485 30.7923 20.9539 31.0288 19.6295 31.0288 C 18.6362 31.0288 18.5889 30.9815 18.0213 30.272 L 1.5136 9.6019 L 1.5136 7.3788 L 15.136 7.3788 L 15.136 2.2231 L 9.8384 2.2231 L 9.8384 0 C 11.6358 0.1419 15.9401 0.1419 17.973999 0.1419 C 19.866 0.1419 23.9811 0.1419 25.6366 0 Z M 15.6563 9.6019 L 3.9732 9.6019 L 15.6563 24.2649 Z "/>
</symbol>
<symbol id="g3F6E68284F2A6689C7073689FF206CEC" overflow="visible">
<path d="M 29.673 10.395 L 29.673 12.348001 L 23.373001 12.348001 L 23.373001 41.013 C 23.373001 42.273 23.373001 42.651 22.365 42.651 C 21.798 42.651 21.609001 42.651 21.105 41.895 L 1.764 12.348001 L 1.764 10.395 L 18.522 10.395 L 18.522 4.914 C 18.522 2.6460001 18.396 1.9530001 13.734 1.9530001 L 12.411 1.9530001 L 12.411 0 C 14.994 0.18900001 18.27 0.18900001 20.916 0.18900001 C 23.562 0.18900001 26.901001 0.18900001 29.484001 0 L 29.484001 1.9530001 L 28.161001 1.9530001 C 23.499 1.9530001 23.373001 2.6460001 23.373001 4.914 L 23.373001 10.395 Z M 18.9 12.348001 L 3.528 12.348001 L 18.9 35.847 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,316 +0,0 @@
<svg class="typst-doc" viewBox="0 0 129.20000000000002 179.52" width="129.20000000000002pt" height="179.52pt" 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(12 12)">
<g class="typst-group">
<g>
<g transform="translate(40.4 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g9A826EA0690AE39EAFE6680061F57048" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g1232B22488ECA63989ACC8AA939D6950" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-12.200000000000001 64.8)">
<g class="typst-group">
<g>
<g transform="translate(6.124900000000002 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gCFEF70472F9D2AA9AC128F96529819DA" x="0" fill="rgba(255, 255, 255, 0.9)" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(40.4 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g7D2321520E42B189FE68CF200F46D3B4" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g33BC6FDCD50A81E45C881AD80F3DD77D" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(40.4 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(72.8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(72.8 97.20000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(1.6200000000000019 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(1.6200000000000019 11.664)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 29.16 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g9A826EA0690AE39EAFE6680061F57048" overflow="visible">
<path d="M 11.6291 4.5066 L 10.981601 4.5066 C 10.8521 3.7296002 10.6708 2.5900002 10.4118 2.2015002 C 10.2305 1.9943 8.5211 1.9943 7.9513 1.9943 L 3.2893002 1.9943 L 6.0347004 4.662 C 10.0751 8.2362 11.6291 9.6348 11.6291 12.2248 C 11.6291 15.177401 9.2981 17.249401 6.1383 17.249401 C 3.2116 17.249401 1.2950001 14.8666 1.2950001 12.561501 C 1.2950001 11.1111 2.5900002 11.1111 2.6677 11.1111 C 3.108 11.1111 4.0145 11.421901 4.0145 12.4838 C 4.0145 13.157201 3.5483 13.830601 2.6418002 13.830601 C 2.4346 13.830601 2.3828 13.830601 2.3051 13.804701 C 2.9008 15.4882 4.2994003 16.4465 5.8016 16.4465 C 8.158501 16.4465 9.272201 14.3486 9.272201 12.2248 C 9.272201 10.152801 7.9772 8.1067 6.5527 6.5009003 L 1.5799 0.95830005 C 1.2950001 0.67340004 1.2950001 0.62160003 1.2950001 0 L 10.9039 0 Z "/>
</symbol>
<symbol id="g1232B22488ECA63989ACC8AA939D6950" overflow="visible">
<path d="M 11.8363 4.3512 C 11.8363 5.2836003 11.5514 6.4491 10.567201 7.5369 C 10.0751 8.0808 9.6607 8.3398 8.0031 9.3758 C 9.8679 10.334101 11.137 11.680901 11.137 13.390301 C 11.137 15.773101 8.831901 17.249401 6.4750004 17.249401 C 3.885 17.249401 1.7871001 15.332801 1.7871001 12.9241 C 1.7871001 12.4579 1.8389001 11.2924 2.9267 10.0751 C 3.2116 9.7643 4.1699 9.1168 4.8174 8.6765 C 3.3152 7.9254003 1.0878 6.4750004 1.0878 3.9109 C 1.0878 1.1655 3.7296002 -0.5698 6.4491 -0.5698 C 9.3758 -0.5698 11.8363 1.5799 11.8363 4.3512 Z M 9.9974 13.390301 C 9.9974 11.9140005 8.9873 10.6708 7.4333 9.7643 L 4.2217 11.8363 C 3.0303001 12.6133 2.9267 13.4939 2.9267 13.9342 C 2.9267 15.5141 4.6102 16.6019 6.4491 16.6019 C 8.3398 16.6019 9.9974 15.2551 9.9974 13.390301 Z M 10.5413 3.4188 C 10.5413 1.5022 8.598801 0.15540001 6.4750004 0.15540001 C 4.2476 0.15540001 2.3828 1.7612001 2.3828 3.9109 C 2.3828 5.4131002 3.2116 7.0707 5.4131002 8.288 L 8.598801 6.2678003 C 9.324 5.7757 10.5413 4.9987 10.5413 3.4188 Z "/>
</symbol>
<symbol id="gCFEF70472F9D2AA9AC128F96529819DA" overflow="visible">
<path d="M 18.6998 6.4750004 C 18.6998 6.7599 18.4667 6.993 18.1818 6.993 L 10.593101 6.993 L 10.593101 14.5817 C 10.593101 14.8666 10.360001 15.0997 10.0751 15.0997 C 9.7902 15.0997 9.5571 14.8666 9.5571 14.5817 L 9.5571 6.993 L 1.9684 6.993 C 1.6835 6.993 1.4504 6.7599 1.4504 6.4750004 C 1.4504 6.1901 1.6835 5.9570003 1.9684 5.9570003 L 9.5571 5.9570003 L 9.5571 -1.6317 C 9.5571 -1.9166001 9.7902 -2.1497002 10.0751 -2.1497002 C 10.360001 -2.1497002 10.593101 -1.9166001 10.593101 -1.6317 L 10.593101 5.9570003 L 18.1818 5.9570003 C 18.4667 5.9570003 18.6998 6.1901 18.6998 6.4750004 Z "/>
</symbol>
<symbol id="g7D2321520E42B189FE68CF200F46D3B4" overflow="visible">
<path d="M 10.8521 0 L 10.8521 0.8029 L 10.0233 0.8029 C 7.6923003 0.8029 7.6146 1.0878 7.6146 2.0461001 L 7.6146 16.576 C 7.6146 17.197601 7.6146 17.249401 7.0189004 17.249401 C 5.4131002 15.591801 3.1339002 15.591801 2.3051 15.591801 L 2.3051 14.7889 C 2.8231 14.7889 4.3512 14.7889 5.698 15.4623 L 5.698 2.0461001 C 5.698 1.1137 5.6203003 0.8029 3.2893002 0.8029 L 2.4605 0.8029 L 2.4605 0 C 3.367 0.077700004 5.6203003 0.077700004 6.6563 0.077700004 C 7.6923003 0.077700004 9.9456005 0.077700004 10.8521 0 Z "/>
</symbol>
<symbol id="g33BC6FDCD50A81E45C881AD80F3DD77D" overflow="visible">
<path d="M 11.6291 5.2059 C 11.6291 8.288 9.5053005 10.878 6.7081003 10.878 C 5.4649 10.878 4.3512 10.4636 3.4188 9.5571 L 3.4188 14.6076 C 3.9368 14.4522 4.7915 14.270901 5.6203003 14.270901 C 8.806001 14.270901 10.619 16.6278 10.619 16.9645 C 10.619 17.1199 10.5413 17.249401 10.360001 17.249401 C 10.360001 17.249401 10.2823 17.249401 10.152801 17.1717 C 9.6348 16.9386 8.3657 16.4206 6.6304 16.4206 C 5.5944 16.4206 4.4030004 16.6019 3.1857002 17.1458 C 2.9785001 17.223501 2.8749 17.223501 2.8749 17.223501 C 2.6159 17.223501 2.6159 17.0163 2.6159 16.6019 L 2.6159 8.9355 C 2.6159 8.4693 2.6159 8.2621 2.9785001 8.2621 C 3.1598 8.2621 3.2116 8.3398 3.3152 8.4952 C 3.6001 8.9096 4.5584 10.3082 6.6563 10.3082 C 8.0031 10.3082 8.6506 9.1168 8.8578005 8.6506 C 9.272201 7.6923003 9.324 6.6822 9.324 5.3872004 C 9.324 4.4807 9.324 2.9267 8.7024 1.8389001 C 8.0808 0.8288 7.1225004 0.15540001 5.9311004 0.15540001 C 4.0404 0.15540001 2.5641 1.5281 2.1238 3.0562 C 2.2015002 3.0303001 2.2792 3.0044 2.5641 3.0044 C 3.4188 3.0044 3.8591 3.6519 3.8591 4.2735 C 3.8591 4.8951 3.4188 5.5426 2.5641 5.5426 C 2.2015002 5.5426 1.2950001 5.3613 1.2950001 4.1699 C 1.2950001 1.9425 3.0821002 -0.5698 5.9829 -0.5698 C 8.9873 -0.5698 11.6291 1.9166001 11.6291 5.2059 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,529 +0,0 @@
<svg class="typst-doc" viewBox="0 0 129.20000000000002 179.52" width="129.20000000000002pt" height="179.52pt" 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(12 12)">
<g class="typst-group">
<g>
<g transform="translate(40.4 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g33BC6FDCD50A81E45C881AD80F3DD77D" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g11751737F8308750F67353B214B1E45A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-12.200000000000001 64.8)">
<g class="typst-group">
<g>
<g transform="translate(6.124900000000002 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gCFEF70472F9D2AA9AC128F96529819DA" x="0" fill="rgba(255, 255, 255, 0.9)" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(40.4 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gC51CB09386F8F4AC77FF525FFB5DD300" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g1232B22488ECA63989ACC8AA939D6950" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(40.4 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(72.8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(39.15 97.20000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(1.6200000000000019 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(1.6200000000000019 11.664)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 29.16 0 "/>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 97.20000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(1.6200000000000019 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(1.6200000000000019 11.664)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 29.16 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g33BC6FDCD50A81E45C881AD80F3DD77D" overflow="visible">
<path d="M 11.6291 5.2059 C 11.6291 8.288 9.5053005 10.878 6.7081003 10.878 C 5.4649 10.878 4.3512 10.4636 3.4188 9.5571 L 3.4188 14.6076 C 3.9368 14.4522 4.7915 14.270901 5.6203003 14.270901 C 8.806001 14.270901 10.619 16.6278 10.619 16.9645 C 10.619 17.1199 10.5413 17.249401 10.360001 17.249401 C 10.360001 17.249401 10.2823 17.249401 10.152801 17.1717 C 9.6348 16.9386 8.3657 16.4206 6.6304 16.4206 C 5.5944 16.4206 4.4030004 16.6019 3.1857002 17.1458 C 2.9785001 17.223501 2.8749 17.223501 2.8749 17.223501 C 2.6159 17.223501 2.6159 17.0163 2.6159 16.6019 L 2.6159 8.9355 C 2.6159 8.4693 2.6159 8.2621 2.9785001 8.2621 C 3.1598 8.2621 3.2116 8.3398 3.3152 8.4952 C 3.6001 8.9096 4.5584 10.3082 6.6563 10.3082 C 8.0031 10.3082 8.6506 9.1168 8.8578005 8.6506 C 9.272201 7.6923003 9.324 6.6822 9.324 5.3872004 C 9.324 4.4807 9.324 2.9267 8.7024 1.8389001 C 8.0808 0.8288 7.1225004 0.15540001 5.9311004 0.15540001 C 4.0404 0.15540001 2.5641 1.5281 2.1238 3.0562 C 2.2015002 3.0303001 2.2792 3.0044 2.5641 3.0044 C 3.4188 3.0044 3.8591 3.6519 3.8591 4.2735 C 3.8591 4.8951 3.4188 5.5426 2.5641 5.5426 C 2.2015002 5.5426 1.2950001 5.3613 1.2950001 4.1699 C 1.2950001 1.9425 3.0821002 -0.5698 5.9829 -0.5698 C 8.9873 -0.5698 11.6291 1.9166001 11.6291 5.2059 Z "/>
</symbol>
<symbol id="g11751737F8308750F67353B214B1E45A" overflow="visible">
<path d="M 12.561501 16.6796 L 6.2678003 16.6796 C 3.108 16.6796 3.0562 17.0163 2.9526 17.5084 L 2.3051 17.5084 L 1.4504 12.173 L 2.0979002 12.173 C 2.1756 12.5874 2.4087 14.2191 2.7454002 14.529901 C 2.9267 14.685301 4.9469004 14.685301 5.2836003 14.685301 L 10.6449 14.685301 L 7.7441 10.593101 C 5.4131002 7.0966 4.5584 3.4965 4.5584 0.8547 C 4.5584 0.5957 4.5584 -0.5698 5.7498 -0.5698 C 6.9412003 -0.5698 6.9412003 0.5957 6.9412003 0.8547 L 6.9412003 2.1756 C 6.9412003 3.6001 7.0189004 5.0246 7.2261 6.4232 C 7.3297 7.0189004 7.6923003 9.246301 8.831901 10.8521 L 12.328401 15.773101 C 12.561501 16.0839 12.561501 16.1357 12.561501 16.6796 Z "/>
</symbol>
<symbol id="gCFEF70472F9D2AA9AC128F96529819DA" overflow="visible">
<path d="M 18.6998 6.4750004 C 18.6998 6.7599 18.4667 6.993 18.1818 6.993 L 10.593101 6.993 L 10.593101 14.5817 C 10.593101 14.8666 10.360001 15.0997 10.0751 15.0997 C 9.7902 15.0997 9.5571 14.8666 9.5571 14.5817 L 9.5571 6.993 L 1.9684 6.993 C 1.6835 6.993 1.4504 6.7599 1.4504 6.4750004 C 1.4504 6.1901 1.6835 5.9570003 1.9684 5.9570003 L 9.5571 5.9570003 L 9.5571 -1.6317 C 9.5571 -1.9166001 9.7902 -2.1497002 10.0751 -2.1497002 C 10.360001 -2.1497002 10.593101 -1.9166001 10.593101 -1.6317 L 10.593101 5.9570003 L 18.1818 5.9570003 C 18.4667 5.9570003 18.6998 6.1901 18.6998 6.4750004 Z "/>
</symbol>
<symbol id="gC51CB09386F8F4AC77FF525FFB5DD300" overflow="visible">
<path d="M 11.8363 5.2836003 C 11.8363 8.5729 9.5312 11.0593 6.6563 11.0593 C 4.8951 11.0593 3.9368 9.7384 3.4188 8.4952 L 3.4188 9.1168 C 3.4188 15.6695 6.6304 16.6019 7.9513 16.6019 C 8.5729 16.6019 9.6607 16.4465 10.2305 15.565901 C 9.842 15.565901 8.806001 15.565901 8.806001 14.4004 C 8.806001 13.597501 9.4276 13.209001 9.9974 13.209001 C 10.4118 13.209001 11.1888 13.442101 11.1888 14.4522 C 11.1888 16.0062 10.0492 17.249401 7.8995004 17.249401 C 4.5843 17.249401 1.0878 13.9083 1.0878 8.184401 C 1.0878 1.2691001 4.0922003 -0.5698 6.5009003 -0.5698 C 9.3758 -0.5698 11.8363 1.8648001 11.8363 5.2836003 Z M 9.5053005 5.3095 C 9.5053005 4.0663 9.5053005 2.7713 9.065001 1.8389001 C 8.288 0.2849 7.0966 0.15540001 6.5009003 0.15540001 C 4.8692 0.15540001 4.0922003 1.7094 3.9368 2.0979002 C 3.4706001 3.3152 3.4706001 5.3872004 3.4706001 5.8534 C 3.4706001 7.8736 4.2994003 10.4636 6.6304 10.4636 C 7.0448003 10.4636 8.2362 10.4636 9.039101 8.8578005 C 9.5053005 7.8995004 9.5053005 6.5786004 9.5053005 5.3095 Z "/>
</symbol>
<symbol id="g1232B22488ECA63989ACC8AA939D6950" overflow="visible">
<path d="M 11.8363 4.3512 C 11.8363 5.2836003 11.5514 6.4491 10.567201 7.5369 C 10.0751 8.0808 9.6607 8.3398 8.0031 9.3758 C 9.8679 10.334101 11.137 11.680901 11.137 13.390301 C 11.137 15.773101 8.831901 17.249401 6.4750004 17.249401 C 3.885 17.249401 1.7871001 15.332801 1.7871001 12.9241 C 1.7871001 12.4579 1.8389001 11.2924 2.9267 10.0751 C 3.2116 9.7643 4.1699 9.1168 4.8174 8.6765 C 3.3152 7.9254003 1.0878 6.4750004 1.0878 3.9109 C 1.0878 1.1655 3.7296002 -0.5698 6.4491 -0.5698 C 9.3758 -0.5698 11.8363 1.5799 11.8363 4.3512 Z M 9.9974 13.390301 C 9.9974 11.9140005 8.9873 10.6708 7.4333 9.7643 L 4.2217 11.8363 C 3.0303001 12.6133 2.9267 13.4939 2.9267 13.9342 C 2.9267 15.5141 4.6102 16.6019 6.4491 16.6019 C 8.3398 16.6019 9.9974 15.2551 9.9974 13.390301 Z M 10.5413 3.4188 C 10.5413 1.5022 8.598801 0.15540001 6.4750004 0.15540001 C 4.2476 0.15540001 2.3828 1.7612001 2.3828 3.9109 C 2.3828 5.4131002 3.2116 7.0707 5.4131002 8.288 L 8.598801 6.2678003 C 9.324 5.7757 10.5413 4.9987 10.5413 3.4188 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,316 +0,0 @@
<svg class="typst-doc" viewBox="0 0 129.20000000000002 179.52" width="129.20000000000002pt" height="179.52pt" 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(12 12)">
<g class="typst-group">
<g>
<g transform="translate(40.4 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g52F799E49960B56A0CB940B7AEFF8E1" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g11751737F8308750F67353B214B1E45A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-12.200000000000001 64.8)">
<g class="typst-group">
<g>
<g transform="translate(6.124900000000002 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gCFEF70472F9D2AA9AC128F96529819DA" x="0" fill="rgba(255, 255, 255, 0.9)" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(40.4 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g851DCC2C348500E95A48CED876DA2801" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g1232B22488ECA63989ACC8AA939D6950" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(40.4 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(72.8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(72.8 97.20000000000002)">
<g class="typst-group">
<g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(1.6200000000000019 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(1.6200000000000019 11.664)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 11.664 L 29.16 11.664 L 29.16 0 Z "/>
</g>
<g transform="translate(0 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 0)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(0 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(5.832 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(11.664 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(17.496000000000002 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
<g transform="translate(23.328 5.832)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" stroke="rgba(255, 255, 255, 0.4)" stroke-width="0.4" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 5.832 L 5.832 5.832 L 5.832 0 Z "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(0 0)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 29.16 0 "/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g52F799E49960B56A0CB940B7AEFF8E1" overflow="visible">
<path d="M 12.1989 4.2735 L 12.1989 5.0764003 L 9.6089 5.0764003 L 9.6089 16.8609 C 9.6089 17.3789 9.6089 17.5343 9.1945 17.5343 C 8.9614 17.5343 8.8837 17.5343 8.6765 17.223501 L 0.7252 5.0764003 L 0.7252 4.2735 L 7.6146 4.2735 L 7.6146 2.0202 C 7.6146 1.0878 7.5628004 0.8029 5.6462 0.8029 L 5.1023 0.8029 L 5.1023 0 C 6.1642003 0.077700004 7.511 0.077700004 8.598801 0.077700004 C 9.686601 0.077700004 11.0593 0.077700004 12.121201 0 L 12.121201 0.8029 L 11.5773 0.8029 C 9.6607 0.8029 9.6089 1.0878 9.6089 2.0202 L 9.6089 4.2735 Z M 7.77 5.0764003 L 1.4504 5.0764003 L 7.77 14.737101 Z "/>
</symbol>
<symbol id="g11751737F8308750F67353B214B1E45A" overflow="visible">
<path d="M 12.561501 16.6796 L 6.2678003 16.6796 C 3.108 16.6796 3.0562 17.0163 2.9526 17.5084 L 2.3051 17.5084 L 1.4504 12.173 L 2.0979002 12.173 C 2.1756 12.5874 2.4087 14.2191 2.7454002 14.529901 C 2.9267 14.685301 4.9469004 14.685301 5.2836003 14.685301 L 10.6449 14.685301 L 7.7441 10.593101 C 5.4131002 7.0966 4.5584 3.4965 4.5584 0.8547 C 4.5584 0.5957 4.5584 -0.5698 5.7498 -0.5698 C 6.9412003 -0.5698 6.9412003 0.5957 6.9412003 0.8547 L 6.9412003 2.1756 C 6.9412003 3.6001 7.0189004 5.0246 7.2261 6.4232 C 7.3297 7.0189004 7.6923003 9.246301 8.831901 10.8521 L 12.328401 15.773101 C 12.561501 16.0839 12.561501 16.1357 12.561501 16.6796 Z "/>
</symbol>
<symbol id="gCFEF70472F9D2AA9AC128F96529819DA" overflow="visible">
<path d="M 18.6998 6.4750004 C 18.6998 6.7599 18.4667 6.993 18.1818 6.993 L 10.593101 6.993 L 10.593101 14.5817 C 10.593101 14.8666 10.360001 15.0997 10.0751 15.0997 C 9.7902 15.0997 9.5571 14.8666 9.5571 14.5817 L 9.5571 6.993 L 1.9684 6.993 C 1.6835 6.993 1.4504 6.7599 1.4504 6.4750004 C 1.4504 6.1901 1.6835 5.9570003 1.9684 5.9570003 L 9.5571 5.9570003 L 9.5571 -1.6317 C 9.5571 -1.9166001 9.7902 -2.1497002 10.0751 -2.1497002 C 10.360001 -2.1497002 10.593101 -1.9166001 10.593101 -1.6317 L 10.593101 5.9570003 L 18.1818 5.9570003 C 18.4667 5.9570003 18.6998 6.1901 18.6998 6.4750004 Z "/>
</symbol>
<symbol id="g851DCC2C348500E95A48CED876DA2801" overflow="visible">
<path d="M 11.8363 4.4289002 C 11.8363 6.5527 10.2046 8.5729 7.511 9.1168 C 9.6348 9.8161 11.137 11.6291 11.137 13.6752 C 11.137 15.799001 8.8578005 17.249401 6.3714004 17.249401 C 3.7555 17.249401 1.7871001 15.6954 1.7871001 13.727 C 1.7871001 12.8723 2.3569 12.3802 3.108 12.3802 C 3.9109 12.3802 4.4289002 12.950001 4.4289002 13.7011 C 4.4289002 14.9961 3.2116 14.9961 2.8231 14.9961 C 3.6260002 16.2652 5.3354 16.6019 6.2678003 16.6019 C 7.3297 16.6019 8.7542 16.0321 8.7542 13.7011 C 8.7542 13.390301 8.7024 11.888101 8.029 10.7485 C 7.2520003 9.5053005 6.3714004 9.4276 5.7239003 9.4017 C 5.5167003 9.3758 4.8951 9.324 4.7138 9.324 C 4.5066 9.2981 4.3253 9.272201 4.3253 9.0132 C 4.3253 8.7283 4.5066 8.7283 4.9469004 8.7283 L 6.0865 8.7283 C 8.2103 8.7283 9.1686 6.9671 9.1686 4.4289002 C 9.1686 0.90650004 7.3815002 0.15540001 6.2419 0.15540001 C 5.1282 0.15540001 3.1857002 0.5957 2.2792 2.1238 C 3.1857002 1.9943 3.9886 2.5641 3.9886 3.5483 C 3.9886 4.4807 3.2893002 4.9987 2.5382001 4.9987 C 1.9166001 4.9987 1.0878 4.6361003 1.0878 3.4965 C 1.0878 1.1396 3.4965 -0.5698 6.3196 -0.5698 C 9.479401 -0.5698 11.8363 1.7871001 11.8363 4.4289002 Z "/>
</symbol>
<symbol id="g1232B22488ECA63989ACC8AA939D6950" overflow="visible">
<path d="M 11.8363 4.3512 C 11.8363 5.2836003 11.5514 6.4491 10.567201 7.5369 C 10.0751 8.0808 9.6607 8.3398 8.0031 9.3758 C 9.8679 10.334101 11.137 11.680901 11.137 13.390301 C 11.137 15.773101 8.831901 17.249401 6.4750004 17.249401 C 3.885 17.249401 1.7871001 15.332801 1.7871001 12.9241 C 1.7871001 12.4579 1.8389001 11.2924 2.9267 10.0751 C 3.2116 9.7643 4.1699 9.1168 4.8174 8.6765 C 3.3152 7.9254003 1.0878 6.4750004 1.0878 3.9109 C 1.0878 1.1655 3.7296002 -0.5698 6.4491 -0.5698 C 9.3758 -0.5698 11.8363 1.5799 11.8363 4.3512 Z M 9.9974 13.390301 C 9.9974 11.9140005 8.9873 10.6708 7.4333 9.7643 L 4.2217 11.8363 C 3.0303001 12.6133 2.9267 13.4939 2.9267 13.9342 C 2.9267 15.5141 4.6102 16.6019 6.4491 16.6019 C 8.3398 16.6019 9.9974 15.2551 9.9974 13.390301 Z M 10.5413 3.4188 C 10.5413 1.5022 8.598801 0.15540001 6.4750004 0.15540001 C 4.2476 0.15540001 2.3828 1.7612001 2.3828 3.9109 C 2.3828 5.4131002 3.2116 7.0707 5.4131002 8.288 L 8.598801 6.2678003 C 9.324 5.7757 10.5413 4.9987 10.5413 3.4188 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,103 +0,0 @@
<svg class="typst-doc" viewBox="0 0 129.20000000000002 153.6" width="129.20000000000002pt" height="153.6pt" 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(12 12)">
<g class="typst-group">
<g>
<g transform="translate(40.4 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g52F799E49960B56A0CB940B7AEFF8E1" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 32.4)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g11751737F8308750F67353B214B1E45A" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-12.200000000000001 64.8)">
<g class="typst-group">
<g>
<g transform="translate(6.124900000000002 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#gCFEF70472F9D2AA9AC128F96529819DA" x="0" fill="rgba(255, 255, 255, 0.9)" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(40.4 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g851DCC2C348500E95A48CED876DA2801" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(72.8 64.8)">
<g class="typst-group">
<g>
<g transform="translate(-0 -0)">
<path class="typst-shape" fill="#ffffff" fill-rule="nonzero" d="M 0 0 L 0 32.4 L 32.4 32.4 L 32.4 0 Z "/>
</g>
<g transform="translate(9.725000000000001 25.044850000000004)">
<g class="typst-text" transform="scale(1, -1)">
<use xlink:href="#g1232B22488ECA63989ACC8AA939D6950" x="0" fill="#000000" fill-rule="nonzero"/>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(40.4 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
<g transform="translate(72.8 97.20000000000002)">
<path class="typst-shape" fill="none" stroke="rgba(255, 255, 255, 0.8)" stroke-width="0.8" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 32.4 0 "/>
</g>
</g>
</g>
</g>
</g>
<defs id="glyph">
<symbol id="g52F799E49960B56A0CB940B7AEFF8E1" overflow="visible">
<path d="M 12.1989 4.2735 L 12.1989 5.0764003 L 9.6089 5.0764003 L 9.6089 16.8609 C 9.6089 17.3789 9.6089 17.5343 9.1945 17.5343 C 8.9614 17.5343 8.8837 17.5343 8.6765 17.223501 L 0.7252 5.0764003 L 0.7252 4.2735 L 7.6146 4.2735 L 7.6146 2.0202 C 7.6146 1.0878 7.5628004 0.8029 5.6462 0.8029 L 5.1023 0.8029 L 5.1023 0 C 6.1642003 0.077700004 7.511 0.077700004 8.598801 0.077700004 C 9.686601 0.077700004 11.0593 0.077700004 12.121201 0 L 12.121201 0.8029 L 11.5773 0.8029 C 9.6607 0.8029 9.6089 1.0878 9.6089 2.0202 L 9.6089 4.2735 Z M 7.77 5.0764003 L 1.4504 5.0764003 L 7.77 14.737101 Z "/>
</symbol>
<symbol id="g11751737F8308750F67353B214B1E45A" overflow="visible">
<path d="M 12.561501 16.6796 L 6.2678003 16.6796 C 3.108 16.6796 3.0562 17.0163 2.9526 17.5084 L 2.3051 17.5084 L 1.4504 12.173 L 2.0979002 12.173 C 2.1756 12.5874 2.4087 14.2191 2.7454002 14.529901 C 2.9267 14.685301 4.9469004 14.685301 5.2836003 14.685301 L 10.6449 14.685301 L 7.7441 10.593101 C 5.4131002 7.0966 4.5584 3.4965 4.5584 0.8547 C 4.5584 0.5957 4.5584 -0.5698 5.7498 -0.5698 C 6.9412003 -0.5698 6.9412003 0.5957 6.9412003 0.8547 L 6.9412003 2.1756 C 6.9412003 3.6001 7.0189004 5.0246 7.2261 6.4232 C 7.3297 7.0189004 7.6923003 9.246301 8.831901 10.8521 L 12.328401 15.773101 C 12.561501 16.0839 12.561501 16.1357 12.561501 16.6796 Z "/>
</symbol>
<symbol id="gCFEF70472F9D2AA9AC128F96529819DA" overflow="visible">
<path d="M 18.6998 6.4750004 C 18.6998 6.7599 18.4667 6.993 18.1818 6.993 L 10.593101 6.993 L 10.593101 14.5817 C 10.593101 14.8666 10.360001 15.0997 10.0751 15.0997 C 9.7902 15.0997 9.5571 14.8666 9.5571 14.5817 L 9.5571 6.993 L 1.9684 6.993 C 1.6835 6.993 1.4504 6.7599 1.4504 6.4750004 C 1.4504 6.1901 1.6835 5.9570003 1.9684 5.9570003 L 9.5571 5.9570003 L 9.5571 -1.6317 C 9.5571 -1.9166001 9.7902 -2.1497002 10.0751 -2.1497002 C 10.360001 -2.1497002 10.593101 -1.9166001 10.593101 -1.6317 L 10.593101 5.9570003 L 18.1818 5.9570003 C 18.4667 5.9570003 18.6998 6.1901 18.6998 6.4750004 Z "/>
</symbol>
<symbol id="g851DCC2C348500E95A48CED876DA2801" overflow="visible">
<path d="M 11.8363 4.4289002 C 11.8363 6.5527 10.2046 8.5729 7.511 9.1168 C 9.6348 9.8161 11.137 11.6291 11.137 13.6752 C 11.137 15.799001 8.8578005 17.249401 6.3714004 17.249401 C 3.7555 17.249401 1.7871001 15.6954 1.7871001 13.727 C 1.7871001 12.8723 2.3569 12.3802 3.108 12.3802 C 3.9109 12.3802 4.4289002 12.950001 4.4289002 13.7011 C 4.4289002 14.9961 3.2116 14.9961 2.8231 14.9961 C 3.6260002 16.2652 5.3354 16.6019 6.2678003 16.6019 C 7.3297 16.6019 8.7542 16.0321 8.7542 13.7011 C 8.7542 13.390301 8.7024 11.888101 8.029 10.7485 C 7.2520003 9.5053005 6.3714004 9.4276 5.7239003 9.4017 C 5.5167003 9.3758 4.8951 9.324 4.7138 9.324 C 4.5066 9.2981 4.3253 9.272201 4.3253 9.0132 C 4.3253 8.7283 4.5066 8.7283 4.9469004 8.7283 L 6.0865 8.7283 C 8.2103 8.7283 9.1686 6.9671 9.1686 4.4289002 C 9.1686 0.90650004 7.3815002 0.15540001 6.2419 0.15540001 C 5.1282 0.15540001 3.1857002 0.5957 2.2792 2.1238 C 3.1857002 1.9943 3.9886 2.5641 3.9886 3.5483 C 3.9886 4.4807 3.2893002 4.9987 2.5382001 4.9987 C 1.9166001 4.9987 1.0878 4.6361003 1.0878 3.4965 C 1.0878 1.1396 3.4965 -0.5698 6.3196 -0.5698 C 9.479401 -0.5698 11.8363 1.7871001 11.8363 4.4289002 Z "/>
</symbol>
<symbol id="g1232B22488ECA63989ACC8AA939D6950" overflow="visible">
<path d="M 11.8363 4.3512 C 11.8363 5.2836003 11.5514 6.4491 10.567201 7.5369 C 10.0751 8.0808 9.6607 8.3398 8.0031 9.3758 C 9.8679 10.334101 11.137 11.680901 11.137 13.390301 C 11.137 15.773101 8.831901 17.249401 6.4750004 17.249401 C 3.885 17.249401 1.7871001 15.332801 1.7871001 12.9241 C 1.7871001 12.4579 1.8389001 11.2924 2.9267 10.0751 C 3.2116 9.7643 4.1699 9.1168 4.8174 8.6765 C 3.3152 7.9254003 1.0878 6.4750004 1.0878 3.9109 C 1.0878 1.1655 3.7296002 -0.5698 6.4491 -0.5698 C 9.3758 -0.5698 11.8363 1.5799 11.8363 4.3512 Z M 9.9974 13.390301 C 9.9974 11.9140005 8.9873 10.6708 7.4333 9.7643 L 4.2217 11.8363 C 3.0303001 12.6133 2.9267 13.4939 2.9267 13.9342 C 2.9267 15.5141 4.6102 16.6019 6.4491 16.6019 C 8.3398 16.6019 9.9974 15.2551 9.9974 13.390301 Z M 10.5413 3.4188 C 10.5413 1.5022 8.598801 0.15540001 6.4750004 0.15540001 C 4.2476 0.15540001 2.3828 1.7612001 2.3828 3.9109 C 2.3828 5.4131002 3.2116 7.0707 5.4131002 8.288 L 8.598801 6.2678003 C 9.324 5.7757 10.5413 4.9987 10.5413 3.4188 Z "/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -398,8 +398,8 @@
<!-- Import Typst.ts and SVG Processor -->
<script>
// Global variables
const typstRenderer = null;
const flashcardsTemplate = null;
let typstRenderer = null;
let flashcardsTemplate = null;
// Initialize everything - use web app's API instead of direct Typst
async function initialize() {

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 01 (US Central Time) - cropped to active beads -->
<svg x="2" y="32.20553730642891" width="96" height="35.588925387142176"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 31.599999999999994">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 02 (US Central Time) - cropped to active beads -->
<svg x="2" y="19.53542937587988" width="96" height="60.92914124824024"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 54.099999999999994">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 03 (US Central Time) - cropped to active beads -->
<svg x="2" y="6.865321445330842" width="96" height="86.26935710933832"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 76.6">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 04 (US Central Time) - cropped to active beads -->
<svg x="8.71321897073662" y="2" width="82.57356205852676" height="96"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 99.1">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-3" transform="translate(56.7, 126.89999999999999)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 05 (US Central Time) - cropped to active beads -->
<svg x="2" y="32.20553730642891" width="96" height="35.588925387142176"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 31.599999999999998">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 06 (US Central Time) - cropped to active beads -->
<svg x="2" y="15.987799155326151" width="96" height="68.0244016893477"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 60.39999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 07 (US Central Time) - cropped to active beads -->
<svg x="2" y="3.317691224777114" width="96" height="93.36461755044577"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 82.89999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 08 (US Central Time) - cropped to active beads -->
<svg x="11.18102466793168" y="2" width="77.63795066413664" height="96"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 105.39999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 09 (US Central Time) - cropped to active beads -->
<svg x="18.010007818608283" y="2" width="63.97998436278343" height="96"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 127.89999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-3" transform="translate(56.7, 126.89999999999999)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 10 (US Central Time) - cropped to active beads -->
<svg x="2" y="32.20553730642891" width="96" height="35.588925387142176"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 31.599999999999994">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 11 (US Central Time) - cropped to active beads -->
<svg x="2" y="32.20553730642891" width="96" height="35.588925387142176"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 31.599999999999994">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 12 (US Central Time) - cropped to active beads -->
<svg x="2" y="19.53542937587988" width="96" height="60.92914124824024"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 54.099999999999994">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 13 (US Central Time) - cropped to active beads -->
<svg x="2" y="6.865321445330842" width="96" height="86.26935710933832"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 76.6">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 14 (US Central Time) - cropped to active beads -->
<svg x="8.71321897073662" y="2" width="82.57356205852676" height="96"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 99.1">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-3" transform="translate(56.7, 126.89999999999999)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 15 (US Central Time) - cropped to active beads -->
<svg x="2" y="15.987799155326151" width="96" height="68.0244016893477"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 60.39999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 16 (US Central Time) - cropped to active beads -->
<svg x="2" y="15.987799155326151" width="96" height="68.0244016893477"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 60.39999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 17 (US Central Time) - cropped to active beads -->
<svg x="2" y="3.317691224777114" width="96" height="93.36461755044577"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 82.89999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 18 (US Central Time) - cropped to active beads -->
<svg x="11.18102466793168" y="2" width="77.63795066413664" height="96"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 105.39999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 19 (US Central Time) - cropped to active beads -->
<svg x="18.010007818608283" y="2" width="63.97998436278343" height="96"
viewBox="2.380000000000001 22.600000000000005 85.24000000000001 127.89999999999999">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g><g class="abacus-bead active " data-testid="bead-place-0-heaven" transform="translate(56.7, 30.600000000000005)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-0" transform="translate(56.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-1" transform="translate(56.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-2" transform="translate(56.7, 104.4)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-0-earth-pos-3" transform="translate(56.7, 126.89999999999999)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#ef4444" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,32 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day 20 (US Central Time) - cropped to active beads -->
<svg x="2" y="19.53542937587988" width="96" height="60.92914124824024"
viewBox="2.380000000000001 51.400000000000006 85.24000000000001 54.099999999999994">
<defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0;
}
</style></defs><rect x="19.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="64.8" y="0" width="5.4" height="216" fill="#1c1917" stroke="#0c0a09" stroke-width="2" opacity="1" class="column-post"></rect><rect x="0" y="54" width="90" height="3.6" fill="#1c1917" stroke="#0c0a09" stroke-width="3" opacity="1"></rect><g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-0" transform="translate(11.7, 59.400000000000006)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g><g class="abacus-bead active " data-testid="bead-place-1-earth-pos-1" transform="translate(11.7, 81.9)" style="transition:opacity 0.2s ease-in-out"><circle cx="10.8" cy="10.8" r="10.8" fill="#f59e0b" stroke="#000" stroke-width="0.5" opacity="1"></circle></g></g><g></g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

Some files were not shown because too many files have changed in this diff Show More