Compare commits

..

4 Commits

Author SHA1 Message Date
semantic-release-bot
fab490ffea chore(release): 4.67.3 [skip ci]
## [4.67.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.2...v4.67.3) (2025-10-23)

### Bug Fixes

* **complement-race:** resolve infinite render loop in useTrackManagement ([8b4dacd](8b4dacdc98))
2025-10-23 11:30:06 +00:00
Thomas Hallock
8b4dacdc98 fix(complement-race): resolve infinite render loop in useTrackManagement
Fixed "Maximum update depth exceeded" error by removing displayPassengers
from the effect dependency array. The effect calls setDisplayPassengers,
which was triggering infinite re-renders at 60fps update frequency.

The displayPassengers state is only used for comparison inside the effect,
not as an input that should trigger re-execution.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 06:28:49 -05:00
semantic-release-bot
28fc0a14be chore(release): 4.67.2 [skip ci]
## [4.67.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.67.2) (2025-10-23)

### Performance Improvements

* **complement-race:** increase train position update frequency to 60fps ([fffaf1d](fffaf1df1d))
2025-10-23 11:24:24 +00:00
Thomas Hallock
fffaf1df1d perf(complement-race): increase train position update frequency to 60fps
Increased update intervals from 50ms (20fps) to 16ms (60fps) for smoother
train movement without using react-spring animations. Changes applied to:

- Game logic loop (useSteamJourney.ts)
- Momentum/position updates (Provider.tsx)
- Position broadcasts for multiplayer (Provider.tsx)

This resolves the regression where react-spring animations caused guest
players' trains to freeze at their starting position in multiplayer games.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 06:23:02 -05:00
358 changed files with 7939 additions and 101508 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
@@ -52,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"
}
}
}
}

25978
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,71 +44,16 @@ 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
@@ -124,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
@@ -134,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
@@ -157,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,325 +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

@@ -1,420 +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

@@ -1,143 +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:
@@ -182,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
@@ -271,51 +121,6 @@ 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.**
@@ -333,50 +138,6 @@ When creating ANY new HTML/JSX element (div, button, section, etc.), add appropr
- ✅ 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:**
@@ -507,139 +268,3 @@ 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.**
When monitoring deployments to production (NAS at abaci.one):
1. **GitHub Actions Success ≠ NAS Deployment**
- GitHub Actions builds and pushes Docker images to GHCR
- The NAS must separately pull and restart containers
- There may be a delay or manual step between these
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
- Ask if manual NAS deployment action is needed
**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

@@ -1,584 +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

@@ -1,468 +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

@@ -1,283 +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

@@ -1,594 +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

View File

@@ -1,943 +0,0 @@
# Platform Integration Roadmap
**Project:** Soroban Abacus Flashcards - Arcade Games Platform
**Focus Areas:** Educational Platform SSO & Game Distribution Channels
**Document Version:** 1.0
**Last Updated:** 2025-11-02
---
## Executive Summary
This roadmap outlines integration strategies for four major platform categories to expand reach and player acquisition:
1. **Google Classroom Integration** - Direct access to teachers and students
2. **Clever/ClassLink SSO** - K-12 institution single sign-on (massive reach)
3. **Game Distribution Portals** - CrazyGames, Poki, Kongregate (casual player discovery)
4. **Steam Distribution** - Educational game marketplace with social features
**Estimated Total Timeline:** 6-9 months for full implementation
**Estimated Total Cost:** $600-1,500 (app fees + hosting)
**Expected Impact:** 10x-100x increase in player discovery reach
---
## Platform #1: Google Classroom Integration
### Overview
Google Classroom API enables teachers to:
- Import game rooms directly into their classes
- Auto-create student accounts with SSO
- Track student progress and scores
- Assign abacus practice as homework
**Target Audience:** 150+ million students and teachers using Google Classroom globally
### Technical Requirements
**Prerequisites:**
- Google Cloud Platform (GCP) project
- OAuth 2.0 implementation
- Google Classroom API access
- HTTPS endpoints
**API Capabilities Needed:**
- Courses API (read class rosters)
- Coursework API (create assignments)
- Students API (manage student accounts)
- Submissions API (track game completion)
### Implementation Phases
#### Phase 1: Basic OAuth & SSO (2-3 weeks)
**Tasks:**
1. Set up GCP project and enable Classroom API
2. Implement OAuth 2.0 "Sign in with Google" flow
3. Add Classroom scope permissions (`classroom.courses.readonly`, `classroom.rosters.readonly`)
4. Create user account mapping (Google ID → Internal User ID)
5. Test with sandbox Google Workspace account
**Deliverables:**
- "Sign in with Google" button on login page
- Auto-account creation for Google users
- Basic profile sync (name, email, photo)
**Effort:** 40-60 hours
**Cost:** $0 (Google API is free for educational use)
#### Phase 2: Class Import & Roster Sync (3-4 weeks)
**Tasks:**
1. Build UI for teachers to import Classroom rosters
2. Implement Courses API integration (list teacher's classes)
3. Create room auto-provisioning from class data
4. Set up automatic roster synchronization (daily sync)
5. Handle student account creation/deactivation
6. Add class management dashboard for teachers
**Deliverables:**
- "Import from Google Classroom" feature
- Teacher dashboard showing all imported classes
- Automatic student account provisioning
- Daily roster sync (new students, removed students)
**Effort:** 80-100 hours
**Cost:** $0
#### Phase 3: Assignment Integration (3-4 weeks)
**Tasks:**
1. Build "Assign to Classroom" feature for game rooms
2. Implement Coursework API integration
3. Create assignment templates for each game type
4. Build grade sync (game scores → Classroom grades)
5. Add completion tracking and submission API integration
6. Create teacher analytics dashboard
**Deliverables:**
- Teachers can push game assignments to Classroom
- Students see assignments in Classroom feed
- Automatic grade passback on game completion
- Teacher analytics (who played, scores, time spent)
**Effort:** 100-120 hours
**Cost:** $0
#### Phase 4: Deep Integration & Polish (2-3 weeks)
**Tasks:**
1. Add "Share Turn" notifications (via Classroom API announcements)
2. Implement progress tracking dashboard
3. Create teacher resource library (lesson plans, tutorials)
4. Add student portfolio (history of games played)
5. Build reporting exports (CSV, PDF)
6. Classroom API webhook integration for real-time updates
**Deliverables:**
- Comprehensive teacher dashboard
- Student progress portfolios
- Automated reporting
- Real-time roster updates
**Effort:** 60-80 hours
**Cost:** $0
### Total Timeline: 10-14 weeks
### Total Effort: 280-360 hours
### Total Cost: $0
### Dependencies
- Existing user authentication system (✓ NextAuth in place)
- Room management system (✓ exists)
- Score/progress tracking (✓ exists per game)
- Email notification system (partially implemented)
### Risks & Mitigation
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| OAuth complexity | Medium | Medium | Use official Google client libraries |
| API rate limits | Low | Low | Implement caching and batch requests |
| Grade sync errors | High | Medium | Add retry logic and error notifications |
| Teacher adoption | High | High | Create detailed onboarding videos |
| Privacy compliance (COPPA) | Critical | Medium | Implement parent consent workflows |
### Success Metrics
- **Adoption:** 100+ teachers in first 3 months
- **Usage:** 50+ classes imported
- **Retention:** 70%+ of teachers assign games monthly
- **Referrals:** 20% teacher-to-teacher referrals
---
## Platform #2: Clever & ClassLink SSO
### Overview
**Clever:** SSO platform serving 75% of U.S. K-12 schools (13,000+ districts)
**ClassLink:** SSO platform serving 20+ million students globally
Both provide instant access to school rosters without manual setup.
**Key Benefits:**
- Zero teacher setup (IT manages access)
- Automatic roster sync
- Instant student login (no passwords)
- District-wide deployment capability
### Technical Requirements
**Clever Requirements:**
- OAuth 2.0 implementation
- REST API integration
- HTTPS endpoints
- District SSO certification
**ClassLink Requirements:**
- SAML 2.0 support (or OAuth/OpenID Connect)
- OneRoster API integration (for roster sync)
- ClassLink Management Console access
### Implementation Phases
#### Phase 1: Clever District SSO (4-5 weeks)
**Tasks:**
1. Register as Clever developer (dev.clever.com)
2. Implement OAuth 2.0 authorization grant flow
3. Add "Log in with Clever" button
4. Integrate Clever Data API v3.1
- Districts API
- Schools API
- Students/Teachers API
- Sections API (classes)
5. Build user account mapping (Clever ID → Internal ID)
6. Create development environment with sandbox district
7. Implement district admin dashboard
8. Apply for District SSO certification
**API Scopes Required:**
- `read:district_admins_basic`
- `read:school_admins_basic`
- `read:students_basic`
- `read:teachers_basic`
- `read:user_id`
- `read:sis` (for full roster data)
**Deliverables:**
- "Log in with Clever" SSO
- Automatic account creation
- District/school/class hierarchy sync
- Certified for live district connections
**Effort:** 100-120 hours
**Cost:** $0 (Clever is free for developers)
#### Phase 2: ClassLink SAML Integration (3-4 weeks)
**Tasks:**
1. Set up SAML 2.0 service provider
2. Register with ClassLink SSO Library
3. Implement SAML authentication flow
4. Add "Log in with ClassLink" button
5. Integrate OneRoster API for roster sync
6. Build automated roster update system
7. Create ClassLink admin dashboard
8. Submit to ClassLink SSO Library (6,000+ app directory)
**Deliverables:**
- "Log in with ClassLink" SSO
- OneRoster-based roster sync
- Listed in ClassLink SSO Library
**Effort:** 80-100 hours
**Cost:** $0
#### Phase 3: Advanced Features (2-3 weeks)
**Tasks:**
1. Implement shared device support (session override)
2. Add UTF-8 character support for names
3. Build district admin analytics dashboard
4. Create automated onboarding emails for admins
5. Add Clever/ClassLink badge to marketing site
6. Build compliance documentation (FERPA, COPPA)
**Deliverables:**
- Production-ready SSO for both platforms
- District admin tools
- Compliance documentation
**Effort:** 60-80 hours
**Cost:** $0
### Total Timeline: 9-12 weeks
### Total Effort: 240-300 hours
### Total Cost: $0
### Dependencies
- SAML library (e.g., `passport-saml` for Node.js)
- OAuth 2.0 implementation (can reuse from Google Classroom)
- Secure session management
- Database schema for district/school/class hierarchy
### Risks & Mitigation
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Certification delays | High | Medium | Start certification process early |
| SAML complexity | Medium | Medium | Use established libraries (passport-saml) |
| District adoption | High | Medium | Partner with early adopter districts |
| Privacy regulations | Critical | Medium | Legal review of terms/privacy policy |
| Competing apps | Medium | High | Emphasize unique abacus/math focus |
### Success Metrics
- **Listings:** Published in both app directories
- **Certification:** Clever District SSO certified
- **Districts:** 10+ districts in first 6 months
- **Students:** 5,000+ student logins in first year
- **Retention:** 60%+ district renewal rate
---
## Platform #3: Game Distribution Portals
### Overview
Three major browser game portals with complementary audiences:
1. **CrazyGames:** Open platform, 35M+ monthly users, SDK monetization
2. **Poki:** Curated platform, 50M+ monthly users, 50/50 revenue share, web exclusivity
3. **Kongregate:** Legacy platform, 20M+ monthly users, now requires pre-approval
**Target Audience:** Casual gamers discovering educational games through play
### Platform Comparison
| Feature | CrazyGames | Poki | Kongregate |
|---------|------------|------|------------|
| **Selectivity** | Moderate | High (curated) | High (email approval) |
| **File Size** | Strict limits | <8MB initial | ~1100x700px max |
| **Exclusivity** | None | Web exclusive | None |
| **Revenue** | SDK ads | 50/50 split | Ads + Kreds |
| **SDK Required** | Yes | Yes | Yes |
| **Approval Time** | 1-2 weeks | 2-4 weeks | Varies |
### Technical Requirements (Common)
**All Platforms:**
- HTML5/WebGL build
- Save system (localStorage/cloud)
- Ad integration (SDK provided)
- Mobile responsive design
- HTTPS hosting
- No external monetization UI
**CrazyGames Specific:**
- CrazyGames SDK integration
- File size optimization
- Ad placements (banner, interstitial, rewarded)
- Leaderboard integration
**Poki Specific:**
- Poki SDK integration
- <8MB initial download (critical!)
- Auto-detect mobile devices
- Remove any IAP UI elements
- Web exclusivity agreement
**Kongregate Specific:**
- Pre-approval via BD@kongregate.com
- Kongregate API integration
- Badge system integration
- Kreds (virtual currency) support optional
- "initialized" stat tracking
### Implementation Phases
#### Phase 1: Game Optimization & SDK Prep (3-4 weeks)
**Tasks:**
1. Audit current build sizes (check all games)
2. Implement code splitting and lazy loading
3. Optimize assets (image compression, audio formats)
4. Create lightweight "portal build" configuration
5. Build asset CDN strategy
6. Implement progress save system (if not present)
7. Add mobile detection and responsive layouts
8. Create build pipeline for portal releases
**Target Build Sizes:**
- Initial load: <5MB (to meet Poki's <8MB requirement)
- Full game: <20MB
- Individual game assets: lazy loaded
**Deliverables:**
- Optimized builds for each game
- Build pipeline for portal releases
- Mobile-responsive UI for all games
**Effort:** 80-100 hours
**Cost:** $0
#### Phase 2: CrazyGames Integration (2-3 weeks)
**Tasks:**
1. Register at CrazyGames developer portal
2. Integrate CrazyGames SDK
- Ad placements (banner, interstitial, rewarded)
- Game analytics
- User progression tracking
3. Submit best-performing games:
- Matching Pairs Battle (most casual-friendly)
- Complement Race (fast-paced)
- Card Sorting (quick sessions)
4. Create game pages with descriptions/screenshots
5. Pass technical review and QA
6. Launch and monitor metrics
**Deliverables:**
- 3 games live on CrazyGames
- SDK integration complete
- Ad monetization active
**Effort:** 60-80 hours
**Cost:** $0
#### Phase 3: Poki Integration (3-4 weeks)
**Tasks:**
1. Submit application via Poki game submission form
2. Wait for curator review (2-4 weeks)
3. If approved, integrate Poki SDK
- Ad integration (Poki manages monetization)
- Analytics tracking
- Commerce API (if applicable)
4. Agree to web exclusivity terms
5. Optimize games to <8MB initial load (critical!)
6. Remove any competing monetization UI
7. Submit games for final review
8. Launch on Poki
**Deliverables:**
- 2-3 games live on Poki (start with best performers)
- Poki SDK integration
- Revenue share active (50/50 split)
**Effort:** 80-100 hours
**Cost:** $0
**Note:** Approval not guaranteed - Poki is highly selective
#### Phase 4: Kongregate Pre-Approval (2-3 weeks)
**Tasks:**
1. Email BD@kongregate.com with game portfolio
2. Prepare pitch deck:
- Game trailers/screenshots
- Unique value proposition (abacus education + multiplayer)
- Target audience data
- Existing player metrics
3. Wait for response (varies)
4. If approved, integrate Kongregate API
- Stats and achievements (badges)
- Optional: Kreds integration
5. Submit games for review
6. Launch on Kongregate
**Deliverables:**
- Pre-approval from Kongregate
- 1-2 games live on platform
- Badge achievements implemented
**Effort:** 60-80 hours
**Cost:** $0
**Note:** Approval required before development
### Total Timeline: 10-14 weeks
### Total Effort: 280-360 hours
### Total Cost: $0
### Game Prioritization for Submission
**Tier 1 (Submit first):**
1. **Matching Pairs Battle** - Most casual-friendly, clear mechanics
2. **Complement Race** - Fast-paced, competitive, good for short sessions
**Tier 2 (Submit if Tier 1 succeeds):**
3. **Card Sorting** - Simple, quick gameplay
4. **Memory Quiz** - Educational but approachable
**Tier 3 (Hold back):**
5. **Rithmomachia** - Too complex for casual portals, better for Steam
### Dependencies
- Current games must be playable without login (guest mode)
- Mobile responsive design
- Build optimization pipeline
- CDN for asset hosting
- Analytics integration
### Risks & Mitigation
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Poki rejection | Medium | High | Focus on CrazyGames first, apply with best metrics |
| File size requirements | High | Medium | Aggressive asset optimization, code splitting |
| Ad integration conflicts | Medium | Medium | Separate builds per platform |
| Revenue lower than expected | Medium | High | Treat as marketing channel, not primary revenue |
| Web exclusivity limits Steam | High | Low | Don't submit Rithmomachia to Poki |
| Multiplayer sync issues | High | Medium | Add offline/single-player modes |
### Success Metrics
**CrazyGames:**
- **Plays:** 10,000+ plays/month per game
- **Retention:** 30%+ day-1 return rate
- **Revenue:** $200+ monthly from ads
**Poki:**
- **Plays:** 50,000+ plays/month per game
- **Revenue:** $500+ monthly (50/50 split)
- **Rating:** 4.0+ stars
**Kongregate:**
- **Plays:** 5,000+ plays/month
- **Badges:** 3+ achievements per game
- **Revenue:** $100+ monthly
---
## Platform #4: Steam Distribution
### Overview
**Why Steam for Educational Games:**
- 120+ million active users
- Strong social features (friends, achievements, leaderboards)
- Educational games perform well (Kerbal Space Program, Human Resource Machine, etc.)
- One-time purchase model fits premium educational content
- Community features drive multiplayer engagement
**Package Concept:** "Soroban Academy: Mathematical Strategy Games"
- Bundle all games into single Steam app
- Include exclusive Steam features (achievements, leaderboards, cloud saves)
- Highlight Rithmomachia as flagship historical strategy game
- Price point: $9.99-14.99
### Technical Requirements
**Platform:**
- Desktop application (Windows, macOS, Linux)
- No native browser game support
**Packaging Options:**
1. **Electron** (most common for web games)
- Pros: Easy conversion, full Chromium
- Cons: Large file size (~100-200MB), memory intensive
2. **Tauri** (modern alternative)
- Pros: Lightweight (~10MB), uses system WebView
- Cons: Newer, less Steam integration examples
3. **NW.js** (alternative to Electron)
- Pros: Similar to Electron, slightly smaller
- Cons: Less popular, smaller community
**Recommended:** Start with Electron (most proven path for HTML5 → Steam)
**Steamworks Integration:**
- Steam Authentication API
- Steam Achievements API
- Steam Leaderboards API
- Steam Cloud (save sync)
- Steam Friends API (invite friends to games)
- Steam Overlay support
### Implementation Phases
#### Phase 1: Desktop Packaging with Electron (4-5 weeks)
**Tasks:**
1. Set up Electron project
2. Package Next.js app for Electron
3. Configure build pipeline:
- Windows (x64)
- macOS (Apple Silicon + Intel)
- Linux (x64)
4. Handle offline mode and local Socket.io server
5. Implement native window controls
6. Add auto-updater support
7. Test on all platforms
8. Optimize bundle size (<500MB total)
**Deliverables:**
- Desktop app for Windows/macOS/Linux
- Standalone builds (no separate server needed)
- Native app experience
**Effort:** 120-150 hours
**Cost:** $0
#### Phase 2: Steamworks SDK Integration (3-4 weeks)
**Tasks:**
1. Register as Steamworks developer (pay $100 fee)
2. Create Steam app page
3. Integrate Steamworks SDK:
- Install Greenworks (Electron + Steamworks bridge)
- Implement Steam Authentication
- Replace internal auth with Steam accounts
4. Build achievement system:
- Define 20-30 achievements per game
- Integrate Steamworks Achievements API
- Create achievement icons (64x64px)
5. Implement Steam Leaderboards:
- Per-game leaderboards
- Global cross-game leaderboard
6. Add Steam Cloud saves:
- Sync game progress across devices
- Handle conflict resolution
7. Integrate Steam Friends API:
- "Invite friend to game room" button
- Show online friends in lobby
8. Test Steam Overlay functionality
**Deliverables:**
- Full Steamworks integration
- Achievement system (20-30 achievements)
- Steam Leaderboards
- Cloud saves
- Friend invites
**Effort:** 100-120 hours
**Cost:** $100 (Steam Direct fee, one-time, recoupable)
#### Phase 3: Store Page & Marketing Assets (2-3 weeks)
**Tasks:**
1. Create Steam store page:
- Title: "Soroban Academy: Mathematical Strategy Games"
- Capsule images (multiple sizes)
- Hero image / header
- 5-10 screenshots per game
- 60-90 second trailer (show all games)
- Detailed description with educational benefits
- System requirements
- Price: $9.99-14.99
2. Write compelling marketing copy
3. Create game trailer (video editing)
4. Design Steam library assets
5. Set up community hub
6. Configure Steam tags:
- Education, Strategy, Multiplayer, Casual, Math, Board Game
7. Prepare press kit and media
**Deliverables:**
- Complete Steam store page
- Marketing trailer
- Press kit
- Community hub setup
**Effort:** 60-80 hours
**Cost:** $0-300 (if hiring video editor)
#### Phase 4: Steam Review & Launch (2-3 weeks)
**Tasks:**
1. Upload build to Steamworks
2. Set release date (2+ weeks out)
3. Make store page public
4. Submit for Steam review (1-5 days)
5. Address any review feedback
6. Prepare launch marketing:
- Email existing users
- Social media campaign
- Press outreach
- Reddit/forum posts
- Product Hunt launch
7. Launch on Steam
8. Monitor reviews and feedback
9. Respond to community
10. Plan post-launch updates
**Deliverables:**
- Live on Steam
- Launch marketing campaign
- Community management process
**Effort:** 60-80 hours
**Cost:** $0-500 (marketing budget)
#### Phase 5: Post-Launch Support (Ongoing)
**Tasks:**
- Weekly bug fixes and patches
- Monthly content updates
- Seasonal events
- New achievements
- Community engagement
- Steam Sale participation
- User feedback integration
**Deliverables:**
- Regular updates
- Active community
- Growing review score
**Effort:** 20-40 hours/month ongoing
**Cost:** $0
### Total Timeline: 11-15 weeks
### Total Effort: 340-430 hours
### Total Cost: $100-900
### Dependencies
- Functioning multiplayer system (✓ exists)
- All games stable and bug-free
- Marketing materials (screenshots, videos)
- Legal documentation (EULA, privacy policy)
- Support email/forum setup
### Risks & Mitigation
| Risk | Impact | Likelihood | Mitigation |
|------|--------|------------|------------|
| Electron bundle too large | Medium | Medium | Optimize assets, use lazy loading |
| Steamworks integration bugs | High | Medium | Extensive testing, use Greenworks |
| Low sales | High | High | Strong marketing, community building |
| Review bombing | Medium | Low | Active community management |
| Multiplayer server costs | Medium | Medium | P2P option or Steam networking |
| Steam review rejection | High | Low | Follow guidelines strictly |
| Competition from free web version | High | High | Add Steam-exclusive features |
### Pricing Strategy
**Options:**
1. **Premium ($9.99-14.99):** Full game bundle with Steam features
2. **Freemium:** Free base + DLC for premium games
3. **Early Access ($7.99):** Launch at lower price, increase at 1.0
**Recommendation:** Premium $12.99 with frequent sales
- Perceived value: 6 games = ~$2 each
- Recoup $100 fee after 8 sales
- Sales can drive volume (50% off = $6.49)
### Marketing Angles for Steam
1. **Rithmomachia Focus:** "Play the 1,000-year-old medieval chess"
2. **Educational Value:** "Math learning disguised as strategy gaming"
3. **Multiplayer Fun:** "Challenge friends in mathematical duels"
4. **Historical Gaming:** "Rediscover forgotten board games"
5. **Family Friendly:** "Safe, educational gaming for all ages"
### Success Metrics
**Launch Goals:**
- **Wishlist:** 1,000+ before launch
- **Sales:** 500+ units in first month
- **Reviews:** 50+ reviews, 85%+ positive
- **Revenue:** $5,000+ in first quarter
**Long-term Goals:**
- **Sales:** 5,000+ units in first year
- **Reviews:** 200+ reviews, 90%+ positive
- **Revenue:** $30,000+ annually
- **Community:** Active Discord/forums with 1,000+ members
---
## Prioritization & Recommended Timeline
### Recommended Order of Execution
**Phase A: Quick Wins (First 3 months)**
1. **Google Classroom - Phase 1 (OAuth/SSO)** - Immediate teacher value
2. **CrazyGames submission** - Fast player discovery
3. **Google Classroom - Phase 2 (Class Import)** - Core teacher feature
**Phase B: Institution Access (Months 4-6)**
4. **Clever SSO integration** - Scale to districts
5. **ClassLink SSO integration** - Additional district reach
6. **Google Classroom - Phase 3 (Assignments)** - Deep integration
**Phase C: Casual Discovery (Months 6-9)**
7. **Poki submission** - Largest casual audience
8. **Kongregate pre-approval + submission** - Niche community
**Phase D: Premium Market (Months 9-12)**
9. **Steam development** - Desktop packaging + Steamworks
10. **Steam launch** - Premium educational game market
### Timeline Gantt Overview
```
Month 1-2: [Google OAuth/SSO] [CrazyGames Prep]
Month 2-3: [Google Class Import] [CrazyGames Launch]
Month 4-5: [Clever SSO] [ClassLink SSO]
Month 6-7: [Google Assignments] [Poki Prep]
Month 7-8: [Poki Launch] [Kongregate]
Month 9-10: [Steam Electron Build]
Month 10-11: [Steam Steamworks Integration]
Month 11-12: [Steam Store Page & Launch]
```
### Total Cost Summary
| Platform | One-time Cost | Ongoing Cost | Notes |
|----------|--------------|--------------|-------|
| Google Classroom | $0 | $0 | Free for education |
| Clever | $0 | $0 | Free for developers |
| ClassLink | $0 | $0 | Free for developers |
| CrazyGames | $0 | $0 | Revenue share via ads |
| Poki | $0 | $0 | 50/50 revenue share |
| Kongregate | $0 | $0 | Revenue share via ads |
| Steam | $100 | $0-200/mo | Server costs optional |
| **TOTAL** | **$100** | **$0-200/mo** | Very affordable |
### Resource Requirements
**Developer Time:**
- **Total effort:** 1,140-1,450 hours across all platforms
- **Timeline:** 9-12 months (with 1-2 developers)
- **Ongoing:** 20-60 hours/month maintenance
**External Costs:**
- Steam Direct fee: $100
- Optional video editing: $0-500
- Optional marketing budget: $0-1,000
- Server costs (if needed): $50-200/month
**Skills Needed:**
- OAuth/SAML implementation
- REST API integration
- Electron/desktop app packaging
- Steamworks SDK integration
- Marketing/community management
---
## Risk Management
### Top 5 Risks Across All Platforms
1. **Privacy Compliance (COPPA, FERPA, GDPR)**
- **Impact:** Critical (could block educational adoption)
- **Mitigation:** Legal review, implement consent systems, privacy policy updates
2. **Platform Rejection (Poki, Kongregate)**
- **Impact:** High (wasted development time)
- **Mitigation:** Apply early, start with less selective platforms, gather metrics
3. **Low Adoption/Sales**
- **Impact:** High (ROI concern)
- **Mitigation:** Strong marketing, community building, teacher outreach
4. **Technical Integration Complexity**
- **Impact:** Medium (timeline delays)
- **Mitigation:** Use established libraries, start simple, iterate
5. **Server Costs for Multiplayer**
- **Impact:** Medium (ongoing expenses)
- **Mitigation:** Optimize server efficiency, consider P2P, price accordingly
---
## Success Metrics Dashboard
### Key Performance Indicators (KPIs)
**User Acquisition:**
- New users per month (by source)
- Sign-up conversion rate
- Platform-specific downloads/plays
**Engagement:**
- Daily/Monthly Active Users (DAU/MAU)
- Average session length
- Games per session
- Return rate (D1, D7, D30)
**Education Impact:**
- Teachers registered
- Classes created
- Assignments completed
- Student progress metrics
**Revenue (Steam):**
- Units sold
- Revenue per month
- Refund rate
- Review score
**Platform-Specific:**
- **Google Classroom:** Classes imported, assignments created
- **Clever/ClassLink:** Districts connected, SSO logins
- **Game Portals:** Plays per game, ad revenue, ratings
- **Steam:** Sales, reviews, concurrent players
---
## Next Steps
### Immediate Actions (This Week)
1. **Decide on priority order** - Which platform to tackle first?
2. **Set up project tracking** - Dedicate repository/board for integration work
3. **Legal review** - Review privacy policy and terms for educational compliance
4. **Create developer accounts:**
- Google Cloud Platform
- Clever Developer Portal
- CrazyGames Developer Portal
- Poki submission prep
5. **Audit current codebase** - Check readiness for each integration
### Quick Wins to Start
**Option 1: Google Classroom OAuth (Fastest Impact)**
- 2-3 weeks to launch
- Immediate teacher value
- No approval needed
**Option 2: CrazyGames (Fastest Player Growth)**
- 3-4 weeks to launch
- Immediate player discovery
- Open platform (high acceptance rate)
**Recommendation:** Start both in parallel if resources allow
---
## Appendix
### Useful Links
**Google Classroom:**
- Developer Docs: https://developers.google.com/classroom
- API Reference: https://developers.google.com/classroom/reference
- OAuth Guide: https://developers.google.com/identity/protocols/oauth2
**Clever:**
- Developer Portal: https://dev.clever.com
- SSO Guide: https://dev.clever.com/docs/getting-started-with-clever-sso
- API Docs: https://dev.clever.com/docs/api-overview
**ClassLink:**
- SSO Library: https://www.classlink.com/resources/sso-search
- OneRoster: https://www.imsglobal.org/activity/onerosterlis
**Game Portals:**
- CrazyGames: https://docs.crazygames.com/
- Poki: https://developers.poki.com/
- Kongregate: BD@kongregate.com
**Steam:**
- Steamworks: https://partner.steamgames.com/
- Electron: https://www.electronjs.org/
- Greenworks: https://github.com/greenheartgames/greenworks
### Code Library Recommendations
**OAuth/SSO:**
- `next-auth` (already in use)
- `passport-saml` (for ClassLink SAML)
- `@googleapis/classroom` (official Google Classroom client)
**Electron:**
- `electron-builder` (packaging)
- `electron-updater` (auto-updates)
- `greenworks` (Steamworks bridge)
**Game Portal SDKs:**
- CrazyGames SDK (provided)
- Poki SDK (provided)
- Kongregate API (provided)
---
**Document End**
*For questions or updates to this roadmap, contact the development team.*

View File

@@ -1,390 +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

@@ -104,69 +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(gh run watch --exit-status 18662351595)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["sqlite"]
}
}

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,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 @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,32 +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",
"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",
"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": {
@@ -100,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

@@ -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

@@ -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() {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,220 +0,0 @@
#!/usr/bin/env tsx
/**
* Generate icon.svg and og-image.svg from AbacusReact component
*
* This script renders AbacusReact server-side to produce the exact same
* SVG output as the interactive client-side version (without animations).
*/
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { writeFileSync } from 'fs'
import { join } from 'path'
import { AbacusReact } from '@soroban/abacus-react'
// Extract just the SVG element content from rendered output
function extractSvgContent(markup: string): string {
// Find the opening <svg and closing </svg> tags
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
if (!svgMatch) {
throw new Error('No SVG element found in rendered output')
}
return svgMatch[1] // Return just the inner content
}
// Generate the favicon (icon.svg) - single column showing value 5
function generateFavicon(): string {
const abacusMarkup = renderToStaticMarkup(
<AbacusReact
value={5}
columns={1}
scaleFactor={1.0}
animated={false}
interactive={false}
showNumbers={false}
customStyles={{
heavenBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
earthBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
columnPosts: {
fill: '#451a03',
stroke: '#292524',
strokeWidth: 2,
},
reckoningBar: {
fill: '#292524',
stroke: '#292524',
strokeWidth: 3,
},
}}
/>
)
// Extract just the SVG content (without div wrapper)
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, '')
// Wrap in SVG with proper viewBox for favicon sizing
// AbacusReact with 1 column + scaleFactor 1.0 = ~25×120px
// Scale 0.7 = ~17.5×84px, centered in 100×100
return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle for better visibility -->
<circle cx="50" cy="50" r="48" fill="#fef3c7"/>
<!-- Abacus from @soroban/abacus-react -->
<g transform="translate(41, 8) scale(0.7)">
${svgContent}
</g>
</svg>
`
}
// Generate the Open Graph image (og-image.svg)
function generateOGImage(): string {
const abacusMarkup = renderToStaticMarkup(
<AbacusReact
value={1234}
columns={4}
scaleFactor={3.5}
animated={false}
interactive={false}
showNumbers={false}
customStyles={{
columnPosts: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
columns: {
0: {
// Ones place (rightmost) - Blue
heavenBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
earthBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
},
1: {
// Tens place - Green
heavenBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
earthBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
},
2: {
// Hundreds place - Yellow/Gold
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
},
3: {
// Thousands place (leftmost) - Purple
heavenBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
earthBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
},
},
}}
/>
)
// Extract just the SVG content (without div wrapper)
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, '')
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
<!-- Dark background like homepage -->
<rect width="1200" height="630" fill="#111827"/>
<!-- Subtle dot pattern background -->
<defs>
<pattern id="dots" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="1" fill="rgba(255, 255, 255, 0.15)" />
</pattern>
</defs>
<rect width="1200" height="630" fill="url(#dots)" opacity="0.1"/>
<!-- Left decorative elements - Diamond shapes and math operators -->
<g opacity="0.4">
<!-- Purple diamond (thousands) -->
<polygon points="150,120 180,150 150,180 120,150" fill="#c084fc" />
<!-- Gold diamond (hundreds) -->
<polygon points="150,220 180,250 150,280 120,250" fill="#fbbf24" />
<!-- Green diamond (tens) -->
<polygon points="150,320 180,350 150,380 120,350" fill="#4ade80" />
<!-- Blue diamond (ones) -->
<polygon points="150,420 180,450 150,480 120,450" fill="#60a5fa" />
</g>
<!-- Left math operators -->
<g opacity="0.35" fill="rgba(255, 255, 255, 0.8)">
<text x="80" y="100" font-family="Arial, sans-serif" font-size="42" font-weight="300">+</text>
<text x="240" y="190" font-family="Arial, sans-serif" font-size="42" font-weight="300">×</text>
<text x="70" y="290" font-family="Arial, sans-serif" font-size="42" font-weight="300">=</text>
<text x="250" y="390" font-family="Arial, sans-serif" font-size="42" font-weight="300"></text>
</g>
<!-- Right decorative elements - Diamond shapes and math operators -->
<g opacity="0.4">
<!-- Purple diamond (thousands) -->
<polygon points="1050,120 1080,150 1050,180 1020,150" fill="#c084fc" />
<!-- Gold diamond (hundreds) -->
<polygon points="1050,220 1080,250 1050,280 1020,250" fill="#fbbf24" />
<!-- Green diamond (tens) -->
<polygon points="1050,320 1080,350 1050,380 1020,350" fill="#4ade80" />
<!-- Blue diamond (ones) -->
<polygon points="1050,420 1080,450 1050,480 1020,450" fill="#60a5fa" />
</g>
<!-- Right math operators -->
<g opacity="0.35" fill="rgba(255, 255, 255, 0.8)">
<text x="940" y="160" font-family="Arial, sans-serif" font-size="42" font-weight="300">÷</text>
<text x="1110" y="270" font-family="Arial, sans-serif" font-size="42" font-weight="300">+</text>
<text x="920" y="360" font-family="Arial, sans-serif" font-size="42" font-weight="300">×</text>
<text x="1120" y="480" font-family="Arial, sans-serif" font-size="42" font-weight="300">=</text>
</g>
<!-- Huge centered abacus from @soroban/abacus-react -->
<!-- AbacusReact 4 columns @ scale 3.5: width ~350px, height ~420px -->
<!-- Center horizontally: (1200-350)/2 = 425px -->
<!-- Center vertically in upper portion: abacus middle at ~225px, so start at 225-210 = 15px -->
<g transform="translate(425, 15)">
${svgContent}
</g>
<!-- Title at bottom, horizontally and vertically centered in lower portion -->
<!-- Position at y=520 for vertical centering in bottom half -->
<text x="600" y="520" font-family="Arial, sans-serif" font-size="72" font-weight="bold" fill="url(#title-gradient)" text-anchor="middle">
Abaci One
</text>
<!-- Gold gradient for title -->
<defs>
<linearGradient id="title-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#fbbf24;stop-opacity:1" />
<stop offset="50%" style="stop-color:#f59e0b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#fbbf24;stop-opacity:1" />
</linearGradient>
</defs>
</svg>
`
}
// Main execution
const appDir = __dirname.replace('/scripts', '')
try {
console.log('Generating Open Graph image from AbacusReact...')
const ogImageSvg = generateOGImage()
writeFileSync(join(appDir, 'public', 'og-image.svg'), ogImageSvg)
console.log('✓ Generated public/og-image.svg')
console.log('\n✅ Icon generated successfully!')
console.log('\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx')
console.log('which calls scripts/generateDayIcon.tsx as a subprocess.')
} catch (error) {
console.error('❌ Error generating icons:', error)
process.exit(1)
}

View File

@@ -1,127 +0,0 @@
// Script to generate example worksheet images for the blog post
// Shows different scaffolding levels for the 2D difficulty blog post
import fs from 'fs'
import path from 'path'
import { generateWorksheetPreview } from '../src/app/create/worksheets/addition/generatePreview'
import { DIFFICULTY_PROFILES } from '../src/app/create/worksheets/addition/difficultyProfiles'
// Output directory
const outputDir = path.join(process.cwd(), 'public', 'blog', 'difficulty-examples')
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
// Generate examples with SAME regrouping level but different scaffolding
// This clearly shows how scaffolding changes while keeping problem complexity constant
const examples = [
{
name: 'full-scaffolding',
filename: 'full-scaffolding.svg',
description: 'Full Scaffolding: Maximum visual support',
// Use medium regrouping with full scaffolding
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: 'always' as const,
answerBoxes: 'always' as const,
placeValueColors: 'always' as const,
tenFrames: 'always' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
{
name: 'medium-scaffolding',
filename: 'medium-scaffolding.svg',
description: 'Medium Scaffolding: Strategic support',
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: 'whenRegrouping' as const,
answerBoxes: 'always' as const,
placeValueColors: 'when3PlusDigits' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
{
name: 'minimal-scaffolding',
filename: 'minimal-scaffolding.svg',
description: 'Minimal Scaffolding: Carry boxes only',
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: 'whenMultipleRegroups' as const,
answerBoxes: 'never' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
{
name: 'no-scaffolding',
filename: 'no-scaffolding.svg',
description: 'No Scaffolding: Students work independently',
config: {
pAllStart: 0.3,
pAnyStart: 0.7,
displayRules: {
carryBoxes: 'never' as const,
answerBoxes: 'never' as const,
placeValueColors: 'never' as const,
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
},
},
},
] as const
console.log('Generating blog example worksheets...\n')
for (const example of examples) {
console.log(`Generating ${example.description}...`)
const config = {
pAllStart: example.config.pAllStart,
pAnyStart: example.config.pAnyStart,
displayRules: example.config.displayRules,
problemsPerPage: 4,
pages: 1,
cols: 2,
}
try {
const result = generateWorksheetPreview(config)
if (!result.success || !result.pages || result.pages.length === 0) {
console.error(`Failed to generate ${example.name}:`, result.error)
continue
}
// Get the first page's SVG
const svg = result.pages[0]
// Save to file
const outputPath = path.join(outputDir, example.filename)
fs.writeFileSync(outputPath, svg, 'utf-8')
console.log(` ✓ Saved to ${outputPath}`)
} catch (error) {
console.error(` ✗ Error generating ${example.name}:`, error)
}
}
console.log('\nDone! Example worksheets generated.')
console.log(`\nFiles saved to: ${outputDir}`)

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env tsx
/**
* Generate a single day-of-month favicon
* Usage: npx tsx scripts/generateDayIcon.tsx <day>
* Example: npx tsx scripts/generateDayIcon.tsx 15
*/
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { AbacusStatic } from '@soroban/abacus-react'
// Extract just the SVG element from rendered output
function extractSvgElement(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
if (!svgMatch) {
throw new Error('No SVG element found in rendered output')
}
return svgMatch[0]
}
// Get day from command line argument
const day = parseInt(process.argv[2], 10)
if (!day || day < 1 || day > 31) {
console.error('Usage: npx tsx scripts/generateDayIcon.tsx <day>')
console.error('Example: npx tsx scripts/generateDayIcon.tsx 15')
process.exit(1)
}
// Render 2-column abacus showing day of month
// Using AbacusStatic for server-side rendering
const abacusMarkup = renderToStaticMarkup(
<AbacusStatic
value={day}
columns={2}
scaleFactor={1.8}
showNumbers={false}
hideInactiveBeads={true}
frameVisible={true}
cropToActiveBeads={{
padding: {
top: 8,
bottom: 2,
left: 5,
right: 5,
},
}}
customStyles={{
columnPosts: {
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 2,
},
reckoningBar: {
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 3,
},
columns: {
0: {
// Ones place - Gold (royal theme)
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
},
1: {
// Tens place - Purple (royal theme)
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
},
},
}}
/>
)
// Extract the cropped SVG
let croppedSvg = extractSvgElement(abacusMarkup)
// Remove !important from CSS (production code policy)
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
// Parse width and height from the cropped SVG
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
if (!widthMatch || !heightMatch) {
throw new Error('Could not parse dimensions from cropped SVG')
}
const croppedWidth = parseFloat(widthMatch[1])
const croppedHeight = parseFloat(heightMatch[1])
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
const targetSize = 96
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
// Center in 100x100 canvas
const scaledWidth = croppedWidth * scale
const scaledHeight = croppedHeight * scale
const offsetX = (100 - scaledWidth) / 2
const offsetY = (100 - scaledHeight) / 2
// Wrap in 100x100 SVG canvas for favicon
// Extract viewBox from cropped SVG to preserve it
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
viewBox="${viewBox}">
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
</svg>
</svg>
`
// Output to stdout so parent process can capture it
process.stdout.write(svg)

View File

@@ -1,206 +0,0 @@
#!/usr/bin/env node
/**
* Test script to parse the Rithmomachia board CSV and verify the layout.
*/
const fs = require('fs')
const path = require('path')
const csvPath = path.join(
process.env.HOME,
'Downloads',
'rithmomachia board setup - Sheet1 (1).csv'
)
function parseCSV(csvContent) {
const lines = csvContent.trim().split('\n')
const pieces = []
// Process in triplets (color, shape, number)
for (let rankIndex = 0; rankIndex < 16; rankIndex++) {
const colorRowIndex = rankIndex * 3
const shapeRowIndex = rankIndex * 3 + 1
const numberRowIndex = rankIndex * 3 + 2
if (numberRowIndex >= lines.length) break
const colorRow = lines[colorRowIndex].split(',')
const shapeRow = lines[shapeRowIndex].split(',')
const numberRow = lines[numberRowIndex].split(',')
// Process each column (8 total)
for (let colIndex = 0; colIndex < 8; colIndex++) {
const color = colorRow[colIndex]?.trim()
const shape = shapeRow[colIndex]?.trim()
const numberStr = numberRow[colIndex]?.trim()
// Skip empty cells (but allow empty number for Pyramids)
if (!color || !shape) continue
// Map CSV position to game square
// CSV column → game row (1-8)
// CSV rank → game column (A-P)
const gameRow = colIndex + 1 // CSV col 0 → row 1, col 7 → row 8
const gameCol = String.fromCharCode(65 + rankIndex) // rank 0 → A, rank 15 → P
const square = `${gameCol}${gameRow}`
// Parse color
const pieceColor = color.toLowerCase() === 'black' ? 'B' : 'W'
// Parse type
let pieceType
const shapeLower = shape.toLowerCase()
if (shapeLower === 'circle') pieceType = 'C'
else if (shapeLower === 'triangle' || shapeLower === 'traingle')
pieceType = 'T' // Handle typo
else if (shapeLower === 'square') pieceType = 'S'
else if (shapeLower === 'pyramid') pieceType = 'P'
else {
console.warn(`Unknown shape "${shape}" at ${square}`)
continue
}
// Parse value/pyramid faces
if (pieceType === 'P') {
// Pyramid - number cell should be empty, use default faces
pieces.push({
color: pieceColor,
type: pieceType,
pyramidFaces: pieceColor === 'B' ? [36, 25, 16, 4] : [64, 49, 36, 25],
square,
})
} else {
// Regular piece needs a number
if (!numberStr) {
console.warn(`Missing number for non-Pyramid ${shape} at ${square}`)
continue
}
const value = parseInt(numberStr, 10)
if (isNaN(value)) {
console.warn(`Invalid number "${numberStr}" at ${square}`)
continue
}
pieces.push({
color: pieceColor,
type: pieceType,
value,
square,
})
}
}
}
return pieces
}
function generateBoardDisplay(pieces) {
const lines = []
lines.push('\n=== Board Layout (Game Orientation) ===')
lines.push('BLACK (top)\n')
lines.push(
' A B C D E F G H I J K L M N O P'
)
for (let row = 8; row >= 1; row--) {
let line = `${row} `
for (let colCode = 65; colCode <= 80; colCode++) {
const col = String.fromCharCode(colCode)
const square = `${col}${row}`
const piece = pieces.find((p) => p.square === square)
if (piece) {
const val = piece.type === 'P' ? ' P' : piece.value.toString().padStart(3, ' ')
line += ` ${piece.color}${piece.type}${val} `
} else {
line += ' ---- '
}
}
lines.push(line)
}
lines.push('\nWHITE (bottom)\n')
return lines.join('\n')
}
function generateColumnSummaries(pieces) {
const lines = []
lines.push('\n=== Column-by-Column Summary ===\n')
for (let colCode = 65; colCode <= 80; colCode++) {
const col = String.fromCharCode(colCode)
const columnPieces = pieces
.filter((p) => p.square[0] === col)
.sort((a, b) => {
const rowA = parseInt(a.square.substring(1))
const rowB = parseInt(b.square.substring(1))
return rowA - rowB
})
if (columnPieces.length === 0) continue
const color = columnPieces[0].color === 'B' ? 'BLACK' : 'WHITE'
lines.push(`Column ${col} (${color}):`)
for (const piece of columnPieces) {
const val = piece.type === 'P' ? 'P[36,25,16,4]' : piece.value
lines.push(` ${piece.square}: ${piece.type}(${val})`)
}
lines.push('')
}
return lines.join('\n')
}
function countPieces(pieces) {
const blackPieces = pieces.filter((p) => p.color === 'B')
const whitePieces = pieces.filter((p) => p.color === 'W')
const countByType = (pieces) => {
const counts = { C: 0, T: 0, S: 0, P: 0 }
for (const p of pieces) counts[p.type]++
return counts
}
const blackCounts = countByType(blackPieces)
const whiteCounts = countByType(whitePieces)
console.log('\n=== Piece Counts ===')
console.log(
`Black: ${blackPieces.length} total (C:${blackCounts.C}, T:${blackCounts.T}, S:${blackCounts.S}, P:${blackCounts.P})`
)
console.log(
`White: ${whitePieces.length} total (C:${whiteCounts.C}, T:${whiteCounts.T}, S:${whiteCounts.S}, P:${whiteCounts.P})`
)
}
// Main
try {
const csvContent = fs.readFileSync(csvPath, 'utf-8')
const pieces = parseCSV(csvContent)
console.log(`\nParsed ${pieces.length} pieces from CSV`)
console.log(generateBoardDisplay(pieces))
console.log(generateColumnSummaries(pieces))
countPieces(pieces)
// Save parsed data
const outputPath = path.join(
__dirname,
'..',
'src',
'arcade-games',
'rithmomachia',
'utils',
'parsedBoard.json'
)
fs.writeFileSync(outputPath, JSON.stringify(pieces, null, 2))
console.log(`\n✅ Saved parsed board to: ${outputPath}`)
} catch (error) {
console.error('Error:', error.message)
process.exit(1)
}

View File

@@ -1,235 +0,0 @@
#!/bin/bash
# Google Classroom API Setup Script
# This script automates GCP project setup from the command line
#
# Prerequisites:
# - gcloud CLI installed (brew install google-cloud-sdk)
# - Valid Google account
#
# Usage:
# ./scripts/setup-google-classroom.sh
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Google Classroom API Setup${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
# Configuration
PROJECT_ID="soroban-abacus-$(date +%s)" # Unique project ID with timestamp
PROJECT_NAME="Soroban Abacus Flashcards"
BILLING_ACCOUNT="" # Will prompt user if needed
REDIRECT_URIS="http://localhost:3000/api/auth/callback/google,https://abaci.one/api/auth/callback/google"
echo -e "${YELLOW}Project ID:${NC} $PROJECT_ID"
echo ""
# Step 1: Check if gcloud is installed
echo -e "${BLUE}[1/9] Checking gcloud installation...${NC}"
if ! command -v gcloud &> /dev/null; then
echo -e "${RED}Error: gcloud CLI not found${NC}"
echo "Install it with: brew install google-cloud-sdk"
exit 1
fi
echo -e "${GREEN}✓ gcloud CLI found${NC}"
echo ""
# Step 2: Authenticate with Google
echo -e "${BLUE}[2/9] Authenticating with Google...${NC}"
CURRENT_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null || echo "")
if [ -z "$CURRENT_ACCOUNT" ]; then
echo "No active account found. Opening browser to authenticate..."
gcloud auth login
CURRENT_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)")
fi
echo -e "${GREEN}✓ Authenticated as: $CURRENT_ACCOUNT${NC}"
echo ""
# Step 3: Create GCP project
echo -e "${BLUE}[3/9] Creating GCP project...${NC}"
echo "Creating project: $PROJECT_ID"
gcloud projects create "$PROJECT_ID" --name="$PROJECT_NAME" 2>/dev/null || {
echo -e "${YELLOW}Project might already exist, continuing...${NC}"
}
gcloud config set project "$PROJECT_ID"
echo -e "${GREEN}✓ Project created/selected: $PROJECT_ID${NC}"
echo ""
# Step 4: Check billing (required for APIs)
echo -e "${BLUE}[4/9] Checking billing account...${NC}"
echo -e "${YELLOW}Note: Google Classroom API requires a billing account, but it's FREE for educational use.${NC}"
echo -e "${YELLOW}You won't be charged unless you explicitly enable paid services.${NC}"
echo ""
# List available billing accounts
BILLING_ACCOUNTS=$(gcloud billing accounts list --format="value(name)" 2>/dev/null || echo "")
if [ -z "$BILLING_ACCOUNTS" ]; then
echo -e "${YELLOW}No billing accounts found.${NC}"
echo -e "${YELLOW}You'll need to create one at: https://console.cloud.google.com/billing${NC}"
echo -e "${YELLOW}Press Enter after creating a billing account, or Ctrl+C to exit${NC}"
read -r
BILLING_ACCOUNTS=$(gcloud billing accounts list --format="value(name)")
fi
# If multiple accounts, let user choose
BILLING_COUNT=$(echo "$BILLING_ACCOUNTS" | wc -l | tr -d ' ')
if [ "$BILLING_COUNT" -eq 1 ]; then
BILLING_ACCOUNT="$BILLING_ACCOUNTS"
else
echo "Available billing accounts:"
gcloud billing accounts list
echo ""
echo -n "Enter billing account ID (e.g., 012345-ABCDEF-678901): "
read -r BILLING_ACCOUNT
fi
# Link billing account to project
echo "Linking billing account to project..."
gcloud billing projects link "$PROJECT_ID" --billing-account="$BILLING_ACCOUNT"
echo -e "${GREEN}✓ Billing account linked${NC}"
echo ""
# Step 5: Enable required APIs
echo -e "${BLUE}[5/9] Enabling required APIs...${NC}"
echo "This may take 1-2 minutes..."
gcloud services enable classroom.googleapis.com --project="$PROJECT_ID"
gcloud services enable people.googleapis.com --project="$PROJECT_ID"
echo -e "${GREEN}✓ APIs enabled:${NC}"
echo " - Google Classroom API"
echo " - Google People API (for profile info)"
echo ""
# Step 6: Create OAuth 2.0 credentials
echo -e "${BLUE}[6/9] Creating OAuth 2.0 credentials...${NC}"
# First check if credentials already exist
EXISTING_CREDS=$(gcloud auth application-default print-access-token &>/dev/null && \
curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
"https://oauth2.googleapis.com/v1/projects/${PROJECT_ID}/oauthClients" 2>/dev/null | \
grep -c "clientId" || echo "0")
if [ "$EXISTING_CREDS" -gt 0 ]; then
echo -e "${YELLOW}OAuth credentials already exist for this project${NC}"
echo "Skipping credential creation..."
else
# Create OAuth client
# Note: This creates a "Web application" type OAuth client
echo "Creating OAuth 2.0 client..."
# Unfortunately, gcloud doesn't have a direct command for this
# We need to use the REST API
echo -e "${YELLOW}Note: OAuth client creation requires using the web console${NC}"
echo -e "${YELLOW}Opening the OAuth credentials page...${NC}"
echo ""
echo -e "${BLUE}Please follow these steps in the browser:${NC}"
echo "1. Click 'Create Credentials' → 'OAuth client ID'"
echo "2. Application type: 'Web application'"
echo "3. Name: 'Soroban Abacus Web'"
echo "4. Authorized JavaScript origins:"
echo " - http://localhost:3000"
echo " - https://abaci.one"
echo "5. Authorized redirect URIs:"
echo " - http://localhost:3000/api/auth/callback/google"
echo " - https://abaci.one/api/auth/callback/google"
echo "6. Click 'Create'"
echo "7. Copy the Client ID and Client Secret"
echo ""
# Open browser to credentials page
open "https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID" 2>/dev/null || \
echo "Open this URL: https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID"
echo -n "Press Enter after creating the OAuth client..."
read -r
fi
echo -e "${GREEN}✓ OAuth credentials configured${NC}"
echo ""
# Step 7: Configure OAuth Consent Screen
echo -e "${BLUE}[7/9] Configuring OAuth consent screen...${NC}"
echo -e "${YELLOW}The OAuth consent screen requires web console configuration${NC}"
echo ""
echo -e "${BLUE}Please follow these steps:${NC}"
echo "1. User Type: 'External' (unless you have Google Workspace)"
echo "2. App name: 'Soroban Abacus Flashcards'"
echo "3. User support email: Your email"
echo "4. Developer contact: Your email"
echo "5. Scopes: Click 'Add or Remove Scopes' and add:"
echo " - .../auth/userinfo.email"
echo " - .../auth/userinfo.profile"
echo " - .../auth/classroom.courses.readonly"
echo " - .../auth/classroom.rosters.readonly"
echo "6. Test users: Add your email for testing"
echo "7. Save and continue"
echo ""
# Open OAuth consent screen configuration
open "https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID" 2>/dev/null || \
echo "Open this URL: https://console.cloud.google.com/apis/credentials/consent?project=$PROJECT_ID"
echo -n "Press Enter after configuring the consent screen..."
read -r
echo -e "${GREEN}✓ OAuth consent screen configured${NC}"
echo ""
# Step 8: Create .env.local file
echo -e "${BLUE}[8/9] Creating environment configuration...${NC}"
echo ""
echo "Please enter your OAuth credentials from the previous step:"
echo -n "Client ID: "
read -r CLIENT_ID
echo -n "Client Secret: "
read -r -s CLIENT_SECRET
echo ""
# Create or update .env.local
ENV_FILE=".env.local"
if [ -f "$ENV_FILE" ]; then
echo "Backing up existing $ENV_FILE to ${ENV_FILE}.backup"
cp "$ENV_FILE" "${ENV_FILE}.backup"
fi
# Add Google OAuth credentials
echo "" >> "$ENV_FILE"
echo "# Google OAuth (Generated by setup-google-classroom.sh)" >> "$ENV_FILE"
echo "GOOGLE_CLIENT_ID=\"$CLIENT_ID\"" >> "$ENV_FILE"
echo "GOOGLE_CLIENT_SECRET=\"$CLIENT_SECRET\"" >> "$ENV_FILE"
echo "" >> "$ENV_FILE"
echo -e "${GREEN}✓ Environment variables added to $ENV_FILE${NC}"
echo ""
# Step 9: Summary
echo -e "${BLUE}[9/9] Setup Complete!${NC}"
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}Setup Summary${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "Project ID: ${BLUE}$PROJECT_ID${NC}"
echo -e "Project Name: ${BLUE}$PROJECT_NAME${NC}"
echo -e "Billing Account: ${BLUE}$BILLING_ACCOUNT${NC}"
echo -e "APIs Enabled: ${BLUE}Classroom, People${NC}"
echo ""
echo -e "${YELLOW}Next Steps:${NC}"
echo "1. Add Google provider to NextAuth configuration"
echo "2. Test login with 'Sign in with Google'"
echo "3. Verify Classroom API access"
echo ""
echo -e "${BLUE}Useful Commands:${NC}"
echo " View project: gcloud projects describe $PROJECT_ID"
echo " List APIs: gcloud services list --enabled --project=$PROJECT_ID"
echo " View quota: gcloud quotas describe classroom.googleapis.com --project=$PROJECT_ID"
echo ""
echo -e "${GREEN}Setup script complete!${NC}"

View File

@@ -1,109 +0,0 @@
import {
DIFFICULTY_PROFILES,
makeHarder,
makeEasier,
findRegroupingIndex,
findScaffoldingIndex,
REGROUPING_PROGRESSION,
SCAFFOLDING_PROGRESSION,
} from "../src/app/create/worksheets/addition/difficultyProfiles";
// Start from beginner
let state = {
pAnyStart: DIFFICULTY_PROFILES.beginner.regrouping.pAnyStart,
pAllStart: DIFFICULTY_PROFILES.beginner.regrouping.pAllStart,
displayRules: DIFFICULTY_PROFILES.beginner.displayRules,
};
console.log("=== MAKE HARDER PATH ===\n");
console.log("Format: (regroupingIdx, scaffoldingIdx) - description\n");
const harderPath: Array<{ r: number; s: number; desc: string }> = [];
// Record starting point
let rIdx = findRegroupingIndex(state.pAnyStart, state.pAllStart);
let sIdx = findScaffoldingIndex(state.displayRules);
harderPath.push({ r: rIdx, s: sIdx, desc: "START (beginner)" });
console.log(`(${rIdx}, ${sIdx}) - START (beginner)`);
// Click "Make Harder" 30 times or until max
for (let i = 0; i < 30; i++) {
const result = makeHarder(state);
const newR = findRegroupingIndex(result.pAnyStart, result.pAllStart);
const newS = findScaffoldingIndex(result.displayRules);
if (newR === rIdx && newS === sIdx) {
console.log(`\n(${newR}, ${newS}) - ${result.changeDescription} (STOPPED)`);
break;
}
rIdx = newR;
sIdx = newS;
state = result;
harderPath.push({ r: rIdx, s: sIdx, desc: result.changeDescription });
console.log(`(${rIdx}, ${sIdx}) - ${result.changeDescription}`);
}
console.log("\n\n=== PATH VISUALIZATION ===\n");
console.log("Regrouping Index →");
console.log("Scaffolding ↓\n");
// Create 2D grid visualization
const grid: string[][] = [];
for (let s = 0; s <= 12; s++) {
grid[s] = [];
for (let r = 0; r <= 18; r++) {
grid[s][r] = " ·";
}
}
// Mark path
harderPath.forEach((point, idx) => {
if (idx === 0) {
grid[point.s][point.r] = " S"; // Start
} else if (idx === harderPath.length - 1) {
grid[point.s][point.r] = " E"; // End
} else {
grid[point.s][point.r] = `${idx.toString().padStart(3)}`;
}
});
// Mark presets
const presets = [
{ label: "BEG", profile: DIFFICULTY_PROFILES.beginner },
{ label: "EAR", profile: DIFFICULTY_PROFILES.earlyLearner },
{ label: "INT", profile: DIFFICULTY_PROFILES.intermediate },
{ label: "ADV", profile: DIFFICULTY_PROFILES.advanced },
{ label: "EXP", profile: DIFFICULTY_PROFILES.expert },
];
presets.forEach((preset) => {
const r = findRegroupingIndex(
preset.profile.regrouping.pAnyStart,
preset.profile.regrouping.pAllStart,
);
const s = findScaffoldingIndex(preset.profile.displayRules);
// Only mark if not already part of path
const onPath = harderPath.some((p) => p.r === r && p.s === s);
if (!onPath) {
grid[s][r] = preset.label;
}
});
// Print grid (inverted so scaffolding increases upward)
console.log(
" 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18",
);
for (let s = 12; s >= 0; s--) {
console.log(`${s.toString().padStart(2)} ${grid[s].join("")}`);
}
console.log("\nLegend:");
console.log(" S = Start (beginner)");
console.log(" E = End (maximum)");
console.log(" 1-29 = Step number");
console.log(" BEG/EAR/INT/ADV/EXP = Preset profiles");
console.log(" · = Not visited");

View File

@@ -34,36 +34,10 @@ app.prepare().then(() => {
}
})
// Debug: Check upgrade handlers at each stage
console.log('📊 Stage 1 - After server creation:')
console.log(` Upgrade handlers: ${server.listeners('upgrade').length}`)
// Initialize Socket.IO
const { initializeSocketServer } = require('./dist/socket-server')
console.log('📊 Stage 2 - Before initializeSocketServer:')
console.log(` Upgrade handlers: ${server.listeners('upgrade').length}`)
initializeSocketServer(server)
console.log('📊 Stage 3 - After initializeSocketServer:')
const allHandlers = server.listeners('upgrade')
console.log(` Upgrade handlers: ${allHandlers.length}`)
allHandlers.forEach((handler, i) => {
console.log(` [${i}] ${handler.name || 'anonymous'} (length: ${handler.length} params)`)
})
// Log all upgrade requests to see handler execution order
const originalEmit = server.emit.bind(server)
server.emit = (event, ...args) => {
if (event === 'upgrade') {
const req = args[0]
console.log(`\n🔄 UPGRADE REQUEST: ${req.url}`)
console.log(` ${allHandlers.length} handlers will be called`)
}
return originalEmit(event, ...args)
}
server
.once('error', (err) => {
console.error(err)

View File

@@ -1,121 +0,0 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getRoomMembers } from '@/lib/arcade/room-membership'
import { getPlayer, getRoomActivePlayers, setPlayerActiveStatus } from '@/lib/arcade/player-manager'
import { getViewerId } from '@/lib/viewer'
import { getSocketIO } from '@/lib/socket-io'
type RouteContext = {
params: Promise<{ roomId: string }>
}
/**
* POST /api/arcade/rooms/:roomId/deactivate-player
* Deactivate a specific player in the room (host only)
* Body:
* - playerId: string - The player to deactivate
*/
export async function POST(req: NextRequest, context: RouteContext) {
console.log('[Deactivate Player API] POST request received')
try {
const { roomId } = await context.params
console.log('[Deactivate Player API] roomId:', roomId)
const viewerId = await getViewerId()
console.log('[Deactivate Player API] viewerId:', viewerId)
const body = await req.json()
console.log('[Deactivate Player API] body:', body)
// Validate required fields
if (!body.playerId) {
console.log('[Deactivate Player API] Missing playerId in body')
return NextResponse.json({ error: 'Missing required field: playerId' }, { status: 400 })
}
// Check if user is the host
console.log('[Deactivate Player API] Fetching room members for roomId:', roomId)
const members = await getRoomMembers(roomId)
console.log('[Deactivate Player API] members count:', members.length)
const currentMember = members.find((m) => m.userId === viewerId)
console.log('[Deactivate Player API] currentMember:', currentMember)
if (!currentMember) {
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
}
if (!currentMember.isCreator) {
return NextResponse.json({ error: 'Only the host can deactivate players' }, { status: 403 })
}
// Get the player
console.log('[Deactivate Player API] Looking up player with ID:', body.playerId)
const player = await getPlayer(body.playerId)
console.log('[Deactivate Player API] Player found:', player)
if (!player) {
console.log('[Deactivate Player API] Player not found in database')
return NextResponse.json({ error: 'Player not found' }, { status: 404 })
}
console.log('[Deactivate Player API] Player userId:', player.userId)
console.log(
'[Deactivate Player API] Room member userIds:',
members.map((m) => m.userId)
)
// Can't deactivate your own players (use the regular player controls for that)
if (player.userId === viewerId) {
console.log('[Deactivate Player API] ERROR: Cannot deactivate your own players')
return NextResponse.json(
{ error: 'Cannot deactivate your own players. Use the player controls in the nav.' },
{ status: 400 }
)
}
// Note: We don't check if the player belongs to a current room member
// because players from users who have left the room may still need to be cleaned up
console.log('[Deactivate Player API] Player validation passed, proceeding with deactivation')
// Deactivate the player
await setPlayerActiveStatus(body.playerId, false)
// Broadcast updates via socket
const io = await getSocketIO()
if (io) {
try {
// Get updated player list
const memberPlayers = await getRoomActivePlayers(roomId)
// Convert memberPlayers Map to object for JSON serialization
const memberPlayersObj: Record<string, any[]> = {}
for (const [uid, players] of memberPlayers.entries()) {
memberPlayersObj[uid] = players
}
// Notify everyone in the room about the player update
io.to(`room:${roomId}`).emit('player-deactivated', {
roomId,
playerId: body.playerId,
playerName: player.name,
deactivatedBy: currentMember.displayName,
memberPlayers: memberPlayersObj,
})
console.log(
`[Deactivate Player API] Player ${body.playerId} (${player.name}) deactivated by host in room ${roomId}`
)
} catch (socketError) {
console.error('[Deactivate Player API] Failed to broadcast deactivation:', socketError)
}
}
console.log('[Deactivate Player API] Success - returning 200')
return NextResponse.json({ success: true }, { status: 200 })
} catch (error: any) {
console.error('[Deactivate Player API] ERROR:', error)
console.error('[Deactivate Player API] Error stack:', error.stack)
return NextResponse.json({ error: 'Failed to deactivate player' }, { status: 500 })
}
}

View File

@@ -1,7 +1,7 @@
import bcrypt from 'bcryptjs'
import { type NextRequest, NextResponse } from 'next/server'
import { getActivePlayers, getRoomActivePlayers } from '@/lib/arcade/player-manager'
import { getInvitation, acceptInvitation } from '@/lib/arcade/room-invitations'
import { getInvitation } from '@/lib/arcade/room-invitations'
import { getJoinRequest } from '@/lib/arcade/room-join-requests'
import { getRoomById, touchRoom } from '@/lib/arcade/room-manager'
import { addRoomMember, getRoomMembers } from '@/lib/arcade/room-membership'
@@ -26,19 +26,12 @@ export async function POST(req: NextRequest, context: RouteContext) {
const viewerId = await getViewerId()
const body = await req.json().catch(() => ({}))
console.log(`[Join API] User ${viewerId} attempting to join room ${roomId}`)
// Get room
const room = await getRoomById(roomId)
if (!room) {
console.log(`[Join API] Room ${roomId} not found`)
return NextResponse.json({ error: 'Room not found' }, { status: 404 })
}
console.log(
`[Join API] Room ${roomId} found: name="${room.name}" accessMode="${room.accessMode}" game="${room.gameName}"`
)
// Check if user is banned
const banned = await isUserBanned(roomId, viewerId)
if (banned) {
@@ -50,20 +43,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
const isExistingMember = members.some((m) => m.userId === viewerId)
const isRoomCreator = room.createdBy === viewerId
// Track invitation/join request to mark as accepted after successful join
let invitationToAccept: string | null = null
let joinRequestToAccept: string | null = null
// Check for pending invitation (regardless of access mode)
// This ensures invitations are marked as accepted when user joins ANY room type
const invitation = await getInvitation(roomId, viewerId)
if (invitation && invitation.status === 'pending') {
invitationToAccept = invitation.id
console.log(
`[Join API] Found pending invitation ${invitation.id} for user ${viewerId} in room ${roomId}`
)
}
// Validate access mode
switch (room.accessMode) {
case 'locked':
@@ -104,20 +83,16 @@ export async function POST(req: NextRequest, context: RouteContext) {
}
case 'restricted': {
console.log(`[Join API] Room is restricted, checking invitation for user ${viewerId}`)
// Room creator can always rejoin their own room
if (!isRoomCreator) {
// For restricted rooms, invitation is REQUIRED
if (!invitationToAccept) {
console.log(`[Join API] No valid pending invitation, rejecting join`)
// Check for valid pending invitation
const invitation = await getInvitation(roomId, viewerId)
if (!invitation || invitation.status !== 'pending') {
return NextResponse.json(
{ error: 'You need a valid invitation to join this room' },
{ status: 403 }
)
}
console.log(`[Join API] Valid invitation found, will accept after member added`)
} else {
console.log(`[Join API] User is room creator, skipping invitation check`)
}
break
}
@@ -133,9 +108,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
{ status: 403 }
)
}
// Note: Join request stays in "approved" status after join
// (No need to update it - "approved" indicates they were allowed in)
joinRequestToAccept = joinRequest.id
}
break
}
@@ -163,13 +135,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
isCreator: false,
})
// Mark invitation as accepted (if applicable)
if (invitationToAccept) {
await acceptInvitation(invitationToAccept)
console.log(`[Join API] Accepted invitation ${invitationToAccept} for user ${viewerId}`)
}
// Note: Join requests stay in "approved" status (no need to update)
// Fetch user's active players (these will participate in the game)
const activePlayers = await getActivePlayers(viewerId)
@@ -205,10 +170,6 @@ export async function POST(req: NextRequest, context: RouteContext) {
}
// Build response with auto-leave info if applicable
console.log(
`[Join API] Successfully added user ${viewerId} to room ${roomId} (invitation=${invitationToAccept ? 'accepted' : 'N/A'})`
)
return NextResponse.json(
{
member,

View File

@@ -17,17 +17,12 @@ type RouteContext = {
/**
* PATCH /api/arcade/rooms/:roomId/settings
* Update room settings
*
* Authorization:
* - gameConfig: Any room member can update
* - All other settings: Host only
*
* Update room settings (host only)
* Body:
* - accessMode?: 'open' | 'locked' | 'retired' | 'password' | 'restricted' | 'approval-only' (host only)
* - password?: string (plain text, will be hashed) (host only)
* - gameName?: string | null (any game with a registered validator) (host only)
* - gameConfig?: object (game-specific settings) (any member)
* - accessMode?: 'open' | 'locked' | 'retired' | 'password' | 'restricted' | 'approval-only'
* - password?: string (plain text, will be hashed)
* - gameName?: string | null (any game with a registered validator)
* - gameConfig?: object (game-specific settings)
*
* Note: gameName is validated at runtime against the validator registry.
* No need to update this file when adding new games!
@@ -68,7 +63,7 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
)
)
// Check if user is a room member
// Check if user is the host
const members = await getRoomMembers(roomId)
const currentMember = members.find((m) => m.userId === viewerId)
@@ -76,24 +71,8 @@ export async function PATCH(req: NextRequest, context: RouteContext) {
return NextResponse.json({ error: 'You are not in this room' }, { status: 403 })
}
// Determine which settings are being changed
const changingRoomSettings = !!(
body.accessMode !== undefined ||
body.password !== undefined ||
body.gameName !== undefined ||
body.name !== undefined ||
body.description !== undefined
)
// Only gameConfig can be changed by any member
// All other settings require host privileges
if (changingRoomSettings && !currentMember.isCreator) {
return NextResponse.json(
{
error: 'Only the host can change room settings (name, access mode, game selection, etc.)',
},
{ status: 403 }
)
if (!currentMember.isCreator) {
return NextResponse.json({ error: 'Only the host can change room settings' }, { status: 403 })
}
// Validate accessMode if provided

View File

@@ -1,12 +0,0 @@
import { NextResponse } from 'next/server'
import { getFeaturedPosts } from '@/lib/blog'
export async function GET() {
try {
const posts = await getFeaturedPosts()
return NextResponse.json(posts)
} catch (error) {
console.error('Error fetching featured posts:', error)
return NextResponse.json({ error: 'Failed to fetch featured posts' }, { status: 500 })
}
}

View File

@@ -1,138 +0,0 @@
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import { generateMonthlyTypst, generateDailyTypst, getDaysInMonth } from '../utils/typstGenerator'
import type { AbacusConfig } from '@soroban/abacus-react'
import { generateCalendarComposite } from '@/utils/calendar/generateCalendarComposite'
import { generateAbacusElement } from '@/utils/calendar/generateCalendarAbacus'
interface CalendarRequest {
month: number
year: number
format: 'monthly' | 'daily'
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
abacusConfig?: AbacusConfig
}
export async function POST(request: NextRequest) {
let tempDir: string | null = null
try {
// Dynamic import to avoid Next.js bundler issues with react-dom/server
const { renderToStaticMarkup } = await import('react-dom/server')
const body: CalendarRequest = await request.json()
const { month, year, format, paperSize, abacusConfig } = body
// Validate inputs
if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
}
// Create temp directory for SVG files
tempDir = join(tmpdir(), `calendar-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
// Generate and write SVG files
const daysInMonth = getDaysInMonth(year, month)
let typstContent: string
if (format === 'monthly') {
// Generate single composite SVG for monthly calendar
const calendarSvg = generateCalendarComposite({
month,
year,
renderToString: renderToStaticMarkup,
})
if (!calendarSvg || calendarSvg.trim().length === 0) {
throw new Error('Generated empty composite calendar SVG')
}
writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
// Generate Typst document
typstContent = generateMonthlyTypst({
month,
year,
paperSize,
daysInMonth,
})
} else {
// Daily format: generate individual SVGs for each day
for (let day = 1; day <= daysInMonth; day++) {
const svg = renderToStaticMarkup(generateAbacusElement(day, 2))
if (!svg || svg.trim().length === 0) {
throw new Error(`Generated empty SVG for day ${day}`)
}
writeFileSync(join(tempDir, `day-${day}.svg`), svg)
}
// Generate year SVG
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
if (!yearSvg || yearSvg.trim().length === 0) {
throw new Error(`Generated empty SVG for year ${year}`)
}
writeFileSync(join(tempDir, 'year.svg'), yearSvg)
// Generate Typst document
typstContent = generateDailyTypst({
month,
year,
paperSize,
daysInMonth,
})
}
// Compile with Typst: stdin for .typ content, stdout for PDF output
let pdfBuffer: Buffer
try {
pdfBuffer = execSync('typst compile --format pdf - -', {
input: typstContent,
cwd: tempDir, // Run in temp dir so relative paths work
maxBuffer: 50 * 1024 * 1024, // 50MB limit for large calendars
})
} catch (error) {
console.error('Typst compilation error:', error)
return NextResponse.json(
{ error: 'Failed to compile PDF. Is Typst installed?' },
{ status: 500 }
)
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
// Return JSON with PDF
return NextResponse.json({
pdf: pdfBuffer.toString('base64'),
filename: `calendar-${year}-${String(month).padStart(2, '0')}.pdf`,
})
} catch (error) {
console.error('Error generating calendar:', error)
// Clean up temp directory if it exists
if (tempDir) {
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
console.error('Failed to clean up temp directory:', cleanupError)
}
}
// Surface the actual error for debugging
const errorMessage = error instanceof Error ? error.message : String(error)
const errorStack = error instanceof Error ? error.stack : undefined
return NextResponse.json(
{
error: 'Failed to generate calendar',
message: errorMessage,
...(process.env.NODE_ENV === 'development' && { stack: errorStack }),
},
{ status: 500 }
)
}
}

View File

@@ -1,201 +0,0 @@
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import { generateMonthlyTypst, getDaysInMonth } from '../utils/typstGenerator'
import { generateCalendarComposite } from '@/utils/calendar/generateCalendarComposite'
import { generateAbacusElement } from '@/utils/calendar/generateCalendarAbacus'
interface PreviewRequest {
month: number
year: number
format: 'monthly' | 'daily'
}
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest) {
let tempDir: string | null = null
try {
const body: PreviewRequest = await request.json()
const { month, year, format } = body
// Validate inputs
if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
}
// Dynamic import to avoid Next.js bundler issues
const { renderToStaticMarkup } = await import('react-dom/server')
// Create temp directory for SVG file(s)
tempDir = join(tmpdir(), `calendar-preview-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
// Generate Typst document content
const daysInMonth = getDaysInMonth(year, month)
let typstContent: string
if (format === 'monthly') {
// Generate and write composite SVG
const calendarSvg = generateCalendarComposite({
month,
year,
renderToString: renderToStaticMarkup,
})
writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
typstContent = generateMonthlyTypst({
month,
year,
paperSize: 'us-letter',
daysInMonth,
})
} else {
// Daily format: Create a SINGLE composite SVG (like monthly) to avoid multi-image export issue
// Generate individual abacus SVGs
const daySvg = renderToStaticMarkup(generateAbacusElement(1, 2))
if (!daySvg || daySvg.trim().length === 0) {
throw new Error('Generated empty SVG for day 1')
}
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
if (!yearSvg || yearSvg.trim().length === 0) {
throw new Error(`Generated empty SVG for year ${year}`)
}
// Create composite SVG with both year and day abacus
const monthName = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
][month - 1]
const dayOfWeek = new Date(year, month - 1, 1).toLocaleDateString('en-US', {
weekday: 'long',
})
// Extract SVG content (remove outer <svg> tags)
const yearSvgContent = yearSvg.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
const daySvgContent = daySvg.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
// Create composite SVG (850x1100 = US Letter aspect ratio)
const compositeWidth = 850
const compositeHeight = 1100
const yearAbacusWidth = 120 // Natural width at scale 1
const yearAbacusHeight = 230
const dayAbacusWidth = 120
const dayAbacusHeight = 230
const compositeSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${compositeWidth}" height="${compositeHeight}" viewBox="0 0 ${compositeWidth} ${compositeHeight}">
<!-- Background -->
<rect width="${compositeWidth}" height="${compositeHeight}" fill="white"/>
<!-- Decorative border -->
<rect x="40" y="40" width="${compositeWidth - 80}" height="${compositeHeight - 80}" fill="none" stroke="#2563eb" stroke-width="3" rx="8"/>
<rect x="50" y="50" width="${compositeWidth - 100}" height="${compositeHeight - 100}" fill="none" stroke="#2563eb" stroke-width="1" rx="4"/>
<!-- Header section with background -->
<rect x="70" y="70" width="${compositeWidth - 140}" height="120" fill="#eff6ff" stroke="#2563eb" stroke-width="2" rx="6"/>
<!-- Month name -->
<text x="${compositeWidth / 2}" y="125" text-anchor="middle" font-family="Georgia, serif" font-size="48" font-weight="bold" fill="#1e40af" letter-spacing="2">
${monthName.toUpperCase()}
</text>
<!-- Year abacus (smaller, in header) -->
<svg x="${compositeWidth / 2 - yearAbacusWidth * 0.4}" y="140" width="${yearAbacusWidth * 0.8}" height="${yearAbacusHeight * 0.8}" viewBox="0 0 ${yearAbacusWidth} ${yearAbacusHeight}">
${yearSvgContent}
</svg>
<!-- Day of week (large and prominent) -->
<text x="${compositeWidth / 2}" y="260" text-anchor="middle" font-family="Georgia, serif" font-size="42" font-weight="bold" fill="#1e3a8a">
${dayOfWeek}
</text>
<!-- Day abacus (much larger, main focus) -->
<svg x="${compositeWidth / 2 - dayAbacusWidth * 1.25}" y="300" width="${dayAbacusWidth * 2.5}" height="${dayAbacusHeight * 2.5}" viewBox="0 0 ${dayAbacusWidth} ${dayAbacusHeight}">
${daySvgContent}
</svg>
<!-- Full date (below day abacus) -->
<text x="${compositeWidth / 2}" y="890" text-anchor="middle" font-family="Georgia, serif" font-size="24" font-weight="500" fill="#475569">
${monthName} 1, ${year}
</text>
<!-- Notes section with decorative box -->
<rect x="70" y="930" width="${compositeWidth - 140}" height="120" fill="#fefce8" stroke="#ca8a04" stroke-width="2" rx="4"/>
<text x="90" y="960" font-family="Georgia, serif" font-size="18" font-weight="bold" fill="#854d0e">
Notes:
</text>
<line x1="90" y1="980" x2="${compositeWidth - 90}" y2="980" stroke="#ca8a04" stroke-width="1"/>
<line x1="90" y1="1005" x2="${compositeWidth - 90}" y2="1005" stroke="#ca8a04" stroke-width="1"/>
<line x1="90" y1="1030" x2="${compositeWidth - 90}" y2="1030" stroke="#ca8a04" stroke-width="1"/>
</svg>`
writeFileSync(join(tempDir, 'daily-preview.svg'), compositeSvg)
// Use single composite image (like monthly)
typstContent = `#set page(
paper: "us-letter",
margin: (x: 0.5in, y: 0.5in),
)
#align(center + horizon)[
#image("daily-preview.svg", width: 100%, fit: "contain")
]
`
}
// Compile with Typst: stdin for .typ content, stdout for SVG output
let svg: string
try {
svg = execSync('typst compile --format svg - -', {
input: typstContent,
encoding: 'utf8',
cwd: tempDir, // Run in temp dir so relative paths work
})
} catch (error) {
console.error('Typst compilation error:', error)
return NextResponse.json(
{ error: 'Failed to compile preview. Is Typst installed?' },
{ status: 500 }
)
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
return NextResponse.json({ svg })
} catch (error) {
console.error('Error generating preview:', error)
// Clean up temp directory if it exists
if (tempDir) {
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
console.error('Failed to clean up temp directory:', cleanupError)
}
}
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json(
{ error: 'Failed to generate preview', message: errorMessage },
{ status: 500 }
)
}
}

View File

@@ -1,195 +0,0 @@
interface TypstMonthlyConfig {
month: number
year: number
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
daysInMonth: number
}
interface TypstDailyConfig {
month: number
year: number
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
daysInMonth: number
}
const MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
export function getDaysInMonth(year: number, month: number): number {
return new Date(year, month, 0).getDate()
}
function getFirstDayOfWeek(year: number, month: number): number {
return new Date(year, month - 1, 1).getDay() // 0 = Sunday
}
function getDayOfWeek(year: number, month: number, day: number): string {
const date = new Date(year, month - 1, day)
return date.toLocaleDateString('en-US', { weekday: 'long' })
}
type PaperSize = 'us-letter' | 'a4' | 'a3' | 'tabloid'
interface PaperConfig {
typstName: string
marginX: string
marginY: string
}
function getPaperConfig(size: string): PaperConfig {
const configs: Record<PaperSize, PaperConfig> = {
// Tight margins to maximize space for calendar grid
'us-letter': { typstName: 'us-letter', marginX: '0.5in', marginY: '0.5in' },
// A4 is slightly taller/narrower than US Letter - adjust margins proportionally
a4: { typstName: 'a4', marginX: '1.3cm', marginY: '1.3cm' },
// A3 is 2x area of A4 - can use same margins but will scale content larger
a3: { typstName: 'a3', marginX: '1.5cm', marginY: '1.5cm' },
// Tabloid (11" × 17") is larger - can use more margin
tabloid: { typstName: 'us-tabloid', marginX: '0.75in', marginY: '0.75in' },
}
return configs[size as PaperSize] || configs['us-letter']
}
export function generateMonthlyTypst(config: TypstMonthlyConfig): string {
const { paperSize } = config
const paperConfig = getPaperConfig(paperSize)
// Single-page design: use one composite SVG that scales to fit
// This prevents overflow - Typst will scale the image to fit available space
return `#set page(
paper: "${paperConfig.typstName}",
margin: (x: ${paperConfig.marginX}, y: ${paperConfig.marginY}),
)
// Composite calendar SVG - scales to fit page (prevents multi-page overflow)
#align(center + horizon)[
#image("calendar.svg", width: 100%, fit: "contain")
]
`
}
export function generateDailyTypst(config: TypstDailyConfig): string {
const { month, year, paperSize, daysInMonth } = config
const paperConfig = getPaperConfig(paperSize)
const monthName = MONTH_NAMES[month - 1]
let pages = ''
for (let day = 1; day <= daysInMonth; day++) {
const dayOfWeek = getDayOfWeek(year, month, day)
pages += `
#page(
paper: "${paperConfig.typstName}",
margin: (x: ${paperConfig.marginX}, y: ${paperConfig.marginY}),
)[
#set text(font: "Georgia")
// Decorative borders
#rect(
width: 100%,
height: 100%,
stroke: (paint: rgb("#2563eb"), thickness: 3pt),
radius: 8pt,
inset: 0pt,
)[
#rect(
width: 100%,
height: 100%,
stroke: (paint: rgb("#2563eb"), thickness: 1pt),
radius: 4pt,
inset: 10pt,
)[
#v(10pt)
// Header section with background
#rect(
width: 100%,
height: 90pt,
fill: rgb("#eff6ff"),
stroke: (paint: rgb("#2563eb"), thickness: 2pt),
radius: 6pt,
)[
#align(center)[
#v(15pt)
#text(size: 32pt, weight: "bold", fill: rgb("#1e40af"), tracking: 2pt)[
${monthName.toUpperCase()}
]
#v(5pt)
#image("year.svg", width: 15%)
]
]
#v(15pt)
// Day of week (large and prominent)
#align(center)[
#text(size: 28pt, weight: "bold", fill: rgb("#1e3a8a"))[
${dayOfWeek}
]
]
#v(10pt)
// Day abacus (main focus, large)
#align(center)[
#image("day-${day}.svg", width: 45%)
]
#v(10pt)
// Full date
#align(center)[
#text(size: 18pt, weight: 500, fill: rgb("#475569"))[
${monthName} ${day}, ${year}
]
]
#v(1fr)
// Notes section with decorative box
#rect(
width: 100%,
height: 90pt,
fill: rgb("#fefce8"),
stroke: (paint: rgb("#ca8a04"), thickness: 2pt),
radius: 4pt,
)[
#v(8pt)
#text(size: 14pt, weight: "bold", fill: rgb("#854d0e"))[
#h(10pt) Notes:
]
#v(8pt)
#line(length: 95%, stroke: (paint: rgb("#ca8a04"), thickness: 1pt))
#v(8pt)
#line(length: 95%, stroke: (paint: rgb("#ca8a04"), thickness: 1pt))
#v(8pt)
#line(length: 95%, stroke: (paint: rgb("#ca8a04"), thickness: 1pt))
]
#v(10pt)
]
]
]
${day < daysInMonth ? '' : ''}`
if (day < daysInMonth) {
pages += '\n'
}
}
return pages
}

View File

@@ -1,188 +0,0 @@
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import type { FlashcardFormState } from '@/app/create/flashcards/page'
import {
generateFlashcardFront,
generateFlashcardBack,
} from '@/utils/flashcards/generateFlashcardSvgs'
export const dynamic = 'force-dynamic'
/**
* Parse range string to get numbers for preview (first page only)
*/
function parseRangeForPreview(range: string, step: number, cardsPerPage: number): number[] {
const numbers: number[] = []
if (range.includes('-')) {
const [start, end] = range.split('-').map((n) => parseInt(n, 10))
for (let i = start; i <= end && numbers.length < cardsPerPage; i += step) {
numbers.push(i)
}
} else if (range.includes(',')) {
const parts = range.split(',').map((n) => parseInt(n.trim(), 10))
numbers.push(...parts.slice(0, cardsPerPage))
} else {
numbers.push(parseInt(range, 10))
}
return numbers.slice(0, cardsPerPage)
}
export async function POST(request: NextRequest) {
let tempDir: string | null = null
try {
const body: FlashcardFormState = await request.json()
const {
range = '0-99',
step = 1,
cardsPerPage = 6,
paperSize = 'us-letter',
orientation = 'portrait',
beadShape = 'diamond',
colorScheme = 'place-value',
colorPalette = 'default',
hideInactiveBeads = false,
showEmptyColumns = false,
columns = 'auto',
scaleFactor = 0.9,
coloredNumerals = false,
} = body
// Dynamic import to avoid Next.js bundler issues
const { renderToStaticMarkup } = await import('react-dom/server')
// Create temp directory for SVG files
tempDir = join(tmpdir(), `flashcards-preview-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
// Get numbers for first page only
const numbers = parseRangeForPreview(range, step, cardsPerPage)
if (numbers.length === 0) {
return NextResponse.json({ error: 'No valid numbers in range' }, { status: 400 })
}
// Generate SVG files for each card (front and back)
const config = {
beadShape,
colorScheme,
colorPalette,
hideInactiveBeads,
showEmptyColumns,
columns: columns === 'auto' ? 'auto' : Number(columns),
scaleFactor,
coloredNumerals,
}
for (let i = 0; i < numbers.length; i++) {
const num = numbers[i]
// Generate front (abacus)
const frontElement = generateFlashcardFront(num, config)
const frontSvg = renderToStaticMarkup(frontElement)
writeFileSync(join(tempDir, `card_${i}_front.svg`), frontSvg)
// Generate back (numeral)
const backElement = generateFlashcardBack(num, config)
const backSvg = renderToStaticMarkup(backElement)
writeFileSync(join(tempDir, `card_${i}_back.svg`), backSvg)
}
// Calculate card dimensions based on paper size and orientation
const paperDimensions = {
'us-letter': { width: 8.5, height: 11 },
a4: { width: 8.27, height: 11.69 },
a3: { width: 11.69, height: 16.54 },
a5: { width: 5.83, height: 8.27 },
}
const paper = paperDimensions[paperSize] || paperDimensions['us-letter']
const [pageWidth, pageHeight] =
orientation === 'landscape' ? [paper.height, paper.width] : [paper.width, paper.height]
// Calculate grid layout (2 columns × 3 rows for 6 cards per page typically)
const cols = 2
const rows = Math.ceil(cardsPerPage / cols)
const margin = 0.5 // inches
const gutter = 0.2 // inches between cards
const availableWidth = pageWidth - 2 * margin - gutter * (cols - 1)
const availableHeight = pageHeight - 2 * margin - gutter * (rows - 1)
const cardWidth = availableWidth / cols
const cardHeight = availableHeight / rows
// Generate Typst document with card grid
const typstContent = `
#set page(
paper: "${paperSize}",
margin: (x: ${margin}in, y: ${margin}in),
flipped: ${orientation === 'landscape'},
)
// Grid layout for flashcards preview (first page only)
#grid(
columns: ${cols},
rows: ${rows},
column-gutter: ${gutter}in,
row-gutter: ${gutter}in,
${numbers
.map((_, i) => {
return ` image("card_${i}_front.svg", width: ${cardWidth}in, height: ${cardHeight}in, fit: "contain"),`
})
.join('\n')}
)
// Add preview label
#place(
top + right,
dx: -0.5in,
dy: 0.25in,
text(10pt, fill: gray)[Preview (first ${numbers.length} cards)]
)
`
// Compile with Typst: stdin for .typ content, stdout for SVG output
let svg: string
try {
svg = execSync('typst compile --format svg - -', {
input: typstContent,
encoding: 'utf8',
cwd: tempDir, // Run in temp dir so relative paths work
})
} catch (error) {
console.error('Typst compilation error:', error)
return NextResponse.json(
{ error: 'Failed to compile preview. Is Typst installed?' },
{ status: 500 }
)
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
return NextResponse.json({ svg })
} catch (error) {
console.error('Error generating preview:', error)
// Clean up temp directory if it exists
if (tempDir) {
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
console.error('Failed to clean up temp directory:', cleanupError)
}
}
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json(
{ error: 'Failed to generate preview', message: errorMessage },
{ status: 500 }
)
}
}

View File

@@ -1,114 +0,0 @@
// API route for generating compact addition problem examples for display option previews
import { type NextRequest, NextResponse } from 'next/server'
import { execSync } from 'child_process'
import { generateProblems } from '@/app/create/worksheets/addition/problemGenerator'
import {
generateTypstHelpers,
generateProblemStackFunction,
} from '@/app/create/worksheets/addition/typstHelpers'
export const dynamic = 'force-dynamic'
interface ExampleRequest {
showCarryBoxes?: boolean
showAnswerBoxes?: boolean
showPlaceValueColors?: boolean
showProblemNumbers?: boolean
showCellBorder?: boolean
showTenFrames?: boolean
showTenFramesForAll?: boolean
fontSize?: number
addend1?: number
addend2?: number
}
/**
* Generate a single compact problem example showing the combined display options
* Uses the EXACT same Typst structure as the full worksheet generator
*/
function generateExampleTypst(config: ExampleRequest): string {
// Use custom addends if provided, otherwise generate a problem
let a: number
let b: number
if (config.addend1 !== undefined && config.addend2 !== undefined) {
a = config.addend1
b = config.addend2
} else {
// Generate a simple 2-digit + 2-digit problem with carries
const problems = generateProblems(1, 0.8, 0.5, false, 12345)
const problem = problems[0]
a = problem.a
b = problem.b
}
const fontSize = config.fontSize || 14
const cellSize = 0.35 // Compact cell size for examples
// Boolean flags matching worksheet generator
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
const showTenFramesForAll = config.showTenFramesForAll ?? false
return String.raw`
#set page(width: auto, height: auto, margin: 8pt, fill: white)
#set text(size: ${fontSize}pt, font: "New Computer Modern Math")
#let heavy-stroke = 0.8pt
#let show-carries = ${showCarries ? 'true' : 'false'}
#let show-answers = ${showAnswers ? 'true' : 'false'}
#let show-colors = ${showColors ? 'true' : 'false'}
#let show-numbers = ${showNumbers ? 'true' : 'false'}
#let show-ten-frames = ${showTenFrames ? 'true' : 'false'}
#let show-ten-frames-for-all = ${showTenFramesForAll ? 'true' : '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, if show-numbers { 0 } else { none })
]
`
}
export async function POST(request: NextRequest) {
try {
const body: ExampleRequest = await request.json()
// Generate Typst source with all display options
const typstSource = generateExampleTypst(body)
// Compile to SVG
const svg = execSync('typst compile --format svg - -', {
input: typstSource,
encoding: 'utf8',
maxBuffer: 2 * 1024 * 1024,
})
return NextResponse.json({ svg })
} catch (error) {
console.error('Error generating example:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json(
{
error: 'Failed to generate example',
message: errorMessage,
},
{ status: 500 }
)
}
}

View File

@@ -1,41 +0,0 @@
// API route for generating addition worksheet previews (SVG)
import { type NextRequest, NextResponse } from 'next/server'
import { generateWorksheetPreview } from '@/app/create/worksheets/addition/generatePreview'
import type { WorksheetFormState } from '@/app/create/worksheets/addition/types'
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest) {
try {
const body: WorksheetFormState = await request.json()
// Generate preview using shared logic
const result = generateWorksheetPreview(body)
if (!result.success) {
return NextResponse.json(
{
error: result.error,
details: result.details,
},
{ status: 400 }
)
}
// Return pages as JSON
return NextResponse.json({ pages: result.pages })
} catch (error) {
console.error('Error generating preview:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json(
{
error: 'Failed to generate preview',
message: errorMessage,
},
{ status: 500 }
)
}
}

View File

@@ -1,90 +0,0 @@
// API route for generating addition worksheets
import { type NextRequest, NextResponse } from 'next/server'
import { execSync } from 'child_process'
import { validateWorksheetConfig } from '@/app/create/worksheets/addition/validation'
import { generateProblems } from '@/app/create/worksheets/addition/problemGenerator'
import { generateTypstSource } from '@/app/create/worksheets/addition/typstGenerator'
import type { WorksheetFormState } from '@/app/create/worksheets/addition/types'
export async function POST(request: NextRequest) {
try {
const body: WorksheetFormState = await request.json()
// Validate configuration
const validation = validateWorksheetConfig(body)
if (!validation.isValid || !validation.config) {
return NextResponse.json(
{ error: 'Invalid configuration', errors: validation.errors },
{ status: 400 }
)
}
const config = validation.config
// Generate problems
const problems = generateProblems(
config.total,
config.pAnyStart,
config.pAllStart,
config.interpolate,
config.seed
)
// Generate Typst sources (one per page)
const typstSources = generateTypstSource(config, problems)
// Join pages with pagebreak for PDF
const typstSource = typstSources.join('\n\n#pagebreak()\n\n')
// Compile with Typst: stdin → stdout
let pdfBuffer: Buffer
try {
pdfBuffer = execSync('typst compile --format pdf - -', {
input: typstSource,
maxBuffer: 10 * 1024 * 1024, // 10MB limit
})
} catch (error) {
console.error('Typst compilation error:', error)
// Extract the actual Typst error message
const stderr =
error instanceof Error && 'stderr' in error
? String((error as any).stderr)
: 'Unknown compilation error'
return NextResponse.json(
{
error: 'Failed to compile worksheet PDF',
details: stderr,
...(process.env.NODE_ENV === 'development' && {
typstSource: typstSource.split('\n').slice(0, 20).join('\n') + '\n...',
}),
},
{ status: 500 }
)
}
// Return binary PDF directly
return new Response(pdfBuffer as unknown as BodyInit, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="addition-worksheet-${Date.now()}.pdf"`,
},
})
} catch (error) {
console.error('Error generating worksheet:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
const errorStack = error instanceof Error ? error.stack : undefined
return NextResponse.json(
{
error: 'Failed to generate worksheet',
message: errorMessage,
...(process.env.NODE_ENV === 'development' && { stack: errorStack }),
},
{ status: 500 }
)
}
}

View File

@@ -4,9 +4,6 @@ import { getActivePlayers } from '@/lib/arcade/player-manager'
import { db, schema } from '@/db'
import { eq } from 'drizzle-orm'
// Force dynamic rendering - this route uses headers()
export const dynamic = 'force-dynamic'
/**
* GET /api/debug/active-players
* Debug endpoint to check active players for current user

View File

@@ -1,237 +1,68 @@
import { SorobanGenerator } from '@soroban/core'
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import type { FlashcardConfig } from '@/app/create/flashcards/page'
import {
generateFlashcardFront,
generateFlashcardBack,
} from '@/utils/flashcards/generateFlashcardSvgs'
import path from 'path'
export const dynamic = 'force-dynamic'
// Global generator instance for better performance
let generator: SorobanGenerator | null = null
/**
* Parse range string to get all numbers
*/
function parseRange(range: string, step: number): number[] {
const numbers: number[] = []
async function getGenerator() {
if (!generator) {
// Point to the core package in our monorepo
const corePackagePath = path.join(process.cwd(), '../../packages/core')
generator = new SorobanGenerator(corePackagePath)
if (range.includes('-')) {
const [start, end] = range.split('-').map((n) => parseInt(n, 10))
for (let i = start; i <= end; i += step) {
numbers.push(i)
}
} else if (range.includes(',')) {
const parts = range.split(',').map((n) => parseInt(n.trim(), 10))
numbers.push(...parts)
} else {
numbers.push(parseInt(range, 10))
}
return numbers
}
/**
* Shuffle array with seed for reproducibility
*/
function shuffleWithSeed<T>(array: T[], seed?: number): T[] {
const shuffled = [...array]
const rng = seed !== undefined ? seededRandom(seed) : Math.random
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
return shuffled
}
/**
* Simple seeded random number generator (Mulberry32)
*/
function seededRandom(seed: number) {
return () => {
seed = (seed + 0x6d2b79f5) | 0
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
// Note: SorobanGenerator from @soroban/core doesn't have initialize method
// It uses one-shot mode by default
}
return generator
}
export async function POST(request: NextRequest) {
let tempDir: string | null = null
try {
const config: FlashcardConfig = await request.json()
const {
range = '0-99',
step = 1,
cardsPerPage = 6,
paperSize = 'us-letter',
orientation = 'portrait',
margins,
gutter = '5mm',
shuffle = false,
seed,
showCutMarks = false,
showRegistration = false,
beadShape = 'diamond',
colorScheme = 'place-value',
colorPalette = 'default',
hideInactiveBeads = false,
showEmptyColumns = false,
columns = 'auto',
scaleFactor = 0.9,
coloredNumerals = false,
format = 'pdf',
} = config
const config = await request.json()
// Dynamic import to avoid Next.js bundler issues
const { renderToStaticMarkup } = await import('react-dom/server')
// Debug: log the received config
console.log('📥 Received config:', JSON.stringify(config, null, 2))
// Create temp directory for SVG files
tempDir = join(tmpdir(), `flashcards-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
// Get all numbers
let numbers = parseRange(range, step)
// Apply shuffle if requested
if (shuffle) {
numbers = shuffleWithSeed(numbers, seed)
// Ensure range is set with a default
if (!config.range) {
console.log('⚠️ No range provided, using default: 0-99')
config.range = '0-99'
}
if (numbers.length === 0) {
return NextResponse.json({ error: 'No valid numbers in range' }, { status: 400 })
}
// Get generator instance
const gen = await getGenerator()
// Generate SVG files for each card (front and back)
const svgConfig = {
beadShape,
colorScheme,
colorPalette,
hideInactiveBeads,
showEmptyColumns,
columns: columns === 'auto' ? 'auto' : Number(columns),
scaleFactor,
coloredNumerals,
}
for (let i = 0; i < numbers.length; i++) {
const num = numbers[i]
// Generate front (abacus)
const frontElement = generateFlashcardFront(num, svgConfig)
const frontSvg = renderToStaticMarkup(frontElement)
writeFileSync(join(tempDir, `card_${i}_front.svg`), frontSvg)
// Generate back (numeral)
const backElement = generateFlashcardBack(num, svgConfig)
const backSvg = renderToStaticMarkup(backElement)
writeFileSync(join(tempDir, `card_${i}_back.svg`), backSvg)
}
// Calculate paper dimensions and layout
const paperDimensions = {
'us-letter': { width: 8.5, height: 11 },
a4: { width: 8.27, height: 11.69 },
a3: { width: 11.69, height: 16.54 },
a5: { width: 5.83, height: 8.27 },
}
const paper = paperDimensions[paperSize] || paperDimensions['us-letter']
const [pageWidth, pageHeight] =
orientation === 'landscape' ? [paper.height, paper.width] : [paper.width, paper.height]
// Calculate grid layout (typically 2 columns × 3 rows for 6 cards)
const cols = 2
const rows = Math.ceil(cardsPerPage / cols)
// Use provided margins or defaults
const margin = {
top: margins?.top || '0.5in',
bottom: margins?.bottom || '0.5in',
left: margins?.left || '0.5in',
right: margins?.right || '0.5in',
}
// Parse gutter (convert from string like "5mm" to inches for calculation)
const gutterInches = parseFloat(gutter) / 25.4 // Rough mm to inch conversion
// Calculate available space (approximate, Typst will handle exact layout)
const marginInches = 0.5 // Simplified for now
const availableWidth = pageWidth - 2 * marginInches - gutterInches * (cols - 1)
const availableHeight = pageHeight - 2 * marginInches - gutterInches * (rows - 1)
const cardWidth = availableWidth / cols
const cardHeight = availableHeight / rows
// Generate pages
const totalPages = Math.ceil(numbers.length / cardsPerPage)
const pages: string[] = []
for (let pageNum = 0; pageNum < totalPages; pageNum++) {
const startIdx = pageNum * cardsPerPage
const endIdx = Math.min(startIdx + cardsPerPage, numbers.length)
const pageCards = []
for (let i = startIdx; i < endIdx; i++) {
pageCards.push(
` image("card_${i}_front.svg", width: ${cardWidth}in, height: ${cardHeight}in, fit: "contain")`
)
}
// Fill remaining slots with empty cells if needed
const remaining = cardsPerPage - pageCards.length
for (let i = 0; i < remaining; i++) {
pageCards.push(` []`) // Empty cell
}
pages.push(`#grid(
columns: ${cols},
rows: ${rows},
column-gutter: ${gutter},
row-gutter: ${gutter},
${pageCards.join(',\n')}
)`)
}
// Generate Typst document
const typstContent = `
#set page(
paper: "${paperSize}",
margin: (x: ${margin.left}, y: ${margin.top}),
flipped: ${orientation === 'landscape'},
)
${pages.join('\n\n#pagebreak()\n\n')}
`
// Compile with Typst
let pdfBuffer: Buffer
try {
pdfBuffer = execSync('typst compile --format pdf - -', {
input: typstContent,
cwd: tempDir, // Run in temp dir so relative paths work
maxBuffer: 100 * 1024 * 1024, // 100MB limit for large sets
})
} catch (error) {
console.error('Typst compilation error:', error)
// Check dependencies before generating
const deps = await gen.checkDependencies?.()
if (deps && (!deps.python || !deps.typst)) {
return NextResponse.json(
{ error: 'Failed to compile PDF. Is Typst installed?' },
{
error: 'Missing system dependencies',
details: {
python: deps.python ? '✅ Available' : '❌ Missing Python 3',
typst: deps.typst ? '✅ Available' : '❌ Missing Typst',
qpdf: deps.qpdf ? '✅ Available' : '⚠️ Missing qpdf (optional)',
},
},
{ status: 500 }
)
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
// Generate flashcards using Python via TypeScript bindings
console.log('🚀 Generating flashcards with config:', JSON.stringify(config, null, 2))
const result = await gen.generate(config)
// SorobanGenerator.generate() returns PDF data directly as Buffer
if (!Buffer.isBuffer(result)) {
throw new Error(`Expected PDF Buffer from generator, got: ${typeof result}`)
}
const pdfBuffer = result
// Create filename for download
const filename = `soroban-flashcards-${range}.pdf`
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
// Return PDF directly as download
return new NextResponse(pdfBuffer, {
return new NextResponse(new Uint8Array(pdfBuffer), {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
@@ -239,45 +70,70 @@ ${pages.join('\n\n#pagebreak()\n\n')}
},
})
} catch (error) {
console.error('Error generating flashcards:', error)
console.error('❌ Generation failed:', error)
// Clean up temp directory if it exists
if (tempDir) {
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
console.error('Failed to clean up temp directory:', cleanupError)
}
}
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json(
{ error: 'Failed to generate flashcards', message: errorMessage },
{
error: 'Failed to generate flashcards',
details: error instanceof Error ? error.message : 'Unknown error',
success: false,
},
{ status: 500 }
)
}
}
// Helper functions to calculate metadata
function _calculateCardCount(range: string, step: number): number {
if (range.includes('-')) {
const [start, end] = range.split('-').map((n) => parseInt(n, 10) || 0)
return Math.floor((end - start + 1) / step)
}
if (range.includes(',')) {
return range.split(',').length
}
return 1
}
function _generateNumbersFromRange(range: string, step: number): number[] {
if (range.includes('-')) {
const [start, end] = range.split('-').map((n) => parseInt(n, 10) || 0)
const numbers: number[] = []
for (let i = start; i <= end; i += step) {
numbers.push(i)
if (numbers.length >= 100) break // Limit to prevent huge arrays
}
return numbers
}
if (range.includes(',')) {
return range.split(',').map((n) => parseInt(n.trim(), 10) || 0)
}
return [parseInt(range, 10) || 0]
}
// Health check endpoint
export async function GET() {
try {
// Check if Typst is available
execSync('typst --version', { encoding: 'utf8' })
const gen = await getGenerator()
const deps = (await gen.checkDependencies?.()) || {
python: true,
typst: true,
qpdf: true,
}
return NextResponse.json({
status: 'healthy',
generator: 'typescript-typst',
dependencies: {
typst: true,
python: false, // No longer needed!
},
dependencies: deps,
})
} catch (error) {
return NextResponse.json(
{
status: 'unhealthy',
error: 'Typst not available',
message: error instanceof Error ? error.message : 'Unknown error',
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)

View File

@@ -1,111 +0,0 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
import { playerStats } from '@/db/schema/player-stats'
import { players } from '@/db/schema/players'
import type { GetPlayerStatsResponse, PlayerStatsData } from '@/lib/arcade/stats/types'
import { getViewerId } from '@/lib/viewer'
/**
* GET /api/player-stats/[playerId]
*
* Fetches stats for a specific player (must be owned by current user).
*/
export async function GET(_request: Request, { params }: { params: { playerId: string } }) {
try {
const { playerId } = params
// 1. Authenticate user
const viewerId = await getViewerId()
if (!viewerId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Verify player belongs to user
const player = await db
.select()
.from(players)
.where(eq(players.id, playerId))
.limit(1)
.then((rows) => rows[0])
if (!player) {
return NextResponse.json({ error: 'Player not found' }, { status: 404 })
}
if (player.userId !== viewerId) {
return NextResponse.json(
{ error: 'Forbidden: player belongs to another user' },
{ status: 403 }
)
}
// 3. Fetch player stats
const stats = await db
.select()
.from(playerStats)
.where(eq(playerStats.playerId, playerId))
.limit(1)
.then((rows) => rows[0])
const playerStatsData: PlayerStatsData = stats
? convertToPlayerStatsData(stats)
: createDefaultPlayerStats(playerId)
// 4. Return response
const response: GetPlayerStatsResponse = {
stats: playerStatsData,
}
return NextResponse.json(response)
} catch (error) {
console.error('❌ Failed to fetch player stats:', error)
return NextResponse.json(
{
error: 'Failed to fetch player stats',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}
/**
* Convert DB record to PlayerStatsData
*/
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
return {
playerId: dbStats.playerId,
gamesPlayed: dbStats.gamesPlayed,
totalWins: dbStats.totalWins,
totalLosses: dbStats.totalLosses,
bestTime: dbStats.bestTime,
highestAccuracy: dbStats.highestAccuracy,
favoriteGameType: dbStats.favoriteGameType,
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
lastPlayedAt: dbStats.lastPlayedAt,
createdAt: dbStats.createdAt,
updatedAt: dbStats.updatedAt,
}
}
/**
* Create default player stats for new player
*/
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
const now = new Date()
return {
playerId,
gamesPlayed: 0,
totalWins: 0,
totalLosses: 0,
bestTime: null,
highestAccuracy: 0,
favoriteGameType: null,
gameStats: {},
lastPlayedAt: null,
createdAt: now,
updatedAt: now,
}
}

View File

@@ -1,277 +0,0 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
import { playerStats } from '@/db/schema/player-stats'
import type {
GameResult,
PlayerGameResult,
PlayerStatsData,
RecordGameRequest,
RecordGameResponse,
StatsUpdate,
} from '@/lib/arcade/stats/types'
import { getViewerId } from '@/lib/viewer'
/**
* POST /api/player-stats/record-game
*
* Records a game result and updates player stats for all participants.
* Supports cooperative games (team wins/losses) and competitive games.
*/
export async function POST(request: Request) {
try {
// 1. Authenticate user
const viewerId = await getViewerId()
if (!viewerId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Parse and validate request
const body: RecordGameRequest = await request.json()
const { gameResult } = body
if (!gameResult || !gameResult.playerResults || gameResult.playerResults.length === 0) {
return NextResponse.json(
{ error: 'Invalid game result: playerResults required' },
{ status: 400 }
)
}
if (!gameResult.gameType) {
return NextResponse.json({ error: 'Invalid game result: gameType required' }, { status: 400 })
}
// 3. Process each player's result
const updates: StatsUpdate[] = []
for (const playerResult of gameResult.playerResults) {
const update = await recordPlayerResult(gameResult, playerResult)
updates.push(update)
}
// 4. Return success response
const response: RecordGameResponse = {
success: true,
updates,
}
return NextResponse.json(response)
} catch (error) {
console.error('❌ Failed to record game result:', error)
return NextResponse.json(
{
error: 'Failed to record game result',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}
/**
* Records stats for a single player's game result
*/
async function recordPlayerResult(
gameResult: GameResult,
playerResult: PlayerGameResult
): Promise<StatsUpdate> {
const { playerId } = playerResult
// 1. Fetch or create player stats
const existingStats = await db
.select()
.from(playerStats)
.where(eq(playerStats.playerId, playerId))
.limit(1)
.then((rows) => rows[0])
const previousStats: PlayerStatsData = existingStats
? convertToPlayerStatsData(existingStats)
: createDefaultPlayerStats(playerId)
// 2. Calculate new stats
const newStats: PlayerStatsData = { ...previousStats }
// Always increment games played
newStats.gamesPlayed++
// Handle wins/losses (cooperative vs competitive)
if (gameResult.metadata?.isTeamVictory !== undefined) {
// Cooperative game: all players share outcome
if (playerResult.won) {
newStats.totalWins++
} else {
newStats.totalLosses++
}
} else {
// Competitive/Solo: individual outcome
if (playerResult.won) {
newStats.totalWins++
} else {
newStats.totalLosses++
}
}
// Update best time (if provided and improved)
if (playerResult.completionTime) {
if (!newStats.bestTime || playerResult.completionTime < newStats.bestTime) {
newStats.bestTime = playerResult.completionTime
}
}
// Update highest accuracy (if provided and improved)
if (playerResult.accuracy !== undefined && playerResult.accuracy > newStats.highestAccuracy) {
newStats.highestAccuracy = playerResult.accuracy
}
// Update per-game stats (JSON)
const gameType = gameResult.gameType
const currentGameStats: GameStatsBreakdown = newStats.gameStats[gameType] || {
gamesPlayed: 0,
wins: 0,
losses: 0,
bestTime: null,
highestAccuracy: 0,
averageScore: 0,
lastPlayed: 0,
}
currentGameStats.gamesPlayed++
if (playerResult.won) {
currentGameStats.wins++
} else {
currentGameStats.losses++
}
// Update game-specific best time
if (playerResult.completionTime) {
if (!currentGameStats.bestTime || playerResult.completionTime < currentGameStats.bestTime) {
currentGameStats.bestTime = playerResult.completionTime
}
}
// Update game-specific highest accuracy
if (
playerResult.accuracy !== undefined &&
playerResult.accuracy > currentGameStats.highestAccuracy
) {
currentGameStats.highestAccuracy = playerResult.accuracy
}
// Update average score
if (playerResult.score !== undefined) {
const previousTotal = currentGameStats.averageScore * (currentGameStats.gamesPlayed - 1)
currentGameStats.averageScore =
(previousTotal + playerResult.score) / currentGameStats.gamesPlayed
}
currentGameStats.lastPlayed = gameResult.completedAt
newStats.gameStats[gameType] = currentGameStats
// Update favorite game type (most played)
newStats.favoriteGameType = getMostPlayedGame(newStats.gameStats)
// Update timestamps
newStats.lastPlayedAt = new Date(gameResult.completedAt)
newStats.updatedAt = new Date()
// 3. Save to database
if (existingStats) {
// Update existing record
await db
.update(playerStats)
.set({
gamesPlayed: newStats.gamesPlayed,
totalWins: newStats.totalWins,
totalLosses: newStats.totalLosses,
bestTime: newStats.bestTime,
highestAccuracy: newStats.highestAccuracy,
favoriteGameType: newStats.favoriteGameType,
gameStats: newStats.gameStats as any, // Drizzle JSON type
lastPlayedAt: newStats.lastPlayedAt,
updatedAt: newStats.updatedAt,
})
.where(eq(playerStats.playerId, playerId))
} else {
// Insert new record
await db.insert(playerStats).values({
playerId: newStats.playerId,
gamesPlayed: newStats.gamesPlayed,
totalWins: newStats.totalWins,
totalLosses: newStats.totalLosses,
bestTime: newStats.bestTime,
highestAccuracy: newStats.highestAccuracy,
favoriteGameType: newStats.favoriteGameType,
gameStats: newStats.gameStats as any,
lastPlayedAt: newStats.lastPlayedAt,
createdAt: newStats.createdAt,
updatedAt: newStats.updatedAt,
})
}
// 4. Return update summary
return {
playerId,
previousStats,
newStats,
changes: {
gamesPlayed: newStats.gamesPlayed - previousStats.gamesPlayed,
wins: newStats.totalWins - previousStats.totalWins,
losses: newStats.totalLosses - previousStats.totalLosses,
},
}
}
/**
* Convert DB record to PlayerStatsData
*/
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
return {
playerId: dbStats.playerId,
gamesPlayed: dbStats.gamesPlayed,
totalWins: dbStats.totalWins,
totalLosses: dbStats.totalLosses,
bestTime: dbStats.bestTime,
highestAccuracy: dbStats.highestAccuracy,
favoriteGameType: dbStats.favoriteGameType,
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
lastPlayedAt: dbStats.lastPlayedAt,
createdAt: dbStats.createdAt,
updatedAt: dbStats.updatedAt,
}
}
/**
* Create default player stats for new player
*/
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
const now = new Date()
return {
playerId,
gamesPlayed: 0,
totalWins: 0,
totalLosses: 0,
bestTime: null,
highestAccuracy: 0,
favoriteGameType: null,
gameStats: {},
lastPlayedAt: null,
createdAt: now,
updatedAt: now,
}
}
/**
* Determine most-played game from game stats
*/
function getMostPlayedGame(gameStats: Record<string, GameStatsBreakdown>): string | null {
const games = Object.entries(gameStats)
if (games.length === 0) return null
return games.reduce((mostPlayed, [gameType, stats]) => {
const mostPlayedStats = gameStats[mostPlayed]
return stats.gamesPlayed > (mostPlayedStats?.gamesPlayed || 0) ? gameType : mostPlayed
}, games[0][0])
}

View File

@@ -1,105 +0,0 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
import { playerStats } from '@/db/schema/player-stats'
import { players } from '@/db/schema/players'
import type { GetAllPlayerStatsResponse, PlayerStatsData } from '@/lib/arcade/stats/types'
import { getViewerId } from '@/lib/viewer'
// Force dynamic rendering - this route uses headers()
export const dynamic = 'force-dynamic'
/**
* GET /api/player-stats
*
* Fetches stats for all of the current user's players.
*/
export async function GET() {
try {
// 1. Authenticate user
const viewerId = await getViewerId()
if (!viewerId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Fetch all user's players
const userPlayers = await db.select().from(players).where(eq(players.userId, viewerId))
const playerIds = userPlayers.map((p) => p.id)
// 3. Fetch stats for all players
const allStats: PlayerStatsData[] = []
for (const playerId of playerIds) {
const stats = await db
.select()
.from(playerStats)
.where(eq(playerStats.playerId, playerId))
.limit(1)
.then((rows) => rows[0])
if (stats) {
allStats.push(convertToPlayerStatsData(stats))
} else {
// Player exists but has no stats yet
allStats.push(createDefaultPlayerStats(playerId))
}
}
// 4. Return response
const response: GetAllPlayerStatsResponse = {
playerStats: allStats,
}
return NextResponse.json(response)
} catch (error) {
console.error('❌ Failed to fetch player stats:', error)
return NextResponse.json(
{
error: 'Failed to fetch player stats',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}
/**
* Convert DB record to PlayerStatsData
*/
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
return {
playerId: dbStats.playerId,
gamesPlayed: dbStats.gamesPlayed,
totalWins: dbStats.totalWins,
totalLosses: dbStats.totalLosses,
bestTime: dbStats.bestTime,
highestAccuracy: dbStats.highestAccuracy,
favoriteGameType: dbStats.favoriteGameType,
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
lastPlayedAt: dbStats.lastPlayedAt,
createdAt: dbStats.createdAt,
updatedAt: dbStats.updatedAt,
}
}
/**
* Create default player stats for new player
*/
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
const now = new Date()
return {
playerId,
gamesPlayed: 0,
totalWins: 0,
totalLosses: 0,
bestTime: null,
highestAccuracy: 0,
favoriteGameType: null,
gameStats: {},
lastPlayedAt: null,
createdAt: now,
updatedAt: now,
}
}

View File

@@ -1,161 +0,0 @@
import { eq, and } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { db, schema } from '@/db'
import { getViewerId } from '@/lib/viewer'
import {
parseAdditionConfig,
serializeAdditionConfig,
defaultAdditionConfig,
type AdditionConfigV1,
} from '@/app/create/worksheets/config-schemas'
/**
* GET /api/worksheets/settings?type=addition
* Load user's saved worksheet settings
*
* Query params:
* - type: 'addition' | 'subtraction' | etc.
*
* Returns:
* - config: Parsed and validated config (latest version)
* - exists: boolean (true if user has saved settings)
*/
export async function GET(req: NextRequest) {
try {
const viewerId = await getViewerId()
const { searchParams } = new URL(req.url)
const worksheetType = searchParams.get('type')
if (!worksheetType) {
return NextResponse.json({ error: 'Missing type parameter' }, { status: 400 })
}
// Only 'addition' is supported for now
if (worksheetType !== 'addition') {
return NextResponse.json(
{ error: `Unsupported worksheet type: ${worksheetType}` },
{ status: 400 }
)
}
// Look up user's saved settings
const [row] = await db
.select()
.from(schema.worksheetSettings)
.where(
and(
eq(schema.worksheetSettings.userId, viewerId),
eq(schema.worksheetSettings.worksheetType, worksheetType)
)
)
.limit(1)
if (!row) {
// No saved settings, return defaults
return NextResponse.json({
config: defaultAdditionConfig,
exists: false,
})
}
// Parse and validate config (auto-migrates to latest version)
const config = parseAdditionConfig(row.config)
return NextResponse.json({
config,
exists: true,
})
} catch (error: any) {
console.error('Failed to load worksheet settings:', error)
return NextResponse.json({ error: 'Failed to load worksheet settings' }, { status: 500 })
}
}
/**
* POST /api/worksheets/settings
* Save user's worksheet settings
*
* Body:
* - type: 'addition' | 'subtraction' | etc.
* - config: Config object (version will be added automatically)
*
* Returns:
* - success: boolean
* - id: string (worksheet_settings row id)
*/
export async function POST(req: NextRequest) {
try {
const viewerId = await getViewerId()
const body = await req.json()
const { type: worksheetType, config } = body
if (!worksheetType) {
return NextResponse.json({ error: 'Missing type field' }, { status: 400 })
}
if (!config) {
return NextResponse.json({ error: 'Missing config field' }, { status: 400 })
}
// Only 'addition' is supported for now
if (worksheetType !== 'addition') {
return NextResponse.json(
{ error: `Unsupported worksheet type: ${worksheetType}` },
{ status: 400 }
)
}
// Serialize config (adds version automatically)
const configJson = serializeAdditionConfig(config)
// Check if user already has settings for this type
const [existing] = await db
.select()
.from(schema.worksheetSettings)
.where(
and(
eq(schema.worksheetSettings.userId, viewerId),
eq(schema.worksheetSettings.worksheetType, worksheetType)
)
)
.limit(1)
const now = new Date()
if (existing) {
// Update existing row
await db
.update(schema.worksheetSettings)
.set({
config: configJson,
updatedAt: now,
})
.where(eq(schema.worksheetSettings.id, existing.id))
return NextResponse.json({
success: true,
id: existing.id,
})
} else {
// Insert new row
const id = crypto.randomUUID()
await db.insert(schema.worksheetSettings).values({
id,
userId: viewerId,
worksheetType,
config: configJson,
createdAt: now,
updatedAt: now,
})
return NextResponse.json({
success: true,
id,
})
}
} catch (error: any) {
console.error('Failed to save worksheet settings:', error)
return NextResponse.json({ error: 'Failed to save worksheet settings' }, { status: 500 })
}
}

View File

@@ -23,11 +23,13 @@ export function AbacusTarget({ number }: AbacusTargetProps) {
<AbacusReact
value={number}
columns={1}
compact={true}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.72}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
</div>
)

View File

@@ -2,16 +2,12 @@
import { animated, useSpring } from '@react-spring/web'
import { AbacusReact } from '@soroban/abacus-react'
import { useAbacusSettings } from '@/hooks/useAbacusSettings'
interface PressureGaugeProps {
pressure: number // 0-150 PSI
}
export function PressureGauge({ pressure }: PressureGaugeProps) {
// Get native abacus numbers setting
const { data: abacusSettings } = useAbacusSettings()
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
const maxPressure = 150
// Animate pressure value smoothly with spring physics
@@ -111,29 +107,17 @@ export function PressureGauge({ pressure }: PressureGaugeProps) {
lineHeight: 0,
}}
>
{useNativeAbacusNumbers ? (
<AbacusReact
value={psi}
columns={3}
interactive={false}
showNumbers={false}
hideInactiveBeads={false}
scaleFactor={0.6}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
) : (
<div
style={{
fontSize: '14px',
fontWeight: 'bold',
color: '#1f2937',
}}
>
{psi}
</div>
)}
<AbacusReact
value={psi}
columns={3}
interactive={false}
showNumbers={false}
hideInactiveBeads={false}
scaleFactor={0.6}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
</div>
</foreignObject>
</g>
@@ -158,7 +142,7 @@ export function PressureGauge({ pressure }: PressureGaugeProps) {
/>
</svg>
{/* Pressure readout */}
{/* Abacus readout */}
<div
style={{
textAlign: 'center',
@@ -169,35 +153,27 @@ export function PressureGauge({ pressure }: PressureGaugeProps) {
minHeight: '32px',
}}
>
{useNativeAbacusNumbers ? (
<>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 0,
}}
>
<AbacusReact
value={Math.round(pressure)}
columns={3}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.35}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
</div>
<span style={{ fontSize: '12px', color: '#6b7280', fontWeight: 'bold' }}>PSI</span>
</>
) : (
<div style={{ fontSize: '20px', fontWeight: 'bold', color: '#1f2937' }}>
{Math.round(pressure)} <span style={{ fontSize: '12px', color: '#6b7280' }}>PSI</span>
</div>
)}
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 0,
}}
>
<AbacusReact
value={Math.round(pressure)}
columns={3}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.35}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
</div>
<span style={{ fontSize: '12px', color: '#6b7280', fontWeight: 'bold' }}>PSI</span>
</div>
</div>
)

View File

@@ -1,233 +0,0 @@
/**
* Test to reproduce delivery thrashing bug
*
* The bug: When a car is at a station for multiple frames (50ms intervals),
* the delivery logic fires repeatedly before the optimistic state update propagates.
* This causes multiple DELIVER_PASSENGER moves to be sent to the server,
* which rejects all but the first one.
*/
import { describe, expect, test } from 'vitest'
interface Passenger {
id: string
name: string
claimedBy: string | null
deliveredBy: string | null
carIndex: number | null
destinationStationId: string
isUrgent: boolean
}
interface Station {
id: string
name: string
emoji: string
position: number
}
describe('useSteamJourney - Delivery Thrashing Reproduction', () => {
const CAR_SPACING = 7
/**
* Simulate the delivery logic from useSteamJourney
* Returns the number of delivery attempts made
*/
function simulateDeliveryAtPosition(
trainPosition: number,
passengers: Passenger[],
stations: Station[],
pendingDeliveryRef: Set<string>
): { deliveryAttempts: number; deliveredPassengerIds: string[] } {
let deliveryAttempts = 0
const deliveredPassengerIds: string[] = []
const currentBoardedPassengers = passengers.filter(
(p) => p.claimedBy !== null && p.deliveredBy === null
)
// PRIORITY 1: Process deliveries FIRST (dispatch DELIVER moves before BOARD moves)
currentBoardedPassengers.forEach((passenger) => {
if (!passenger || passenger.deliveredBy !== null || passenger.carIndex === null) return
// Skip if already has a pending delivery request
if (pendingDeliveryRef.has(passenger.id)) return
const station = stations.find((s) => s.id === passenger.destinationStationId)
if (!station) return
// Calculate this passenger's car position using PHYSICAL carIndex
const carPosition = Math.max(0, trainPosition - (passenger.carIndex + 1) * CAR_SPACING)
const distance = Math.abs(carPosition - station.position)
// If this car is at the destination station (within 5% tolerance), deliver
if (distance < 5) {
// Mark as pending BEFORE dispatch to prevent duplicate delivery attempts across frames
pendingDeliveryRef.add(passenger.id)
deliveryAttempts++
deliveredPassengerIds.push(passenger.id)
}
})
return { deliveryAttempts, deliveredPassengerIds }
}
test('WITHOUT fix: multiple frames at same position cause thrashing', () => {
const stations: Station[] = [
{ id: 's1', name: 'Start', emoji: '🏠', position: 20 },
{ id: 's2', name: 'Middle', emoji: '🏢', position: 40 },
{ id: 's3', name: 'End', emoji: '🏁', position: 80 },
]
// Passenger "Bob" is in Car 1, heading to station s2 at position 40
const passengers: Passenger[] = [
{
id: 'bob',
name: 'Bob',
claimedBy: 'player1',
deliveredBy: null, // Not yet delivered
carIndex: 1, // In car 1 (second car)
destinationStationId: 's2',
isUrgent: false,
},
]
// NO pending delivery tracking (simulating the bug)
const noPendingRef = new Set<string>()
// Train position where Car 1 is at station s2 (position 40)
// Car 1 position = trainPosition - (carIndex + 1) * CAR_SPACING
// Car 1 position = trainPosition - 2 * 7 = trainPosition - 14
// For Car 1 to be at position 40: trainPosition = 40 + 14 = 54
const trainPosition = 53.9
const carPosition = Math.max(0, trainPosition - (1 + 1) * CAR_SPACING)
console.log(`Train at ${trainPosition}, Car 1 at ${carPosition}, Station at 40`)
expect(Math.abs(carPosition - 40)).toBeLessThan(5) // Verify we're in delivery range
let totalAttempts = 0
// Simulate 10 frames (50ms each = 500ms total) at the same position
// This mimics what happens when the train is near/at a station
for (let frame = 0; frame < 10; frame++) {
const result = simulateDeliveryAtPosition(trainPosition, passengers, stations, noPendingRef)
totalAttempts += result.deliveryAttempts
// WITHOUT the pendingDeliveryRef fix, every frame triggers a delivery attempt
// because the optimistic update hasn't propagated yet
}
// Without the fix, we expect 10 delivery attempts (one per frame)
// because nothing prevents duplicate attempts
console.log(`Total delivery attempts without fix: ${totalAttempts}`)
expect(totalAttempts).toBe(10) // This demonstrates the bug!
})
test('WITH fix: pendingDeliveryRef prevents thrashing', () => {
const stations: Station[] = [
{ id: 's1', name: 'Start', emoji: '🏠', position: 20 },
{ id: 's2', name: 'Middle', emoji: '🏢', position: 40 },
{ id: 's3', name: 'End', emoji: '🏁', position: 80 },
]
const passengers: Passenger[] = [
{
id: 'bob',
name: 'Bob',
claimedBy: 'player1',
deliveredBy: null,
carIndex: 1,
destinationStationId: 's2',
isUrgent: false,
},
]
// WITH pending delivery tracking (the fix)
const pendingDeliveryRef = new Set<string>()
const trainPosition = 53.9
let totalAttempts = 0
// Simulate 10 frames at the same position
for (let frame = 0; frame < 10; frame++) {
const result = simulateDeliveryAtPosition(
trainPosition,
passengers,
stations,
pendingDeliveryRef
)
totalAttempts += result.deliveryAttempts
}
// With the fix, only the FIRST frame should attempt delivery
// All subsequent frames skip because passenger.id is in pendingDeliveryRef
console.log(`Total delivery attempts with fix: ${totalAttempts}`)
expect(totalAttempts).toBe(1) // Only one attempt! ✅
})
test('EDGE CASE: multiple passengers at same station', () => {
const stations: Station[] = [
{ id: 's1', name: 'Start', emoji: '🏠', position: 20 },
{ id: 's2', name: 'Middle', emoji: '🏢', position: 40 },
{ id: 's3', name: 'End', emoji: '🏁', position: 80 },
]
// Two passengers in different cars, both going to station s2
const passengers: Passenger[] = [
{
id: 'alice',
name: 'Alice',
claimedBy: 'player1',
deliveredBy: null,
carIndex: 0, // Car 0
destinationStationId: 's2',
isUrgent: false,
},
{
id: 'bob',
name: 'Bob',
claimedBy: 'player1',
deliveredBy: null,
carIndex: 1, // Car 1
destinationStationId: 's2',
isUrgent: false,
},
]
const pendingDeliveryRef = new Set<string>()
// Position where both cars are near station s2 (position 40)
// Car 0 at position 40: trainPosition = 40 + 7 = 47
// Car 1 at position 40: trainPosition = 40 + 14 = 54
// Let's use 50 so Car 0 is at 43 and Car 1 is at 36 (both within 5 of 40)
const trainPosition = 46.5
// Debug: Check car positions
const car0Pos = Math.max(0, trainPosition - (0 + 1) * CAR_SPACING)
const car1Pos = Math.max(0, trainPosition - (1 + 1) * CAR_SPACING)
console.log(
`Train at ${trainPosition}, Car 0 at ${car0Pos} (dist ${Math.abs(car0Pos - 40)}), Car 1 at ${car1Pos} (dist ${Math.abs(car1Pos - 40)})`
)
let totalAttempts = 0
// Simulate 5 frames
for (let frame = 0; frame < 5; frame++) {
const result = simulateDeliveryAtPosition(
trainPosition,
passengers,
stations,
pendingDeliveryRef
)
totalAttempts += result.deliveryAttempts
if (result.deliveryAttempts > 0) {
console.log(`Frame ${frame}: Delivered ${result.deliveredPassengerIds.join(', ')}`)
}
}
// Should deliver BOTH passengers exactly once (2 total attempts)
console.log(`Total delivery attempts for 2 passengers: ${totalAttempts}`)
expect(totalAttempts).toBe(2) // Alice once, Bob once ✅
})
})

View File

@@ -1,5 +1,4 @@
import { useCallback, useContext, useRef } from 'react'
import { PreviewModeContext } from '@/components/GamePreview'
import { useCallback, useRef } from 'react'
/**
* Web Audio API sound effects system
@@ -16,7 +15,6 @@ interface Note {
export function useSoundEffects() {
const audioContextsRef = useRef<AudioContext[]>([])
const previewMode = useContext(PreviewModeContext)
/**
* Helper function to play multi-note 90s arcade sounds
@@ -109,11 +107,6 @@ export function useSoundEffects() {
| 'steam_hiss',
volume: number = 0.15
) => {
// Disable all audio in preview mode
if (previewMode?.isPreview) {
return
}
try {
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
@@ -445,7 +438,7 @@ export function useSoundEffects() {
console.log('🎵 Web Audio not supported - missing out on rad 90s sounds!')
}
},
[play90sSound, previewMode]
[play90sSound]
)
/**

View File

@@ -34,7 +34,7 @@ const MOMENTUM_DECAY_RATES = {
const MOMENTUM_GAIN_PER_CORRECT = 15 // Momentum added for each correct answer
const SPEED_MULTIPLIER = 0.15 // Convert momentum to speed (% per second at momentum=100)
const UPDATE_INTERVAL = 50 // Update every 50ms (~20 fps)
const UPDATE_INTERVAL = 16 // Update every 16ms (~60 fps for smooth animation)
const GAME_DURATION = 60000 // 60 seconds in milliseconds
export function useSteamJourney() {
@@ -45,7 +45,6 @@ export function useSteamJourney() {
const routeExitThresholdRef = useRef<number>(107) // Default for 1 car: 100 + 7
const missedPassengersRef = useRef<Set<string>>(new Set()) // Track which passengers have been logged as missed
const pendingBoardingRef = useRef<Set<string>>(new Set()) // Track passengers with pending boarding requests across frames
const pendingDeliveryRef = useRef<Set<string>>(new Set()) // Track passengers with pending delivery requests across frames
const previousTrainPositionRef = useRef<number>(0) // Track previous position to detect threshold crossings
// Initialize game start time
@@ -66,10 +65,9 @@ export function useSteamJourney() {
}
}, [state.currentRoute, state.passengers, state.stations, state.maxConcurrentPassengers])
// Clean up pendingBoardingRef when passengers are claimed/delivered
// NOTE: We do NOT clean up pendingDeliveryRef here because delivery should only happen once per route
// Clean up pendingBoardingRef when passengers are claimed/delivered or route changes
useEffect(() => {
// Remove passengers from pending boarding set if they've been claimed or delivered
// Remove passengers from pending set if they've been claimed or delivered
state.passengers.forEach((passenger) => {
if (passenger.claimedBy !== null || passenger.deliveredBy !== null) {
pendingBoardingRef.current.delete(passenger.id)
@@ -77,10 +75,9 @@ export function useSteamJourney() {
})
}, [state.passengers])
// Clear all pending boarding and delivery requests when route changes
// Clear all pending boarding requests when route changes
useEffect(() => {
pendingBoardingRef.current.clear()
pendingDeliveryRef.current.clear()
missedPassengersRef.current.clear()
previousTrainPositionRef.current = 0 // Reset previous position for new route
}, [state.currentRoute])
@@ -162,9 +159,6 @@ export function useSteamJourney() {
currentBoardedPassengers.forEach((passenger) => {
if (!passenger || passenger.deliveredBy !== null || passenger.carIndex === null) return
// Skip if already has a pending delivery request
if (pendingDeliveryRef.current.has(passenger.id)) return
const station = state.stations.find((s) => s.id === passenger.destinationStationId)
if (!station) return
@@ -178,10 +172,6 @@ export function useSteamJourney() {
console.log(
`🎯 DELIVERY: ${passenger.name} delivered from Car ${passenger.carIndex} to ${station.emoji} ${station.name} (+${points} pts) (trainPos=${trainPosition.toFixed(1)}, carPos=${carPosition.toFixed(1)}, stationPos=${station.position})`
)
// Mark as pending BEFORE dispatch to prevent duplicate delivery attempts across frames
pendingDeliveryRef.current.add(passenger.id)
dispatch({
type: 'DELIVER_PASSENGER',
passengerId: passenger.id,

View File

@@ -104,7 +104,9 @@ export function useTrackManagement({
setDisplayPassengers(passengers)
}
// Otherwise, keep displaying old passengers until train resets
}, [passengers, displayPassengers, trainPosition, currentRoute])
// Note: displayPassengers is intentionally NOT in deps to avoid infinite loop
// (it's used for comparison, but we don't need to re-run when it changes)
}, [passengers, trainPosition, currentRoute])
// Generate ties and rails when path is ready
useEffect(() => {

View File

@@ -1,8 +1,8 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState, useEffect } from 'react'
import { useRoomData, useSetRoomGame, useCreateRoom } from '@/hooks/useRoomData'
import { useState } from 'react'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
@@ -17,51 +17,20 @@ import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade regardless of selection, setup, or gameplay.
*
* Auto-creates a solo room if the user doesn't have one, ensuring they always have
* a context in which to play games.
* Note: We show a friendly message with a link if no room exists to avoid navigation loops.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*
* Test: Verifying compose-updater automatic deployment cycle
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { data: viewerId } = useViewerId()
const { mutate: setRoomGame } = useSetRoomGame()
const { mutate: createRoom, isPending: isCreatingRoom } = useCreateRoom()
const [permissionError, setPermissionError] = useState<string | null>(null)
// Auto-create room when user has no room
// This happens when:
// 1. First time visiting /arcade
// 2. After leaving a room
useEffect(() => {
if (!isLoading && !roomData && viewerId && !isCreatingRoom) {
console.log('[RoomPage] No room found, auto-creating room for user:', viewerId)
createRoom(
{
name: 'My Room',
gameName: null, // No game selected yet
gameConfig: undefined, // No game config since no game selected
accessMode: 'open' as const, // Open by default - user can change settings later
},
{
onSuccess: (newRoom) => {
console.log('[RoomPage] Successfully created room:', newRoom.id)
},
onError: (error) => {
console.error('[RoomPage] Failed to auto-create room:', error)
},
}
)
}
}, [isLoading, roomData, viewerId, isCreatingRoom, createRoom])
// Show loading state (includes both initial load and room creation)
if (isLoading || isCreatingRoom) {
// Show loading state
if (isLoading) {
return (
<div
style={{
@@ -73,13 +42,12 @@ export default function RoomPage() {
color: '#666',
}}
>
{isCreatingRoom ? 'Creating solo room...' : 'Loading room...'}
Loading room...
</div>
)
}
// If still no room after loading and creation attempt, show fallback
// This should rarely happen (only if auto-creation fails)
// Show error if no room (instead of redirecting)
if (!roomData) {
return (
<div
@@ -94,8 +62,16 @@ export default function RoomPage() {
gap: '1rem',
}}
>
<div>Unable to create room</div>
<div style={{ fontSize: '14px', color: '#999' }}>Please try refreshing the page</div>
<div>No active room found</div>
<a
href="/arcade"
style={{
color: '#3b82f6',
textDecoration: 'underline',
}}
>
Go to Champion Arena
</a>
</div>
)
}

View File

@@ -1,22 +0,0 @@
'use client'
import { useState } from 'react'
import { PlayingGuideModal } from '@/arcade-games/rithmomachia/components/PlayingGuideModal'
export default function RithmomachiaGuidePage() {
// Guide is always open in this standalone page
const [isOpen] = useState(true)
return (
<div
style={{
width: '100vw',
height: '100vh',
overflow: 'hidden',
background: '#f3f4f6',
}}
>
<PlayingGuideModal isOpen={isOpen} onClose={() => window.close()} standalone={true} />
</div>
)
}

View File

@@ -1,16 +0,0 @@
'use client'
import { rithmomachiaGame } from '@/arcade-games/rithmomachia'
// Force dynamic rendering to avoid build-time initialization errors
export const dynamic = 'force-dynamic'
const { Provider, GameComponent } = rithmomachiaGame
export default function RithmomachiaPage() {
return (
<Provider>
<GameComponent />
</Provider>
)
}

View File

@@ -1,365 +0,0 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { getPostBySlug, getAllPostSlugs } from '@/lib/blog'
import { css } from '../../../../styled-system/css'
interface Props {
params: {
slug: string
}
}
// Generate static params for all blog posts
export async function generateStaticParams() {
const slugs = getAllPostSlugs()
return slugs.map((slug) => ({ slug }))
}
// Generate metadata for SEO
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://abaci.one'
const postUrl = `${siteUrl}/blog/${params.slug}`
return {
title: `${post.title} | Abaci.one Blog`,
description: post.description,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.description,
url: postUrl,
siteName: 'Abaci.one',
type: 'article',
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author],
tags: post.tags,
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.description,
},
alternates: {
canonical: postUrl,
},
}
}
export default async function BlogPost({ params }: Props) {
let post
try {
post = await getPostBySlug(params.slug)
} catch {
notFound()
}
// Format date for display
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
const updatedDate = new Date(post.updatedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
const showUpdatedDate = post.publishedAt !== post.updatedAt
return (
<div
data-component="blog-post-page"
className={css({
minH: '100vh',
bg: 'gray.900',
pt: 'var(--app-nav-height-full)',
})}
>
{/* Background pattern */}
<div
className={css({
position: 'fixed',
inset: 0,
opacity: 0.05,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
pointerEvents: 'none',
zIndex: 0,
})}
/>
<div
className={css({
position: 'relative',
zIndex: 1,
maxW: '48rem',
mx: 'auto',
px: { base: '1rem', md: '2rem' },
py: { base: '2rem', md: '4rem' },
})}
>
{/* Back link */}
<Link
href="/blog"
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
mb: '2rem',
color: 'rgba(196, 181, 253, 0.8)',
fontSize: '0.875rem',
fontWeight: '500',
textDecoration: 'none',
transition: 'color 0.2s',
_hover: {
color: 'rgba(196, 181, 253, 1)',
},
})}
>
<span></span>
<span>Back to Blog</span>
</Link>
{/* Article */}
<article data-element="blog-article">
<header
data-section="article-header"
className={css({
mb: '3rem',
pb: '2rem',
borderBottom: '1px solid',
borderColor: 'rgba(75, 85, 99, 0.5)',
})}
>
<h1
className={css({
fontSize: { base: '2rem', md: '2.5rem', lg: '3rem' },
fontWeight: 'bold',
lineHeight: '1.2',
mb: '1rem',
color: 'white',
})}
>
{post.title}
</h1>
<p
className={css({
fontSize: { base: '1.125rem', md: '1.25rem' },
color: 'rgba(209, 213, 219, 0.8)',
lineHeight: '1.6',
mb: '1.5rem',
})}
>
{post.description}
</p>
<div
data-element="article-meta"
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '1rem',
alignItems: 'center',
fontSize: '0.875rem',
color: 'rgba(196, 181, 253, 0.8)',
})}
>
<span data-element="author">{post.author}</span>
<span></span>
<time dateTime={post.publishedAt}>{publishedDate}</time>
{showUpdatedDate && (
<>
<span></span>
<span>Updated: {updatedDate}</span>
</>
)}
</div>
{post.tags.length > 0 && (
<div
data-element="tags"
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
mt: '1rem',
})}
>
{post.tags.map((tag) => (
<span
key={tag}
className={css({
px: '0.75rem',
py: '0.25rem',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'rgba(196, 181, 253, 1)',
borderRadius: '0.25rem',
fontSize: '0.875rem',
fontWeight: '500',
})}
>
{tag}
</span>
))}
</div>
)}
</header>
{/* Article Content */}
<div
data-section="article-content"
className={css({
fontSize: { base: '1rem', md: '1.125rem' },
lineHeight: '1.75',
color: 'rgba(229, 231, 235, 0.95)',
// Typography styles for markdown content
'& h1': {
fontSize: { base: '1.875rem', md: '2.25rem' },
fontWeight: 'bold',
mt: '2.5rem',
mb: '1rem',
lineHeight: '1.25',
color: 'white',
},
'& h2': {
fontSize: { base: '1.5rem', md: '1.875rem' },
fontWeight: 'bold',
mt: '2rem',
mb: '0.875rem',
lineHeight: '1.3',
color: 'rgba(196, 181, 253, 1)',
},
'& h3': {
fontSize: { base: '1.25rem', md: '1.5rem' },
fontWeight: '600',
mt: '1.75rem',
mb: '0.75rem',
lineHeight: '1.4',
color: 'rgba(196, 181, 253, 0.9)',
},
'& p': {
mb: '1.25rem',
},
'& strong': {
fontWeight: '600',
color: 'white',
},
'& a': {
color: 'rgba(147, 197, 253, 1)',
textDecoration: 'underline',
_hover: {
color: 'rgba(59, 130, 246, 1)',
},
},
'& ul, & ol': {
pl: '1.5rem',
mb: '1.25rem',
},
'& li': {
mb: '0.5rem',
},
'& code': {
bg: 'rgba(0, 0, 0, 0.4)',
px: '0.375rem',
py: '0.125rem',
borderRadius: '0.25rem',
fontSize: '0.875em',
fontFamily: 'monospace',
color: 'rgba(196, 181, 253, 1)',
border: '1px solid',
borderColor: 'rgba(139, 92, 246, 0.3)',
},
'& pre': {
bg: 'rgba(0, 0, 0, 0.5)',
border: '1px solid',
borderColor: 'rgba(139, 92, 246, 0.3)',
color: 'rgba(229, 231, 235, 0.95)',
p: '1rem',
borderRadius: '0.5rem',
overflow: 'auto',
mb: '1.25rem',
},
'& pre code': {
bg: 'transparent',
p: '0',
border: 'none',
color: 'inherit',
fontSize: '0.875rem',
},
'& blockquote': {
borderLeft: '4px solid',
borderColor: 'rgba(139, 92, 246, 0.5)',
pl: '1rem',
py: '0.5rem',
my: '1.5rem',
color: 'rgba(209, 213, 219, 0.8)',
fontStyle: 'italic',
bg: 'rgba(139, 92, 246, 0.05)',
borderRadius: '0 0.25rem 0.25rem 0',
},
'& hr': {
my: '2rem',
borderColor: 'rgba(75, 85, 99, 0.5)',
},
'& table': {
width: '100%',
mb: '1.25rem',
borderCollapse: 'collapse',
},
'& th': {
bg: 'rgba(139, 92, 246, 0.2)',
px: '1rem',
py: '0.75rem',
textAlign: 'left',
fontWeight: '600',
borderBottom: '2px solid',
borderColor: 'rgba(139, 92, 246, 0.5)',
color: 'rgba(196, 181, 253, 1)',
},
'& td': {
px: '1rem',
py: '0.75rem',
borderBottom: '1px solid',
borderColor: 'rgba(75, 85, 99, 0.3)',
color: 'rgba(209, 213, 219, 0.9)',
},
'& tr:hover td': {
bg: 'rgba(139, 92, 246, 0.05)',
},
})}
dangerouslySetInnerHTML={{ __html: post.html }}
/>
</article>
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.description,
author: {
'@type': 'Person',
name: post.author,
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
keywords: post.tags.join(', '),
}),
}}
/>
</div>
</div>
)
}

View File

@@ -1,334 +0,0 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { getAllPostsMetadata, getFeaturedPosts } from '@/lib/blog'
import { css } from '../../../styled-system/css'
export const metadata: Metadata = {
title: 'Blog | Abaci.one',
description:
'Articles about educational technology, pedagogy, and innovative approaches to learning with the abacus.',
openGraph: {
title: 'Abaci.one Blog',
description:
'Articles about educational technology, pedagogy, and innovative approaches to learning with the abacus.',
url: `${process.env.NEXT_PUBLIC_SITE_URL || 'https://abaci.one'}/blog`,
siteName: 'Abaci.one',
type: 'website',
},
}
export default async function BlogIndex() {
const featuredPosts = await getFeaturedPosts()
const allPosts = await getAllPostsMetadata()
return (
<div
data-component="blog-index-page"
className={css({
minH: '100vh',
bg: 'gray.900',
pt: 'var(--app-nav-height-full)',
})}
>
{/* Background pattern */}
<div
className={css({
position: 'fixed',
inset: 0,
opacity: 0.05,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
pointerEvents: 'none',
zIndex: 0,
})}
/>
<div
className={css({
position: 'relative',
zIndex: 1,
maxW: '64rem',
mx: 'auto',
px: { base: '1rem', md: '2rem' },
py: { base: '2rem', md: '4rem' },
})}
>
{/* Page Header */}
<header
data-section="page-header"
className={css({
mb: '3rem',
textAlign: 'center',
})}
>
<h1
className={css({
fontSize: { base: '2.5rem', md: '3.5rem' },
fontWeight: 'bold',
mb: '1rem',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Blog
</h1>
<p
className={css({
fontSize: { base: '1.125rem', md: '1.25rem' },
color: 'rgba(209, 213, 219, 0.8)',
maxW: '42rem',
mx: 'auto',
})}
>
Exploring educational technology, pedagogy, and innovative approaches to learning.
</p>
</header>
{/* Featured Posts */}
{featuredPosts.length > 0 && (
<section
data-section="featured-posts"
className={css({
mb: '4rem',
})}
>
<h2
className={css({
fontSize: { base: '1.5rem', md: '1.875rem' },
fontWeight: 'bold',
mb: '1.5rem',
color: 'rgba(196, 181, 253, 1)',
})}
>
Featured
</h2>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(auto-fit, minmax(300px, 1fr))' },
gap: '1.5rem',
})}
>
{featuredPosts.map((post) => {
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
data-action="view-featured-post"
className={css({
display: 'block',
p: '1.5rem',
bg: 'rgba(139, 92, 246, 0.1)',
backdropFilter: 'blur(10px)',
borderRadius: '0.75rem',
border: '1px solid',
borderColor: 'rgba(139, 92, 246, 0.3)',
transition: 'all 0.3s',
_hover: {
bg: 'rgba(139, 92, 246, 0.15)',
borderColor: 'rgba(139, 92, 246, 0.5)',
transform: 'translateY(-4px)',
boxShadow: '0 8px 24px rgba(139, 92, 246, 0.2)',
},
})}
>
<h3
className={css({
fontSize: { base: '1.25rem', md: '1.5rem' },
fontWeight: '600',
mb: '0.5rem',
color: 'white',
})}
>
{post.title}
</h3>
<p
className={css({
color: 'rgba(209, 213, 219, 0.8)',
mb: '1rem',
lineHeight: '1.6',
})}
>
{post.excerpt || post.description}
</p>
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.75rem',
alignItems: 'center',
fontSize: '0.875rem',
color: 'rgba(196, 181, 253, 0.8)',
})}
>
<span>{post.author}</span>
<span></span>
<time dateTime={post.publishedAt}>{publishedDate}</time>
</div>
{post.tags.length > 0 && (
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
mt: '1rem',
})}
>
{post.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className={css({
px: '0.5rem',
py: '0.125rem',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'rgba(196, 181, 253, 1)',
borderRadius: '0.25rem',
fontSize: '0.75rem',
fontWeight: '500',
})}
>
{tag}
</span>
))}
</div>
)}
</Link>
)
})}
</div>
</section>
)}
{/* All Posts */}
<section data-section="all-posts">
<h2
className={css({
fontSize: { base: '1.5rem', md: '1.875rem' },
fontWeight: 'bold',
mb: '1.5rem',
color: 'rgba(196, 181, 253, 1)',
})}
>
All Posts
</h2>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '2rem',
})}
>
{allPosts.map((post) => {
const publishedDate = new Date(post.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<article
key={post.slug}
data-element="post-preview"
className={css({
pb: '2rem',
borderBottom: '1px solid',
borderColor: 'rgba(75, 85, 99, 0.5)',
_last: {
borderBottom: 'none',
},
})}
>
<Link
href={`/blog/${post.slug}`}
data-action="view-post"
className={css({
display: 'block',
_hover: {
'& h3': {
color: 'rgba(196, 181, 253, 1)',
},
},
})}
>
<h3
className={css({
fontSize: { base: '1.5rem', md: '1.875rem' },
fontWeight: '600',
mb: '0.5rem',
color: 'white',
transition: 'color 0.2s',
})}
>
{post.title}
</h3>
</Link>
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.75rem',
alignItems: 'center',
fontSize: '0.875rem',
color: 'rgba(196, 181, 253, 0.7)',
mb: '1rem',
})}
>
<span>{post.author}</span>
<span></span>
<time dateTime={post.publishedAt}>{publishedDate}</time>
</div>
<p
className={css({
color: 'rgba(209, 213, 219, 0.8)',
lineHeight: '1.6',
mb: '1rem',
})}
>
{post.excerpt || post.description}
</p>
{post.tags.length > 0 && (
<div
className={css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
})}
>
{post.tags.map((tag) => (
<span
key={tag}
className={css({
px: '0.5rem',
py: '0.125rem',
bg: 'rgba(75, 85, 99, 0.5)',
color: 'rgba(209, 213, 219, 0.8)',
borderRadius: '0.25rem',
fontSize: '0.75rem',
fontWeight: '500',
})}
>
{tag}
</span>
))}
</div>
)}
</article>
)
})}
</div>
</section>
</div>
</div>
)
}

View File

@@ -1,321 +0,0 @@
'use client'
import { useTranslations } from 'next-intl'
import { css } from '../../../../../styled-system/css'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { AbacusDisplayDropdown } from '@/components/AbacusDisplayDropdown'
interface CalendarConfigPanelProps {
month: number
year: number
format: 'monthly' | 'daily'
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
isGenerating: boolean
onMonthChange: (month: number) => void
onYearChange: (year: number) => void
onFormatChange: (format: 'monthly' | 'daily') => void
onPaperSizeChange: (size: 'us-letter' | 'a4' | 'a3' | 'tabloid') => void
onGenerate: () => void
}
export function CalendarConfigPanel({
month,
year,
format,
paperSize,
isGenerating,
onMonthChange,
onYearChange,
onFormatChange,
onPaperSizeChange,
onGenerate,
}: CalendarConfigPanelProps) {
const t = useTranslations('calendar')
const abacusConfig = useAbacusConfig()
const MONTHS = [
t('months.january'),
t('months.february'),
t('months.march'),
t('months.april'),
t('months.may'),
t('months.june'),
t('months.july'),
t('months.august'),
t('months.september'),
t('months.october'),
t('months.november'),
t('months.december'),
]
return (
<div
data-component="calendar-config-panel"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '1.5rem',
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
})}
>
{/* Format Selection */}
<fieldset
data-section="format-selection"
className={css({
border: 'none',
padding: '0',
margin: '0',
})}
>
<legend
className={css({
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.75rem',
color: 'yellow.400',
})}
>
{t('format.title')}
</legend>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
})}
>
<label
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
cursor: 'pointer',
padding: '0.5rem',
borderRadius: '6px',
_hover: { bg: 'gray.700' },
})}
>
<input
type="radio"
value="monthly"
checked={format === 'monthly'}
onChange={(e) => onFormatChange(e.target.value as 'monthly' | 'daily')}
className={css({
cursor: 'pointer',
})}
/>
<span>{t('format.monthly')}</span>
</label>
<label
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
cursor: 'pointer',
padding: '0.5rem',
borderRadius: '6px',
_hover: { bg: 'gray.700' },
})}
>
<input
type="radio"
value="daily"
checked={format === 'daily'}
onChange={(e) => onFormatChange(e.target.value as 'monthly' | 'daily')}
className={css({
cursor: 'pointer',
})}
/>
<span>{t('format.daily')}</span>
</label>
</div>
</fieldset>
{/* Date Selection */}
<fieldset
data-section="date-selection"
className={css({
border: 'none',
padding: '0',
margin: '0',
})}
>
<legend
className={css({
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.75rem',
color: 'yellow.400',
})}
>
{t('date.title')}
</legend>
<div
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
<select
data-element="month-select"
value={month}
onChange={(e) => onMonthChange(Number(e.target.value))}
className={css({
flex: '1',
padding: '0.5rem',
borderRadius: '6px',
bg: 'gray.700',
color: 'white',
border: '1px solid',
borderColor: 'gray.600',
cursor: 'pointer',
_hover: { borderColor: 'gray.500' },
})}
>
{MONTHS.map((monthName, index) => (
<option key={monthName} value={index + 1}>
{monthName}
</option>
))}
</select>
<input
type="number"
data-element="year-input"
value={year}
onChange={(e) => onYearChange(Number(e.target.value))}
min={1}
max={9999}
className={css({
width: '100px',
padding: '0.5rem',
borderRadius: '6px',
bg: 'gray.700',
color: 'white',
border: '1px solid',
borderColor: 'gray.600',
_hover: { borderColor: 'gray.500' },
})}
/>
</div>
</fieldset>
{/* Paper Size */}
<fieldset
data-section="paper-size"
className={css({
border: 'none',
padding: '0',
margin: '0',
})}
>
<legend
className={css({
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.75rem',
color: 'yellow.400',
})}
>
{t('paperSize.title')}
</legend>
<select
data-element="paper-size-select"
value={paperSize}
onChange={(e) =>
onPaperSizeChange(e.target.value as 'us-letter' | 'a4' | 'a3' | 'tabloid')
}
className={css({
width: '100%',
padding: '0.5rem',
borderRadius: '6px',
bg: 'gray.700',
color: 'white',
border: '1px solid',
borderColor: 'gray.600',
cursor: 'pointer',
_hover: { borderColor: 'gray.500' },
})}
>
<option value="us-letter">{t('paperSize.usLetter')}</option>
<option value="a4">{t('paperSize.a4')}</option>
<option value="a3">{t('paperSize.a3')}</option>
<option value="tabloid">{t('paperSize.tabloid')}</option>
</select>
</fieldset>
{/* Abacus Styling */}
<div
data-section="styling-info"
className={css({
padding: '1rem',
bg: 'gray.700',
borderRadius: '8px',
})}
>
<p
className={css({
fontSize: '0.875rem',
marginBottom: '0.75rem',
color: 'gray.300',
})}
>
{t('styling.preview')}
</p>
<div
className={css({
display: 'flex',
justifyContent: 'center',
marginBottom: '0.75rem',
})}
>
<AbacusReact
value={12}
columns={2}
customStyles={abacusConfig.customStyles}
scaleFactor={0.5}
showNumbers={false}
/>
</div>
<div
className={css({
display: 'flex',
justifyContent: 'center',
})}
>
<AbacusDisplayDropdown />
</div>
</div>
{/* Generate Button */}
<button
type="button"
data-action="generate-calendar"
onClick={onGenerate}
disabled={isGenerating}
className={css({
padding: '1rem',
bg: 'yellow.500',
color: 'gray.900',
fontWeight: '600',
fontSize: '1.125rem',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
bg: 'yellow.400',
},
_disabled: {
bg: 'gray.600',
color: 'gray.400',
cursor: 'not-allowed',
},
})}
>
{isGenerating ? t('generate.generating') : t('generate.button')}
</button>
</div>
)
}

View File

@@ -1,114 +0,0 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useTranslations } from 'next-intl'
import { css } from '../../../../../styled-system/css'
interface CalendarPreviewProps {
month: number
year: number
format: 'monthly' | 'daily'
previewSvg: string | null
}
async function fetchTypstPreview(
month: number,
year: number,
format: string
): Promise<string | null> {
const response = await fetch('/api/create/calendar/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ month, year, format }),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.error || errorData.message || 'Failed to fetch preview')
}
const data = await response.json()
return data.svg
}
export function CalendarPreview({ month, year, format, previewSvg }: CalendarPreviewProps) {
const t = useTranslations('calendar')
// Use React Query to fetch Typst-generated preview (client-side only)
const { data: typstPreviewSvg, isLoading } = useQuery({
queryKey: ['calendar-typst-preview', month, year, format],
queryFn: () => fetchTypstPreview(month, year, format),
enabled: typeof window !== 'undefined', // Run on client for both formats
})
// Use generated PDF SVG if available, otherwise use Typst live preview
const displaySvg = previewSvg || typstPreviewSvg
// Show loading state while fetching preview
if (isLoading || !displaySvg) {
return (
<div
data-component="calendar-preview"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '600px',
})}
>
<p
className={css({
fontSize: '1.25rem',
color: 'gray.400',
textAlign: 'center',
})}
>
{isLoading ? t('preview.loading') : t('preview.noPreview')}
</p>
</div>
)
}
return (
<div
data-component="calendar-preview"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
})}
>
<p
className={css({
fontSize: '1.125rem',
color: 'yellow.400',
marginBottom: '1rem',
textAlign: 'center',
fontWeight: 'bold',
})}
>
{previewSvg
? t('preview.generatedPdf')
: format === 'daily'
? t('preview.livePreviewFirstDay')
: t('preview.livePreview')}
</p>
<div
className={css({
bg: 'white',
borderRadius: '8px',
padding: '1rem',
maxWidth: '100%',
overflow: 'auto',
})}
dangerouslySetInnerHTML={{ __html: displaySvg }}
/>
</div>
)
}

View File

@@ -1,154 +0,0 @@
'use client'
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { css } from '../../../../styled-system/css'
import { useAbacusConfig } from '@soroban/abacus-react'
import { PageWithNav } from '@/components/PageWithNav'
import { CalendarConfigPanel } from './components/CalendarConfigPanel'
import { CalendarPreview } from './components/CalendarPreview'
export default function CalendarCreatorPage() {
const t = useTranslations('calendar')
const currentDate = new Date()
const abacusConfig = useAbacusConfig()
const [month, setMonth] = useState(currentDate.getMonth() + 1) // 1-12
const [year, setYear] = useState(currentDate.getFullYear())
const [format, setFormat] = useState<'monthly' | 'daily'>('monthly')
const [paperSize, setPaperSize] = useState<'us-letter' | 'a4' | 'a3' | 'tabloid'>('us-letter')
const [isGenerating, setIsGenerating] = useState(false)
const [previewSvg, setPreviewSvg] = useState<string | null>(null)
// Detect default paper size based on user's locale (client-side only)
useEffect(() => {
// Get user's locale
const locale = navigator.language || navigator.languages?.[0] || 'en-US'
const country = locale.split('-')[1]?.toUpperCase()
// Countries that use US Letter (8.5" × 11")
const letterCountries = ['US', 'CA', 'MX', 'GT', 'PA', 'DO', 'PR', 'PH']
const detectedSize = letterCountries.includes(country || '') ? 'us-letter' : 'a4'
setPaperSize(detectedSize)
}, [])
const handleGenerate = async () => {
setIsGenerating(true)
try {
const response = await fetch('/api/create/calendar/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
month,
year,
format,
paperSize,
abacusConfig,
}),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.message || 'Failed to generate calendar')
}
const data = await response.json()
// Convert base64 PDF to blob and trigger download
const pdfBytes = Uint8Array.from(atob(data.pdf), (c) => c.charCodeAt(0))
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = data.filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Error generating calendar:', error)
alert(
`Failed to generate calendar: ${error instanceof Error ? error.message : 'Unknown error'}`
)
} finally {
setIsGenerating(false)
}
}
return (
<PageWithNav navTitle="Create" navEmoji="📅">
<div
data-component="calendar-creator"
className={`with-fixed-nav ${css({
minHeight: '100vh',
bg: 'gray.900',
color: 'white',
padding: '2rem',
})}`}
>
<div
className={css({
maxWidth: '1400px',
margin: '0 auto',
})}
>
{/* Header */}
<header
data-section="page-header"
className={css({
textAlign: 'center',
marginBottom: '3rem',
})}
>
<h1
className={css({
fontSize: '2.5rem',
fontWeight: 'bold',
marginBottom: '0.5rem',
color: 'yellow.400',
})}
>
{t('pageTitle')}
</h1>
<p
className={css({
fontSize: '1.125rem',
color: 'gray.300',
})}
>
{t('pageSubtitle')}
</p>
</header>
{/* Main Content */}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', lg: '350px 1fr' },
gap: '2rem',
})}
>
{/* Configuration Panel */}
<CalendarConfigPanel
month={month}
year={year}
format={format}
paperSize={paperSize}
isGenerating={isGenerating}
onMonthChange={setMonth}
onYearChange={setYear}
onFormatChange={setFormat}
onPaperSizeChange={setPaperSize}
onGenerate={handleGenerate}
/>
{/* Preview */}
<CalendarPreview month={month} year={year} format={format} previewSvg={previewSvg} />
</div>
</div>
</div>
</PageWithNav>
)
}

View File

@@ -1,413 +0,0 @@
'use client'
import { useAbacusConfig } from '@soroban/abacus-react'
import { useForm } from '@tanstack/react-form'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationFormWithoutGenerate'
import { GenerationProgress } from '@/components/GenerationProgress'
import { FlashcardPreview } from '@/components/FlashcardPreview'
import { PageWithNav } from '@/components/PageWithNav'
import { StyleControls } from '@/components/StyleControls'
import { css } from '../../../../styled-system/css'
import { container, grid, hstack, stack } from '../../../../styled-system/patterns'
// Complete, validated configuration ready for generation
export interface FlashcardConfig {
range: string
step?: number
cardsPerPage?: number
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
orientation?: 'portrait' | 'landscape'
margins?: {
top?: string
bottom?: string
left?: string
right?: string
}
gutter?: string
shuffle?: boolean
seed?: number
showCutMarks?: boolean
showRegistration?: boolean
fontFamily?: string
fontSize?: string
columns?: string | number
showEmptyColumns?: boolean
hideInactiveBeads?: boolean
beadShape?: 'diamond' | 'circle' | 'square'
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
coloredNumerals?: boolean
scaleFactor?: number
format?: 'pdf' | 'html' | 'png' | 'svg'
}
// Partial form state during editing (may have undefined values)
export interface FlashcardFormState {
range?: string
step?: number
cardsPerPage?: number
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
orientation?: 'portrait' | 'landscape'
margins?: {
top?: string
bottom?: string
left?: string
right?: string
}
gutter?: string
shuffle?: boolean
seed?: number
showCutMarks?: boolean
showRegistration?: boolean
fontFamily?: string
fontSize?: string
columns?: string | number
showEmptyColumns?: boolean
hideInactiveBeads?: boolean
beadShape?: 'diamond' | 'circle' | 'square'
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
coloredNumerals?: boolean
scaleFactor?: number
format?: 'pdf' | 'html' | 'png' | 'svg'
}
// Validation function to convert form state to complete config
function validateAndCompleteConfig(formState: FlashcardFormState): FlashcardConfig {
return {
// Required fields with defaults
range: formState.range || '0-99',
// Optional fields with defaults
step: formState.step ?? 1,
cardsPerPage: formState.cardsPerPage ?? 6,
paperSize: formState.paperSize ?? 'us-letter',
orientation: formState.orientation ?? 'portrait',
gutter: formState.gutter ?? '5mm',
shuffle: formState.shuffle ?? false,
seed: formState.seed,
showCutMarks: formState.showCutMarks ?? false,
showRegistration: formState.showRegistration ?? false,
fontFamily: formState.fontFamily ?? 'DejaVu Sans',
fontSize: formState.fontSize ?? '48pt',
columns: formState.columns ?? 'auto',
showEmptyColumns: formState.showEmptyColumns ?? false,
hideInactiveBeads: formState.hideInactiveBeads ?? false,
beadShape: formState.beadShape ?? 'diamond',
colorScheme: formState.colorScheme ?? 'place-value',
coloredNumerals: formState.coloredNumerals ?? false,
scaleFactor: formState.scaleFactor ?? 0.9,
format: formState.format ?? 'pdf',
margins: formState.margins,
}
}
type GenerationStatus = 'idle' | 'generating' | 'error'
export default function CreatePage() {
const t = useTranslations('create.flashcards')
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
const [error, setError] = useState<string | null>(null)
const globalConfig = useAbacusConfig()
const form = useForm<FlashcardFormState>({
defaultValues: {
range: '0-99',
step: 1,
cardsPerPage: 6,
paperSize: 'us-letter',
orientation: 'portrait',
gutter: '5mm',
shuffle: false,
showCutMarks: false,
showRegistration: false,
fontFamily: 'DejaVu Sans',
fontSize: '48pt',
columns: 'auto',
showEmptyColumns: false,
// Use global config for abacus display settings
hideInactiveBeads: globalConfig.hideInactiveBeads,
beadShape: globalConfig.beadShape,
colorScheme: globalConfig.colorScheme,
coloredNumerals: globalConfig.coloredNumerals,
scaleFactor: globalConfig.scaleFactor,
format: 'pdf',
},
})
const handleGenerate = async (formState: FlashcardFormState) => {
setGenerationStatus('generating')
setError(null)
try {
// Validate and complete the configuration
const config = validateAndCompleteConfig(formState)
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
})
if (!response.ok) {
// Handle error response (should be JSON)
const errorResult = await response.json()
throw new Error(errorResult.error || 'Generation failed')
}
// Success - response is binary PDF data, trigger download
const blob = await response.blob()
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
// Create download link and trigger download
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
setGenerationStatus('idle') // Reset to idle after successful download
} catch (err) {
console.error('Generation error:', err)
setError(err instanceof Error ? err.message : 'Unknown error occurred')
setGenerationStatus('error')
}
}
const handleNewGeneration = () => {
setGenerationStatus('idle')
setError(null)
}
return (
<PageWithNav navTitle={t('navTitle')} navEmoji="✨">
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
{/* Main Content */}
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
<div className={stack({ gap: '6', mb: '8' })}>
<div className={stack({ gap: '2', textAlign: 'center' })}>
<h1
className={css({
fontSize: '3xl',
fontWeight: 'bold',
color: 'gray.900',
})}
>
{t('pageTitle')}
</h1>
<p
className={css({
fontSize: 'lg',
color: 'gray.600',
})}
>
{t('pageSubtitle')}
</p>
</div>
</div>
{/* Configuration Interface */}
<div
className={grid({
columns: { base: 1, lg: 3 },
gap: '8',
alignItems: 'start',
})}
>
{/* Main Configuration Panel */}
<div
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '8',
})}
>
<ConfigurationFormWithoutGenerate form={form} />
</div>
{/* Style Controls Panel */}
<div
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '6',
})}
>
<div className={stack({ gap: '4' })}>
<div className={stack({ gap: '1' })}>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
color: 'gray.900',
})}
>
{t('stylePanel.title')}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{t('stylePanel.subtitle')}
</p>
</div>
<form.Subscribe
selector={(state) => state}
children={(_state) => <StyleControls form={form} />}
/>
</div>
</div>
{/* Live Preview Panel */}
<div
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '6',
})}
>
<div className={stack({ gap: '6' })}>
<form.Subscribe
selector={(state) => state}
children={(state) => <FlashcardPreview config={state.values} />}
/>
{/* Generate Button within Preview */}
<div
className={css({
borderTop: '1px solid',
borderColor: 'gray.200',
pt: '6',
})}
>
{/* Generation Status */}
{generationStatus === 'generating' && (
<div className={css({ mb: '4' })}>
<GenerationProgress config={form.state.values} />
</div>
)}
<button
onClick={() => handleGenerate(form.state.values)}
disabled={generationStatus === 'generating'}
className={css({
w: 'full',
px: '6',
py: '4',
bg: 'brand.600',
color: 'white',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
transition: 'all',
cursor: generationStatus === 'generating' ? 'not-allowed' : 'pointer',
opacity: generationStatus === 'generating' ? '0.7' : '1',
_hover:
generationStatus === 'generating'
? {}
: {
bg: 'brand.700',
transform: 'translateY(-1px)',
shadow: 'modal',
},
})}
>
<span className={hstack({ gap: '3', justify: 'center' })}>
{generationStatus === 'generating' ? (
<>
<div
className={css({
w: '5',
h: '5',
border: '2px solid',
borderColor: 'white',
borderTopColor: 'transparent',
rounded: 'full',
animation: 'spin 1s linear infinite',
})}
/>
{t('generate.generating')}
</>
) : (
<>
<div className={css({ fontSize: 'xl' })}></div>
{t('generate.button')}
</>
)}
</span>
</button>
</div>
</div>
</div>
</div>
{/* Error Display - moved to global level */}
{generationStatus === 'error' && error && (
<div
className={css({
bg: 'red.50',
border: '1px solid',
borderColor: 'red.200',
rounded: '2xl',
p: '8',
mt: '8',
})}
>
<div className={stack({ gap: '4' })}>
<div className={hstack({ gap: '3', alignItems: 'center' })}>
<div className={css({ fontSize: '2xl' })}></div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'semibold',
color: 'red.800',
})}
>
{t('error.title')}
</h3>
</div>
<p
className={css({
color: 'red.700',
lineHeight: 'relaxed',
})}
>
{error}
</p>
<button
onClick={handleNewGeneration}
className={css({
alignSelf: 'start',
px: '4',
py: '2',
bg: 'red.600',
color: 'white',
fontWeight: 'medium',
rounded: 'lg',
transition: 'all',
_hover: { bg: 'red.700' },
})}
>
{t('error.tryAgain')}
</button>
</div>
</div>
)}
</div>
</div>
</PageWithNav>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,227 +0,0 @@
# Worksheet Config Schema Guide
## Type-Safe JSON Blob with Schema Versioning
This system provides type-safe storage of worksheet settings using JSON blobs with automatic schema migration.
## Key Features
1. **Runtime Type Safety**: Zod validates all configs at runtime
2. **Schema Versioning**: Each config has a `version` field for evolution
3. **Automatic Migration**: Old configs automatically upgrade to latest version
4. **Graceful Degradation**: Invalid configs fall back to sensible defaults
5. **Future-Proof**: Add new worksheet types without schema changes
## Adding a New Setting to Existing Worksheet Type
**Example**: Add `showHints` to addition worksheets
1. **Update the schema** (`config-schemas.ts`):
```typescript
export const additionConfigV2Schema = z.object({
version: z.literal(2),
// ... all V1 fields ...
showHints: z.boolean(), // NEW FIELD
})
```
2. **Create migration function**:
```typescript
function migrateAdditionV1toV2(v1: AdditionConfigV1): AdditionConfigV2 {
return {
...v1,
version: 2,
showHints: false, // Default value for new field
}
}
```
3. **Update discriminated union**:
```typescript
export const additionConfigSchema = z.discriminatedUnion('version', [
additionConfigV1Schema,
additionConfigV2Schema, // Add new version
])
```
4. **Update migration switch**:
```typescript
export function migrateAdditionConfig(rawConfig: unknown): AdditionConfigV2 {
const parsed = additionConfigSchema.safeParse(rawConfig)
if (!parsed.success) return defaultAdditionConfig
switch (parsed.data.version) {
case 1:
return migrateAdditionV1toV2(parsed.data)
case 2:
return parsed.data // Latest version
default:
return defaultAdditionConfig
}
}
```
5. **Update default**:
```typescript
export const defaultAdditionConfig: AdditionConfigV2 = {
version: 2,
// ... all fields including showHints: false
}
```
## Adding a New Worksheet Type
**Example**: Add multiplication worksheets
1. **Create schema**:
```typescript
export const multiplicationConfigV1Schema = z.object({
version: z.literal(1),
problemsPerPage: z.number().int().min(1).max(100),
showTimes Table: z.boolean(),
// ... multiplication-specific fields
})
```
2. **Create helpers**:
```typescript
export function parseMultiplicationConfig(jsonString: string): MultiplicationConfigV1 {
try {
const raw = JSON.parse(jsonString)
return multiplicationConfigSchema.parse(raw)
} catch (error) {
return defaultMultiplicationConfig
}
}
export function serializeMultiplicationConfig(config: Omit<MultiplicationConfigV1, 'version'>): string {
return JSON.stringify({ ...config, version: 1 })
}
```
3. **Use in app**: No database migration needed!
## Usage in Application Code
### Saving Settings
```typescript
import { serializeAdditionConfig } from './config-schemas'
const configJSON = serializeAdditionConfig({
problemsPerPage: 20,
cols: 5,
// ... all other fields (version added automatically)
})
await db.insert(worksheetSettings).values({
id: crypto.randomUUID(),
userId: user.id,
worksheetType: 'addition',
config: configJSON,
createdAt: new Date(),
updatedAt: new Date(),
})
```
### Loading Settings
```typescript
import { parseAdditionConfig } from './config-schemas'
const row = await db
.select()
.from(worksheetSettings)
.where(eq(worksheetSettings.userId, userId))
.where(eq(worksheetSettings.worksheetType, 'addition'))
.limit(1)
const config = row ? parseAdditionConfig(row.config) : defaultAdditionConfig
// config is now type-safe and guaranteed to be latest version!
```
## Migration Examples
### Scenario 1: User has V1 config, app is on V2
```json
// Stored in DB (V1):
{
"version": 1,
"problemsPerPage": 20,
"cols": 5,
...
}
// Automatically migrated to V2 when loaded:
{
"version": 2,
"problemsPerPage": 20,
"cols": 5,
"showHints": false, // <-- Added with default
...
}
```
### Scenario 2: Invalid/corrupted config
```typescript
// Stored in DB (corrupted):
"{invalid json{{"
// Falls back to defaults:
parseAdditionConfig("{invalid json{{")
// Returns: defaultAdditionConfig
```
### Scenario 3: Future version (app downgrade)
```json
// Stored in DB (V3, unknown to current app):
{
"version": 3,
"someNewField": "value",
...
}
// Falls back to defaults (can't parse unknown version):
// Returns: defaultAdditionConfig
```
## Best Practices
1. **Never remove fields in new versions** - only add (backwards compatible)
2. **Always provide defaults** in migration functions
3. **Test migrations** with real V1 data before deploying V2
4. **Document breaking changes** if absolutely necessary
5. **Keep CURRENT_VERSION constant** in sync with latest schema
## Type Safety Benefits
```typescript
// TypeScript catches missing fields at compile time:
const config: AdditionConfigV1 = {
version: 1,
problemsPerPage: 20,
// ❌ Error: Missing required field 'cols'
}
// Runtime validation catches invalid values:
parseAdditionConfig('{"version": 1, "cols": 999}')
// ❌ Zod error: cols must be <= 10, falls back to defaults
// Full autocomplete in editors:
config.show // ← autocomplete suggests: showCarryBoxes, showAnswerBoxes, etc.
```
## Database Schema Evolution
**Important**: The database schema NEVER needs to change when:
- Adding new worksheet types (just store different JSON)
- Adding new fields to existing types (handled by migration)
- Changing default values (handled in defaultConfig)
**Only needs migration when**:
- Adding indexes for performance
- Changing primary key structure
- Adding completely new columns to worksheet_settings table itself

View File

@@ -1,507 +0,0 @@
# Publication Plan: Constrained 2D Pedagogical Difficulty System
**Status**: Planning Stage
**Created**: November 2025
**Last Updated**: November 2025
## Related Files
- **Implementation**: [difficultyProfiles.ts](./difficultyProfiles.ts) - Core constraint system
- **UI**: [ConfigPanel.tsx](./components/ConfigPanel.tsx) - Split button interface + debug graph
- **Specification**: [SMART_DIFFICULTY_SPEC.md](./SMART_DIFFICULTY_SPEC.md) - Complete technical spec
- **Verification**: [scripts/traceDifficultyPath.ts](../../../../../scripts/traceDifficultyPath.ts) - Path visualization
- **Live Demo**: https://abaci.one/create/worksheets/addition
## The Innovation
### What We Built
A **constrained 2D pedagogical space** for addition worksheet difficulty that treats difficulty as two independent-but-constrained dimensions:
1. **Challenge Axis** (Regrouping): Problem complexity (0-18 levels)
2. **Support Axis** (Scaffolding): Visual aids and guidance (0-12 levels)
3. **Constraint Band**: Diagonal zone of valid (Challenge, Support) pairs
**Key Insight**: Higher challenge requires lower support (and vice versa), encoding pedagogical principles directly into the difficulty space.
### Why This Matters
**Problem with traditional 1D difficulty**:
- Conflates problem complexity with instructional support
- Can't differentiate "ready for harder problems but still needs visual aids" from "struggling with current level"
- Forces teachers into one-size-fits-all progression
**Our 2D approach enables**:
- Dimension-specific adjustments (challenge-only, support-only, or both)
- Pedagogically-valid combinations only (no "hard problems + max scaffolding" or "easy problems + zero support")
- Precise differentiation for individual student needs
### Theoretical Foundation
- **Zone of Proximal Development** (Vygotsky): Constraint band represents learnable space
- **Cognitive Load Theory** (Sweller): Balance intrinsic load (challenge) vs extraneous load (from poor scaffolding)
- **Scaffolding Fading** (Wood, Bruner, Ross): Support should decrease as mastery develops
## Publication Venues (Ranked by Fit)
### 1. ACM Learning @ Scale (L@S) - **BEST FIT**
**Why**: Novel systems for scalable personalized learning
**Format**:
- Full paper: 10 pages
- Work-in-Progress: 4 pages (easier entry point)
**Timeline**:
- Conference: June annually
- Submission: ~January
- Reviews: March
- Camera-ready: April
**What they want**:
- Novel educational technology systems
- Learning theory grounding
- Evidence of impact (can be preliminary for WiP)
- Scalability considerations
**URL**: https://learningatscale.acm.org/
**Strategy**: Submit WiP paper January 2026, full paper 2027 (with evaluation data)
### 2. International Journal of Artificial Intelligence in Education (IJAIED)
**Why**: AI-driven adaptive learning systems
**Format**: Full article (25-40 pages typical)
**Timeline**: Rolling submissions, 3-6 month review
**URL**: https://link.springer.com/journal/40593
**What they want**:
- Computational/algorithmic contributions
- Strong theoretical framework
- Empirical validation required
**Strategy**: Target after teacher study (2026-2027)
### 3. Learning Analytics & Knowledge (LAK) Conference
**Why**: Data-driven educational design
**Format**: Full paper (8-10 pages) or short (4 pages)
**Timeline**: Annual (March), submission ~October
**URL**: https://www.solaresearch.org/events/lak/
**What they want**:
- Use of learning analytics in design
- Evidence from usage data
- Insights from student/teacher behavior
**Strategy**: After collecting usage logs and learning outcome data
### 4. Journal of Educational Technology & Society (ETS)
**Why**: Educational technology innovations
**Format**: ~20 pages, open access
**Timeline**: Rolling submissions
**URL**: https://www.j-ets.net/
**Strategy**: Backup venue if conference submissions don't work
## What We Have vs. What We Need
### ✅ Already Have
1. **Working Implementation**
- Core constraint system ([difficultyProfiles.ts](./difficultyProfiles.ts))
- Teacher-facing UI with split buttons ([ConfigPanel.tsx](./components/ConfigPanel.tsx))
- Debug tools (clickable graph, trace script)
- Live at https://abaci.one/create/worksheets/addition
2. **Technical Documentation**
- Complete specification ([SMART_DIFFICULTY_SPEC.md](./SMART_DIFFICULTY_SPEC.md))
- Algorithm descriptions
- Architecture rationale
3. **Theoretical Framework**
- ZPD mapping
- Cognitive load theory connections
- Scaffolding fading principles
### ⏳ Need for Publications
#### For Work-in-Progress Paper (4 pages, Jan 2026):
1. **Design Rationale** (1-2 pages)
- Why 2D vs 1D?
- How did we derive the constraint band?
- What design alternatives did we consider?
2. **Related Work** (1 page)
- Intelligent Tutoring Systems (ALEKS, ASSISTments, etc.)
- Khan Academy's mastery learning
- Adaptive difficulty systems
- How is our approach different/better?
3. **Usage Scenarios** (0.5 pages)
- Example teacher workflows
- Screenshots showing the interface
- How teachers would use dimension-specific adjustments
4. **Preliminary Evaluation** (0.5 pages)
- Your own testing
- Initial teacher feedback (if we can get some)
- Identified limitations
#### For Full Research Paper (10 pages, 2027):
1. **Teacher Study** (Required)
- 10-15 teachers using the system
- Interview data: How did they use it? Was 2D helpful?
- Usage logs: Which modes did they use? Navigation patterns?
- Comparison group: Teachers using 1D slider version
2. **Student Learning Outcomes** (Ideal)
- 40-60 students
- Pre/post assessments
- Compare: 2D constrained vs 1D slider vs fixed difficulty
- Track learning trajectories over 6-8 weeks
3. **Quantitative Analysis**
- Statistical significance of learning gains
- Teacher satisfaction surveys
- Student engagement metrics
## Publication Paths (3 Options)
### Path 1: Quick Impact (6 months) - **RECOMMENDED TO START**
**Timeline**:
- **Now - Dec 2025**: Write blog post + gather initial feedback
- **Dec 2025 - Jan 2026**: Write 4-page WiP paper
- **Jan 2026**: Submit to ACM L@S WiP track
- **Mar 2026**: Reviews back
- **Jun 2026**: Present at L@S (if accepted)
**Deliverables**:
1. Blog post explaining the system (for teachers/educators)
2. 4-page WiP paper (academic audience)
3. Presentation at L@S
**Effort**: ~40 hours writing + travel to conference
**Outcome**:
- Get idea into academic discourse
- Receive feedback from learning science researchers
- Build credibility for follow-up work
### Path 2: Full Research Study (12-18 months)
**Timeline**:
- **Nov 2025 - Jan 2026**: IRB approval (if university-affiliated)
- **Jan - Mar 2026**: Recruit teachers (10-15)
- **Mar - May 2026**: Teacher study
- Give access to system
- Weekly check-ins
- Usage log collection
- End-of-study interviews
- **Jun - Aug 2026**: Analysis + paper writing
- **Sep 2026**: Submit to IJAIED or LAK 2027
- **2027**: Publication
**Deliverables**:
1. IRB protocol + approval
2. Teacher recruitment materials
3. Interview protocol
4. Usage log analysis pipeline
5. 25-40 page research paper
**Effort**: ~200-300 hours + IRB overhead
**Outcome**:
- Peer-reviewed empirical research paper
- Strong evidence for effectiveness claims
- Foundation for future grant proposals
### Path 3: Open Source + Community (Immediate) - **ALSO RECOMMENDED**
**Timeline**:
- **This week**: Write comprehensive blog post
- **Ongoing**: Share on HN, Teacher Twitter, EdTech Reddit
- **Ongoing**: Respond to feedback, track usage
**Deliverables**:
1. Blog post (~2000 words)
- Problem statement
- System design
- How to use it
- Theoretical grounding
2. Social media campaign
3. Outreach to homeschool/teacher communities
**Effort**: ~20 hours initial + ongoing engagement
**Outcome**:
- Organic user base
- Real-world feedback
- Potential citations/adoption
- Informal peer review
## Recommended Strategy
**Do Paths 1 + 3 in parallel**:
1. **This Week** (Path 3):
- Write blog post explaining the system
- Share widely to get feedback
- Start tracking usage/interest
2. **December 2025** (Path 1):
- Draft 4-page WiP paper
- Include preliminary feedback from blog responses
- Submit to L@S in January
3. **Spring 2026** (Path 1):
- Present at L@S (if accepted)
- Get feedback from researchers
- Build network in learning sciences
4. **Summer 2026** (Evaluate):
- If system gains users → Path 2 (research study)
- If limited adoption → Iterate on design
- If strong conference feedback → Target full paper
## How to Execute: WiP Paper (January 2026)
### Paper Structure (4 pages)
**1. Introduction (0.75 pages)**
- Problem: 1D difficulty conflates challenge and support
- Our solution: Constrained 2D space
- Contribution: Novel UI paradigm + theoretical framework
**2. Related Work (0.75 pages)**
- Intelligent Tutoring Systems (ALEKS, Carnegie Learning)
- Adaptive learning platforms (Khan Academy, Duolingo)
- Difficulty calibration research (IRT, Elo rating)
- Gap: No systems separate challenge from scaffolding
**3. System Design (1.5 pages)**
- Hybrid discrete/continuous architecture
- Constraint band derivation
- Movement modes (both/challenge/support)
- Split button UI design
- Screenshot of interface
**4. Theoretical Framework (0.5 pages)**
- ZPD mapping to constraint band
- Cognitive load theory justification
- Scaffolding fading principles
**5. Preliminary Evaluation (0.3 pages)**
- Your testing experience
- Initial teacher feedback (if available)
- Identified use cases
**6. Discussion & Future Work (0.2 pages)**
- Planned teacher study
- Potential for other domains
- Limitations and next steps
### Writing Timeline
**Week 1 (Dec 2-8)**:
- Draft sections 1-2 (intro + related work)
- Literature search for related systems
**Week 2 (Dec 9-15)**:
- Draft section 3 (system design)
- Create figures/screenshots
**Week 3 (Dec 16-22)**:
- Draft sections 4-6
- Get feedback from educator friends
**Week 4 (Dec 23-Jan 5)**:
- Revise based on feedback
- Polish writing
- Format for L@S template
**Jan 6-10, 2026**:
- Final read-through
- Submit to L@S WiP track
## How to Execute: Blog Post (This Week)
### Blog Structure (~2000 words)
**Title**: "Beyond Easy and Hard: A 2D Approach to Worksheet Difficulty"
**1. The Problem** (400 words)
- Teachers need to differentiate instruction
- Current tools: "easy/medium/hard" or 1-5 sliders
- Real teaching scenario: Student ready for harder problems but still needs visual aids
- Can't express this with 1D slider
**2. Our Solution** (600 words)
- Two dimensions: Challenge (problem complexity) vs Support (scaffolding)
- Constraint band: Not all combinations are pedagogically valid
- Split button interface: Default (both) or dimension-specific
- Screenshots showing the UI
**3. Theoretical Grounding** (400 words)
- Why this maps to learning theory (ZPD, cognitive load)
- How constraints encode teaching expertise
- Connection to scaffolding fading
**4. How to Use It** (400 words)
- Walkthrough: Creating a worksheet
- Examples of when to use challenge-only vs support-only
- Clicking on the 2D graph (debug feature)
**5. Try It Yourself** (200 words)
- Link to live demo
- Open source code
- Invitation for feedback
### Distribution Channels
- **Your blog** (if you have one)
- **Medium** (cross-post for reach)
- **Hacker News** (Show HN: A 2D difficulty system for math worksheets)
- **Reddit**: r/teachers, r/homeschool, r/education
- **Twitter/X**: Thread with screenshots
- **LinkedIn** (if you're active there)
## Success Metrics
### Short-term (3 months)
- [ ] Blog post published and shared
- [ ] 50+ teachers try the system
- [ ] 5+ pieces of detailed feedback
- [ ] WiP paper submitted to L@S
### Medium-term (1 year)
- [ ] WiP paper accepted and presented
- [ ] 200+ teachers using the system
- [ ] Teacher study conducted (if pursuing Path 2)
- [ ] Full paper submitted to journal/conference
### Long-term (2-3 years)
- [ ] Peer-reviewed research publication
- [ ] System adopted by curriculum companies
- [ ] Citations from other researchers
- [ ] Follow-up studies by other groups
## Resources Needed
### For WiP Paper
- **Time**: ~40 hours writing
- **Cost**: Conference registration (~$500-800) + travel (~$1000-2000)
- **Skills**: Academic writing (you + me collaborating)
### For Teacher Study
- **Time**: ~200-300 hours over 6 months
- **Cost**: Teacher incentives ($50/teacher × 15 = $750)
- **Skills**: Qualitative research methods
- **Optional**: IRB approval (if university-affiliated)
### For Blog/Outreach
- **Time**: ~20 hours initial
- **Cost**: $0 (all free platforms)
- **Skills**: Technical writing, social media engagement
## Next Steps
**Immediate (This Week)**:
1. [ ] Draft blog post outline
2. [ ] Take screenshots of the UI in action
3. [ ] Create 2-3 usage scenarios with example teacher workflows
**December 2025**:
1. [ ] Publish blog post + share widely
2. [ ] Start WiP paper draft
3. [ ] Conduct literature review for related work
**January 2026**:
1. [ ] Complete WiP paper
2. [ ] Submit to ACM L@S
3. [ ] Evaluate user feedback from blog post
## Questions to Consider
1. **Do you have academic affiliation?**
- Needed for IRB approval (teacher study)
- Some conferences require institutional affiliation
- Can collaborate with university researchers if not
2. **What's your bandwidth?**
- WiP paper: ~10 hours/week for 4 weeks
- Teacher study: ~10-15 hours/week for 6 months
- Blog post: ~10 hours total
3. **What's your goal?**
- Academic credibility → Prioritize WiP paper
- Real-world impact → Prioritize blog + outreach
- Research career → Prioritize full study
- All of the above → Do Path 1 + 3, then evaluate
4. **Do you want to recruit teachers now?**
- Could start informal study alongside blog post
- Interview 5-10 teachers who use the system
- Include in WiP paper as preliminary findings
## Conclusion
We have a genuinely novel contribution that combines:
- **Theoretical rigor** (learning science foundations)
- **Technical innovation** (constrained 2D space + hybrid architecture)
- **Practical utility** (working system teachers can use today)
This is publishable material. The question is timeline and effort:
- **Lowest effort**: Blog post + social sharing (~20 hours)
- **Medium effort**: Blog + WiP paper (~60 hours + travel)
- **High effort**: Full research study (~300 hours over 18 months)
**My recommendation**: Start with blog + WiP paper (Paths 1 + 3). This gets the idea into academic circulation with minimal risk, while building the foundation for a larger study if the system gains traction.
Would you like help drafting the blog post or WiP paper outline?

View File

@@ -1,538 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import React, { useState, useEffect } from "react";
import { PageWithNav } from "@/components/PageWithNav";
import { css } from "../../../../../../styled-system/css";
import {
container,
grid,
hstack,
stack,
} from "../../../../../../styled-system/patterns";
import { ConfigPanel } from "./ConfigPanel";
import { WorksheetPreview } from "./WorksheetPreview";
import type { WorksheetFormState } from "../types";
import { validateWorksheetConfig } from "../validation";
type GenerationStatus = "idle" | "generating" | "error";
/**
* Get current date formatted as "Month Day, Year"
*/
function getDefaultDate(): string {
const now = new Date();
return now.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}
interface AdditionWorksheetClientProps {
initialSettings: Omit<WorksheetFormState, "date" | "rows" | "total">;
initialPreview?: string[];
}
export function AdditionWorksheetClient({
initialSettings,
initialPreview,
}: AdditionWorksheetClientProps) {
console.log("[Worksheet Client] Component render, initialSettings:", {
problemsPerPage: initialSettings.problemsPerPage,
cols: initialSettings.cols,
pages: initialSettings.pages,
seed: initialSettings.seed,
});
const t = useTranslations("create.worksheets.addition");
const [generationStatus, setGenerationStatus] =
useState<GenerationStatus>("idle");
const [error, setError] = useState<string | null>(null);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const [isSaving, setIsSaving] = useState(false);
// Calculate derived state from initial settings
// Use defaults for required fields (server should always provide these, but TypeScript needs guarantees)
const problemsPerPage = initialSettings.problemsPerPage ?? 20;
const pages = initialSettings.pages ?? 1;
const cols = initialSettings.cols ?? 5;
const rows = Math.ceil((problemsPerPage * pages) / cols);
const total = problemsPerPage * pages;
// Immediate form state (for controls - updates instantly)
const [formState, setFormState] = useState<WorksheetFormState>(() => {
const initial = {
...initialSettings,
rows,
total,
date: "", // Will be set at generation time
// seed comes from initialSettings (server-generated, stable across StrictMode remounts)
};
console.log("[Worksheet Client] Initial formState:", {
seed: initial.seed,
});
return initial;
});
// Debounced form state (for preview - updates after delay)
const [debouncedFormState, setDebouncedFormState] =
useState<WorksheetFormState>(() => {
console.log(
"[Worksheet Client] Initial debouncedFormState (same as formState)",
);
return formState;
});
// Store the previous formState to detect real changes
const prevFormStateRef = React.useRef(formState);
// Log whenever debouncedFormState changes (this triggers preview re-fetch)
useEffect(() => {
console.log(
"[Worksheet Client] debouncedFormState changed - preview will re-fetch:",
{
seed: debouncedFormState.seed,
problemsPerPage: debouncedFormState.problemsPerPage,
},
);
}, [debouncedFormState]);
// Debounce preview updates (500ms delay) - only when formState actually changes
useEffect(() => {
console.log("[Debounce Effect] Triggered");
console.log("[Debounce Effect] Current formState seed:", formState.seed);
console.log(
"[Debounce Effect] Previous formState seed:",
prevFormStateRef.current.seed,
);
// Skip if formState hasn't actually changed (handles StrictMode double-render)
if (formState === prevFormStateRef.current) {
console.log("[Debounce Effect] Skipping - formState reference unchanged");
return;
}
prevFormStateRef.current = formState;
console.log(
"[Debounce Effect] Setting timer to update debouncedFormState in 500ms",
);
const timer = setTimeout(() => {
console.log(
"[Debounce Effect] Timer fired - updating debouncedFormState",
);
setDebouncedFormState(formState);
}, 500);
return () => {
console.log("[Debounce Effect] Cleanup - clearing timer");
clearTimeout(timer);
};
}, [formState]);
// Store the previous formState for auto-save to detect real changes
const prevAutoSaveFormStateRef = React.useRef(formState);
// Auto-save settings when they change (debounced) - skip on initial mount
useEffect(() => {
// Skip auto-save if formState hasn't actually changed (handles StrictMode double-render)
if (formState === prevAutoSaveFormStateRef.current) {
console.log(
"[Worksheet Settings] Skipping auto-save - formState reference unchanged",
);
return;
}
prevAutoSaveFormStateRef.current = formState;
console.log("[Worksheet Settings] Settings changed, will save in 1s...");
const timer = setTimeout(async () => {
console.log("[Worksheet Settings] Attempting to save settings...");
setIsSaving(true);
try {
// Extract only the fields we want to persist (exclude date, seed, derived state)
const {
problemsPerPage,
cols,
pages,
orientation,
name,
pAnyStart,
pAllStart,
interpolate,
showCarryBoxes,
showAnswerBoxes,
showPlaceValueColors,
showProblemNumbers,
showCellBorder,
showTenFrames,
showTenFramesForAll,
fontSize,
} = formState;
const response = await fetch("/api/worksheets/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "addition",
config: {
problemsPerPage,
cols,
pages,
orientation,
name,
pAnyStart,
pAllStart,
interpolate,
showCarryBoxes,
showAnswerBoxes,
showPlaceValueColors,
showProblemNumbers,
showCellBorder,
showTenFrames,
showTenFramesForAll,
fontSize,
},
}),
});
if (response.ok) {
const data = await response.json();
console.log("[Worksheet Settings] Save response:", data);
if (data.success) {
console.log("[Worksheet Settings] ✓ Settings saved successfully");
setLastSaved(new Date());
} else {
console.log("[Worksheet Settings] Save skipped");
}
} else {
console.error(
"[Worksheet Settings] Save failed with status:",
response.status,
);
}
} catch (error) {
// Silently fail - settings persistence is not critical
console.error("[Worksheet Settings] Settings save error:", error);
} finally {
setIsSaving(false);
}
}, 1000); // 1 second debounce for auto-save
return () => clearTimeout(timer);
}, [formState]);
const handleFormChange = (updates: Partial<WorksheetFormState>) => {
setFormState((prev) => {
const newState = { ...prev, ...updates };
// Generate new seed when problem settings change
const affectsProblems =
updates.problemsPerPage !== undefined ||
updates.cols !== undefined ||
updates.pages !== undefined ||
updates.orientation !== undefined ||
updates.pAnyStart !== undefined ||
updates.pAllStart !== undefined ||
updates.interpolate !== undefined;
if (affectsProblems) {
newState.seed = Date.now() % 2147483647;
}
return newState;
});
};
const handleGenerate = async () => {
setGenerationStatus("generating");
setError(null);
try {
// Set current date at generation time
const configWithDate = {
...formState,
date: getDefaultDate(),
};
// Validate configuration
const validation = validateWorksheetConfig(configWithDate);
if (!validation.isValid || !validation.config) {
throw new Error(
validation.errors?.join(", ") || "Invalid configuration",
);
}
const response = await fetch("/api/create/worksheets/addition", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(configWithDate),
});
if (!response.ok) {
const errorResult = await response.json();
const errorMsg = errorResult.details
? `${errorResult.error}\n\n${errorResult.details}`
: errorResult.error || "Generation failed";
throw new Error(errorMsg);
}
// Success - response is binary PDF data, trigger download
const blob = await response.blob();
const filename = `addition-worksheet-${formState.name || "student"}-${Date.now()}.pdf`;
// Create download link and trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
setGenerationStatus("idle");
} catch (err) {
console.error("Generation error:", err);
setError(err instanceof Error ? err.message : "Unknown error occurred");
setGenerationStatus("error");
}
};
const handleNewGeneration = () => {
setGenerationStatus("idle");
setError(null);
};
return (
<PageWithNav navTitle={t("navTitle")} navEmoji="📝">
<div
data-component="addition-worksheet-page"
className={css({ minHeight: "100vh", bg: "gray.50" })}
>
{/* Main Content */}
<div className={container({ maxW: "7xl", px: "4", py: "8" })}>
<div className={stack({ gap: "6", mb: "8" })}>
<div className={stack({ gap: "2", textAlign: "center" })}>
<h1
className={css({
fontSize: "3xl",
fontWeight: "bold",
color: "gray.900",
})}
>
{t("pageTitle")}
</h1>
<p
className={css({
fontSize: "lg",
color: "gray.600",
})}
>
{t("pageSubtitle")}
</p>
</div>
</div>
{/* Configuration Interface */}
<div
className={grid({
columns: { base: 1, lg: 2 },
gap: "8",
alignItems: "start",
})}
>
{/* Configuration Panel */}
<div className={stack({ gap: "3" })}>
<div
data-section="config-panel"
className={css({
bg: "white",
rounded: "2xl",
shadow: "card",
p: "8",
})}
>
<ConfigPanel
formState={formState}
onChange={handleFormChange}
/>
</div>
{/* Settings saved indicator */}
<div
data-element="settings-status"
className={css({
fontSize: "sm",
color: "gray.600",
textAlign: "center",
py: "2",
})}
>
{isSaving ? (
<span className={css({ color: "gray.500" })}>
Saving settings...
</span>
) : lastSaved ? (
<span className={css({ color: "green.600" })}>
Settings saved at {lastSaved.toLocaleTimeString()}
</span>
) : null}
</div>
</div>
{/* Preview & Generate Panel */}
<div className={stack({ gap: "8" })}>
{/* Preview */}
<div
data-section="preview-panel"
className={css({
bg: "white",
rounded: "2xl",
shadow: "card",
p: "6",
})}
>
<WorksheetPreview
formState={debouncedFormState}
initialData={initialPreview}
/>
</div>
{/* Generate Button */}
<div
data-section="generate-panel"
className={css({
bg: "white",
rounded: "2xl",
shadow: "card",
p: "6",
})}
>
<button
data-action="generate-worksheet"
onClick={handleGenerate}
disabled={generationStatus === "generating"}
className={css({
w: "full",
px: "6",
py: "4",
bg: "brand.600",
color: "white",
fontSize: "lg",
fontWeight: "semibold",
rounded: "xl",
shadow: "card",
transition: "all",
cursor:
generationStatus === "generating"
? "not-allowed"
: "pointer",
opacity: generationStatus === "generating" ? "0.7" : "1",
_hover:
generationStatus === "generating"
? {}
: {
bg: "brand.700",
transform: "translateY(-1px)",
shadow: "modal",
},
})}
>
<span className={hstack({ gap: "3", justify: "center" })}>
{generationStatus === "generating" ? (
<>
<div
className={css({
w: "5",
h: "5",
border: "2px solid",
borderColor: "white",
borderTopColor: "transparent",
rounded: "full",
animation: "spin 1s linear infinite",
})}
/>
{t("generate.generating")}
</>
) : (
<>
<div className={css({ fontSize: "xl" })}>📝</div>
{t("generate.button")}
</>
)}
</span>
</button>
</div>
</div>
</div>
{/* Error Display */}
{generationStatus === "error" && error && (
<div
data-status="error"
className={css({
bg: "red.50",
border: "1px solid",
borderColor: "red.200",
rounded: "2xl",
p: "8",
mt: "8",
})}
>
<div className={stack({ gap: "4" })}>
<div className={hstack({ gap: "3", alignItems: "center" })}>
<div className={css({ fontSize: "2xl" })}></div>
<h3
className={css({
fontSize: "xl",
fontWeight: "semibold",
color: "red.800",
})}
>
{t("error.title")}
</h3>
</div>
<pre
className={css({
color: "red.700",
lineHeight: "relaxed",
whiteSpace: "pre-wrap",
fontFamily: "mono",
fontSize: "sm",
overflowX: "auto",
})}
>
{error}
</pre>
<button
data-action="try-again"
onClick={handleNewGeneration}
className={css({
alignSelf: "start",
px: "4",
py: "2",
bg: "red.600",
color: "white",
fontWeight: "medium",
rounded: "lg",
transition: "all",
_hover: { bg: "red.700" },
})}
>
{t("error.tryAgain")}
</button>
</div>
</div>
)}
</div>
</div>
</PageWithNav>
);
}

View File

@@ -1,284 +0,0 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { css } from '../../../../../../styled-system/css'
import type { WorksheetFormState } from '../types'
interface DisplayOptionsPreviewProps {
formState: WorksheetFormState
}
interface MathSentenceProps {
operands: number[]
operator: string
onChange: (operands: number[]) => void
labels?: string[]
}
/**
* Flexible math sentence component supporting operators with arity 1-3
* Examples:
* Arity 1 (unary): [64] with "√" → "√64"
* Arity 2 (binary): [45, 27] with "+" → "45 + 27"
* Arity 3 (ternary): [5, 10, 15] with "between" → "5 < 10 < 15"
*/
function MathSentence({ operands, operator, onChange, labels }: MathSentenceProps) {
const handleOperandChange = (index: number, value: string) => {
const numValue = Number.parseInt(value, 10)
if (!Number.isNaN(numValue) && numValue >= 0 && numValue <= 99) {
const newOperands = [...operands]
newOperands[index] = numValue
onChange(newOperands)
}
}
const renderInput = (value: number, index: number) => (
<input
key={index}
type="number"
min="0"
max="99"
value={value}
onChange={(e) => handleOperandChange(index, e.target.value)}
aria-label={labels?.[index] || `operand ${index + 1}`}
className={css({
width: '3.5em',
px: '1',
py: '0.5',
fontSize: 'sm',
fontWeight: 'medium',
textAlign: 'center',
border: '1px solid',
borderColor: 'transparent',
rounded: 'sm',
outline: 'none',
transition: 'border-color 0.2s',
_hover: {
borderColor: 'gray.300',
},
_focus: {
borderColor: 'brand.500',
ring: '1px',
ringColor: 'brand.200',
},
})}
/>
)
// Render based on arity
if (operands.length === 1) {
// Unary operator (prefix): √64 or ±5
return (
<div
data-component="math-sentence"
className={css({
display: 'flex',
alignItems: 'center',
gap: '1',
fontSize: 'sm',
fontWeight: 'medium',
})}
>
<span>{operator}</span>
{renderInput(operands[0], 0)}
</div>
)
}
if (operands.length === 2) {
// Binary operator (infix): 45 + 27
return (
<div
data-component="math-sentence"
className={css({
display: 'flex',
alignItems: 'center',
gap: '1',
fontSize: 'sm',
fontWeight: 'medium',
})}
>
{renderInput(operands[0], 0)}
<span>{operator}</span>
{renderInput(operands[1], 1)}
</div>
)
}
if (operands.length === 3) {
// Ternary operator: 5 < 10 < 15 or similar
return (
<div
data-component="math-sentence"
className={css({
display: 'flex',
alignItems: 'center',
gap: '1',
fontSize: 'sm',
fontWeight: 'medium',
})}
>
{renderInput(operands[0], 0)}
<span>{operator}</span>
{renderInput(operands[1], 1)}
<span>{operator}</span>
{renderInput(operands[2], 2)}
</div>
)
}
return null
}
async function fetchExample(options: {
showCarryBoxes: boolean
showAnswerBoxes: boolean
showPlaceValueColors: boolean
showProblemNumbers: boolean
showCellBorder: boolean
showTenFrames: boolean
showTenFramesForAll: boolean
addend1: number
addend2: number
}): Promise<string> {
const response = await fetch('/api/create/worksheets/addition/example', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...options,
fontSize: 16,
}),
})
if (!response.ok) {
throw new Error('Failed to fetch example')
}
const data = await response.json()
return data.svg
}
export function DisplayOptionsPreview({ formState }: DisplayOptionsPreviewProps) {
// Local state for operands (not debounced - we want immediate feedback)
const [operands, setOperands] = useState([45, 27])
// Debounce the display options to avoid hammering the server
const [debouncedOptions, setDebouncedOptions] = useState({
showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true,
showProblemNumbers: formState.showProblemNumbers ?? true,
showCellBorder: formState.showCellBorder ?? true,
showTenFrames: formState.showTenFrames ?? false,
showTenFramesForAll: formState.showTenFramesForAll ?? false,
addend1: operands[0],
addend2: operands[1],
})
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedOptions({
showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true,
showProblemNumbers: formState.showProblemNumbers ?? true,
showCellBorder: formState.showCellBorder ?? true,
showTenFrames: formState.showTenFrames ?? false,
showTenFramesForAll: formState.showTenFramesForAll ?? false,
addend1: operands[0],
addend2: operands[1],
})
}, 300) // 300ms debounce
return () => clearTimeout(timer)
}, [
formState.showCarryBoxes,
formState.showAnswerBoxes,
formState.showPlaceValueColors,
formState.showProblemNumbers,
formState.showCellBorder,
formState.showTenFrames,
formState.showTenFramesForAll,
operands,
])
const { data: svg, isLoading } = useQuery({
queryKey: ['display-example', debouncedOptions],
queryFn: () => fetchExample(debouncedOptions),
staleTime: 5 * 60 * 1000, // 5 minutes
})
return (
<div
data-component="display-options-preview"
className={css({
p: '3',
bg: 'white',
rounded: 'xl',
border: '2px solid',
borderColor: 'brand.200',
display: 'flex',
flexDirection: 'column',
gap: '2',
width: 'fit-content',
maxWidth: '100%',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
})}
>
<div
className={css({
fontSize: 'xs',
fontWeight: 'semibold',
color: 'gray.500',
textTransform: 'uppercase',
letterSpacing: 'wider',
})}
>
Preview
</div>
<MathSentence
operands={operands}
operator="+"
onChange={setOperands}
labels={['addend', 'addend']}
/>
</div>
{isLoading ? (
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minH: '200px',
color: 'gray.400',
fontSize: 'sm',
})}
>
Generating preview...
</div>
) : svg ? (
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minH: '200px',
'& svg': {
maxW: 'full',
h: 'auto',
},
})}
dangerouslySetInnerHTML={{ __html: svg }}
/>
) : null}
</div>
)
}

View File

@@ -1,353 +0,0 @@
'use client'
import { Suspense, useState, useEffect, useRef } from 'react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTranslations } from 'next-intl'
import { css } from '../../../../../../styled-system/css'
import { hstack, stack } from '../../../../../../styled-system/patterns'
import type { WorksheetFormState } from '../types'
interface WorksheetPreviewProps {
formState: WorksheetFormState
initialData?: string[]
}
function getDefaultDate(): string {
const now = new Date()
return now.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
}
async function fetchWorksheetPreview(formState: WorksheetFormState): Promise<string[]> {
const fetchId = Math.random().toString(36).slice(2, 9)
console.log(`[WorksheetPreview] fetchWorksheetPreview called (ID: ${fetchId})`, {
seed: formState.seed,
problemsPerPage: formState.problemsPerPage,
})
// Set current date for preview
const configWithDate = {
...formState,
date: getDefaultDate(),
}
// Use absolute URL for SSR compatibility
const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'
const url = `${baseUrl}/api/create/worksheets/addition/preview`
console.log(`[WorksheetPreview] Fetching from API (ID: ${fetchId})...`)
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(configWithDate),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const errorMsg = errorData.error || errorData.message || 'Failed to fetch preview'
const details = errorData.details ? `\n\n${errorData.details}` : ''
const errors = errorData.errors ? `\n\nErrors:\n${errorData.errors.join('\n')}` : ''
throw new Error(errorMsg + details + errors)
}
const data = await response.json()
console.log(`[WorksheetPreview] Fetch complete (ID: ${fetchId}), pages:`, data.pages.length)
return data.pages
}
function PreviewContent({ formState, initialData }: WorksheetPreviewProps) {
const t = useTranslations('create.worksheets.addition')
const [currentPage, setCurrentPage] = useState(0)
// Track if we've used the initial data (so we only use it once)
const initialDataUsed = useRef(false)
console.log('[WorksheetPreview] Rendering with formState:', {
seed: formState.seed,
problemsPerPage: formState.problemsPerPage,
hasInitialData: !!initialData,
initialDataUsed: initialDataUsed.current,
})
// Only use initialData on the very first query, not on subsequent fetches
const queryInitialData = !initialDataUsed.current && initialData ? initialData : undefined
if (queryInitialData) {
console.log('[WorksheetPreview] Using server-generated initial data')
initialDataUsed.current = true
}
// Use Suspense Query - will suspend during loading
const { data: pages } = useSuspenseQuery({
queryKey: [
'worksheet-preview',
// PRIMARY state
formState.problemsPerPage,
formState.cols,
formState.pages,
formState.orientation,
// Other settings that affect appearance
formState.name,
formState.pAnyStart,
formState.pAllStart,
formState.interpolate,
formState.showCarryBoxes,
formState.showAnswerBoxes,
formState.showPlaceValueColors,
formState.showProblemNumbers,
formState.showCellBorder,
formState.showTenFrames,
formState.showTenFramesForAll,
formState.seed, // Include seed to bust cache when problem set regenerates
// Note: fontSize, date, rows, total intentionally excluded
// (rows and total are derived from primary state)
],
queryFn: () => {
console.log('[WorksheetPreview] Fetching preview from API...')
return fetchWorksheetPreview(formState)
},
initialData: queryInitialData, // Only use on first render
})
console.log('[WorksheetPreview] Preview fetched, pages:', pages.length)
const totalPages = pages.length
// Reset to first page when preview updates
useEffect(() => {
setCurrentPage(0)
}, [pages])
return (
<div data-component="worksheet-preview" className={stack({ gap: '4' })}>
<div className={stack({ gap: '1' })}>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
color: 'gray.900',
})}
>
{t('preview.title')}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{totalPages > 1 ? `${totalPages} pages` : t('preview.subtitle')}
</p>
</div>
{/* Pagination Controls (top) */}
{totalPages > 1 && (
<div
className={hstack({
gap: '3',
justify: 'center',
align: 'center',
})}
>
<button
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
disabled={currentPage === 0}
className={css({
px: '4',
py: '2',
bg: 'brand.600',
color: 'white',
rounded: 'lg',
fontWeight: 'medium',
cursor: 'pointer',
_disabled: {
opacity: 0.5,
cursor: 'not-allowed',
},
_hover: {
bg: 'brand.700',
},
})}
>
Previous
</button>
<span
className={css({
fontSize: 'sm',
color: 'gray.700',
fontWeight: 'medium',
})}
>
Page {currentPage + 1} of {totalPages}
</span>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={currentPage === totalPages - 1}
className={css({
px: '4',
py: '2',
bg: 'brand.600',
color: 'white',
rounded: 'lg',
fontWeight: 'medium',
cursor: 'pointer',
_disabled: {
opacity: 0.5,
cursor: 'not-allowed',
},
_hover: {
bg: 'brand.700',
},
})}
>
Next
</button>
</div>
)}
{/* SVG Preview */}
<div
data-element="svg-preview"
className={css({
bg: 'white',
rounded: 'lg',
p: '4',
border: '1px solid',
borderColor: 'gray.200',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& svg': {
maxWidth: '100%',
maxHeight: '70vh',
height: 'auto',
width: 'auto',
},
})}
dangerouslySetInnerHTML={{ __html: pages[currentPage] }}
/>
{/* Pagination Controls (bottom) */}
{totalPages > 1 && (
<div
className={hstack({
gap: '3',
justify: 'center',
align: 'center',
})}
>
<button
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
disabled={currentPage === 0}
className={css({
px: '4',
py: '2',
bg: 'brand.600',
color: 'white',
rounded: 'lg',
fontWeight: 'medium',
cursor: 'pointer',
_disabled: {
opacity: 0.5,
cursor: 'not-allowed',
},
_hover: {
bg: 'brand.700',
},
})}
>
Previous
</button>
<span
className={css({
fontSize: 'sm',
color: 'gray.700',
fontWeight: 'medium',
})}
>
Page {currentPage + 1} of {totalPages}
</span>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={currentPage === totalPages - 1}
className={css({
px: '4',
py: '2',
bg: 'brand.600',
color: 'white',
rounded: 'lg',
fontWeight: 'medium',
cursor: 'pointer',
_disabled: {
opacity: 0.5,
cursor: 'not-allowed',
},
_hover: {
bg: 'brand.700',
},
})}
>
Next
</button>
</div>
)}
{/* Info about full worksheet */}
<div
className={css({
bg: 'blue.50',
border: '1px solid',
borderColor: 'blue.200',
rounded: 'lg',
p: '3',
fontSize: 'sm',
color: 'blue.800',
})}
>
<strong>Full worksheet:</strong> {formState.total} problems in a {formState.cols}×
{formState.rows} grid
{formState.interpolate && ' (progressive difficulty: easy → hard)'}
</div>
</div>
)
}
function PreviewFallback() {
console.log('[WorksheetPreview] Showing fallback (Suspense boundary)')
return (
<div
data-component="worksheet-preview-loading"
className={css({
bg: 'white',
rounded: '2xl',
p: '6',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '600px',
})}
>
<p
className={css({
fontSize: 'lg',
color: 'gray.400',
textAlign: 'center',
})}
>
Generating preview...
</p>
</div>
)
}
export function WorksheetPreview({ formState, initialData }: WorksheetPreviewProps) {
return (
<Suspense fallback={<PreviewFallback />}>
<PreviewContent formState={formState} initialData={initialData} />
</Suspense>
)
}

View File

@@ -1,91 +0,0 @@
// Shared logic for generating worksheet previews (used by both API route and SSR)
import { execSync } from 'child_process'
import { validateWorksheetConfig } from './validation'
import { generateProblems } from './problemGenerator'
import { generateTypstSource } from './typstGenerator'
import type { WorksheetFormState } from './types'
export interface PreviewResult {
success: boolean
pages?: string[]
error?: string
details?: string
}
/**
* Generate worksheet preview SVG pages
* Can be called from API routes or Server Components
*/
export function generateWorksheetPreview(config: WorksheetFormState): PreviewResult {
try {
// Validate configuration
const validation = validateWorksheetConfig(config)
if (!validation.isValid || !validation.config) {
return {
success: false,
error: 'Invalid configuration',
details: validation.errors?.join(', '),
}
}
const validatedConfig = validation.config
// Generate all problems for full preview
const problems = generateProblems(
validatedConfig.total,
validatedConfig.pAnyStart,
validatedConfig.pAllStart,
validatedConfig.interpolate,
validatedConfig.seed
)
// Generate Typst sources (one per page)
const typstSources = generateTypstSource(validatedConfig, problems)
// Compile each page source to SVG (using stdout for single-page output)
const pages: string[] = []
for (let i = 0; i < typstSources.length; i++) {
const typstSource = typstSources[i]
// Compile to SVG via stdin/stdout
try {
const svgOutput = execSync('typst compile --format svg - -', {
input: typstSource,
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024, // 10MB limit
})
pages.push(svgOutput)
} catch (error) {
console.error(`Typst compilation error (page ${i + 1}):`, error)
// Extract the actual Typst error message
const stderr =
error instanceof Error && 'stderr' in error
? String((error as any).stderr)
: 'Unknown compilation error'
return {
success: false,
error: `Failed to compile preview (page ${i + 1})`,
details: stderr,
}
}
}
return {
success: true,
pages,
}
} catch (error) {
console.error('Error generating preview:', error)
const errorMessage = error instanceof Error ? error.message : String(error)
return {
success: false,
error: 'Failed to generate preview',
details: errorMessage,
}
}
}

View File

@@ -1,95 +0,0 @@
import { eq, and } from 'drizzle-orm'
import { db, schema } from '@/db'
import { getViewerId } from '@/lib/viewer'
import { parseAdditionConfig, defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { AdditionWorksheetClient } from './components/AdditionWorksheetClient'
import type { WorksheetFormState } from './types'
import { generateWorksheetPreview } from './generatePreview'
/**
* Get current date formatted as "Month Day, Year"
*/
function getDefaultDate(): string {
const now = new Date()
return now.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
}
/**
* Load worksheet settings from database (server-side)
*/
async function loadWorksheetSettings(): Promise<
Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
> {
try {
const viewerId = await getViewerId()
// Look up user's saved settings
const [row] = await db
.select()
.from(schema.worksheetSettings)
.where(
and(
eq(schema.worksheetSettings.userId, viewerId),
eq(schema.worksheetSettings.worksheetType, 'addition')
)
)
.limit(1)
if (!row) {
// No saved settings, return defaults with a stable seed
return {
...defaultAdditionConfig,
seed: Date.now() % 2147483647,
}
}
// Parse and validate config (auto-migrates to latest version)
const config = parseAdditionConfig(row.config)
return {
...config,
seed: Date.now() % 2147483647,
}
} catch (error) {
console.error('Failed to load worksheet settings:', error)
// Return defaults on error with a stable seed
return {
...defaultAdditionConfig,
seed: Date.now() % 2147483647,
}
}
}
export default async function AdditionWorksheetPage() {
const initialSettings = await loadWorksheetSettings()
// Calculate derived state needed for preview
const rows = Math.ceil(
(initialSettings.problemsPerPage * initialSettings.pages) / initialSettings.cols
)
const total = initialSettings.problemsPerPage * initialSettings.pages
// Create full config for preview generation
const fullConfig: WorksheetFormState = {
...initialSettings,
rows,
total,
date: getDefaultDate(),
}
// Pre-generate worksheet preview on the server
console.log('[SSR] Generating worksheet preview on server...')
const previewResult = generateWorksheetPreview(fullConfig)
console.log('[SSR] Preview generation complete:', previewResult.success ? 'success' : 'failed')
// Pass settings and preview to client
return (
<AdditionWorksheetClient
initialSettings={initialSettings}
initialPreview={previewResult.success ? previewResult.pages : undefined}
/>
)
}

View File

@@ -1,184 +0,0 @@
// Problem generation logic for double-digit addition worksheets
import type { AdditionProblem, ProblemCategory } from './types'
/**
* Mulberry32 PRNG for reproducible random number generation
*/
export function createPRNG(seed: number) {
let state = seed
return function rand(): number {
let t = (state += 0x6d2b79f5)
t = Math.imul(t ^ (t >>> 15), t | 1)
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
/**
* Pick a random element from an array
*/
function pick<T>(arr: T[], rand: () => number): T {
return arr[Math.floor(rand() * arr.length)]
}
/**
* Generate random integer between min and max (inclusive)
*/
function randint(min: number, max: number, rand: () => number): number {
return Math.floor(rand() * (max - min + 1)) + min
}
/**
* Generate a random two-digit number (10-99)
*/
function twoDigit(rand: () => number): number {
const tens = randint(1, 9, rand)
const ones = randint(0, 9, rand)
return tens * 10 + ones
}
/**
* Generate a problem with NO regrouping
* (ones sum < 10 AND tens sum < 10)
*/
export function generateNonRegroup(rand: () => number): [number, number] {
for (let i = 0; i < 5000; i++) {
const a = twoDigit(rand)
const b = twoDigit(rand)
const aT = Math.floor((a % 100) / 10)
const aO = a % 10
const bT = Math.floor((b % 100) / 10)
const bO = b % 10
if (aO + bO < 10 && aT + bT < 10) {
return [a, b]
}
}
// Fallback
return [12, 34]
}
/**
* Generate a problem with regrouping in ONES only
* (ones sum >= 10 AND tens sum + carry < 10)
*/
export function generateOnesOnly(rand: () => number): [number, number] {
for (let i = 0; i < 5000; i++) {
const a = twoDigit(rand)
const b = twoDigit(rand)
const aT = Math.floor((a % 100) / 10)
const aO = a % 10
const bT = Math.floor((b % 100) / 10)
const bO = b % 10
if (aO + bO >= 10 && aT + bT + 1 < 10) {
return [a, b]
}
}
// Fallback
return [58, 31]
}
/**
* Generate a problem with regrouping in BOTH ones and tens
* (ones sum >= 10 AND tens sum + carry >= 10)
*/
export function generateBoth(rand: () => number): [number, number] {
for (let i = 0; i < 5000; i++) {
const a = twoDigit(rand)
const b = twoDigit(rand)
const aT = Math.floor((a % 100) / 10)
const aO = a % 10
const bT = Math.floor((b % 100) / 10)
const bO = b % 10
if (aO + bO >= 10 && aT + bT + 1 >= 10) {
return [a, b]
}
}
// Fallback
return [68, 47]
}
/**
* Try to add a unique problem to the list
* Returns true if added, false if duplicate
*/
function uniquePush(list: AdditionProblem[], a: number, b: number, seen: Set<string>): boolean {
const key = [Math.min(a, b), Math.max(a, b)].join('+')
if (seen.has(key) || a === b) {
return false
}
seen.add(key)
list.push({ a, b })
return true
}
/**
* Generate a complete set of problems based on difficulty parameters
*/
export function generateProblems(
total: number,
pAnyStart: number,
pAllStart: number,
interpolate: boolean,
seed: number
): AdditionProblem[] {
const rand = createPRNG(seed)
const problems: AdditionProblem[] = []
const seen = new Set<string>()
for (let i = 0; i < total; i++) {
// Calculate position from start (0) to end (1)
const frac = total <= 1 ? 0 : i / (total - 1)
// Progressive difficulty: start easy, end hard
const difficultyMultiplier = interpolate ? frac : 1.0
// Effective probabilities at this position
const pAll = Math.max(0, Math.min(1, pAllStart * difficultyMultiplier))
const pAny = Math.max(0, Math.min(1, pAnyStart * difficultyMultiplier))
const pOnesOnly = Math.max(0, pAny - pAll)
const pNon = Math.max(0, 1 - pAny)
// Sample category based on probabilities
const r = rand()
let picked: ProblemCategory
if (r < pAll) {
picked = 'both'
} else if (r < pAll + pOnesOnly) {
picked = 'onesOnly'
} else {
picked = 'non'
}
// Generate problem with retries for uniqueness
let tries = 0
let ok = false
while (tries++ < 3000 && !ok) {
let a: number, b: number
if (picked === 'both') {
;[a, b] = generateBoth(rand)
} else if (picked === 'onesOnly') {
;[a, b] = generateOnesOnly(rand)
} else {
;[a, b] = generateNonRegroup(rand)
}
ok = uniquePush(problems, a, b, seen)
// If stuck, try a different category
if (!ok && tries % 50 === 0) {
picked = pick(['both', 'onesOnly', 'non'], rand)
}
}
// Last resort: add any valid two-digit problem
if (!ok) {
const a = twoDigit(rand)
const b = twoDigit(rand)
uniquePush(problems, a, b, seen)
}
}
return problems
}

View File

@@ -1,87 +0,0 @@
// Type definitions for double-digit addition worksheet creator
import type { AdditionConfigV2 } from "../config-schemas";
/**
* Complete, validated configuration for worksheet generation
* Extends V2 config with additional derived fields needed for rendering
*
* Note: Includes V1 compatibility fields during migration period
*/
export type WorksheetConfig = AdditionConfigV2 & {
// Problem set - DERIVED state
total: number; // total = problemsPerPage * pages
rows: number; // rows = (problemsPerPage / cols) * pages
// Personalization
date: string;
seed: number;
// Layout
page: {
wIn: number;
hIn: number;
};
margins: {
left: number;
right: number;
top: number;
bottom: number;
};
// V1 compatibility: Include individual boolean flags during migration
// These will be derived from displayRules during validation
showCarryBoxes: boolean;
showAnswerBoxes: boolean;
showPlaceValueColors: boolean;
showProblemNumbers: boolean;
showCellBorder: boolean;
showTenFrames: boolean;
};
/**
* Partial form state - user may be editing, fields optional
* Based on V2 config with additional derived state
*
* Note: For backwards compatibility during migration, this type accepts either:
* - V2 displayRules (preferred)
* - V1 individual boolean flags (will be converted to displayRules)
*/
export type WorksheetFormState = Partial<Omit<AdditionConfigV2, "version">> & {
// DERIVED state (calculated from primary state)
rows?: number;
total?: number;
date?: string;
seed?: number;
// V1 compatibility: Accept individual boolean flags
// These will be converted to displayRules internally
showCarryBoxes?: boolean;
showAnswerBoxes?: boolean;
showPlaceValueColors?: boolean;
showProblemNumbers?: boolean;
showCellBorder?: boolean;
showTenFrames?: boolean;
};
/**
* A single addition problem
*/
export interface AdditionProblem {
a: number;
b: number;
}
/**
* Validation result
*/
export interface ValidationResult {
isValid: boolean;
config?: WorksheetConfig;
errors?: string[];
}
/**
* Problem category for difficulty control
*/
export type ProblemCategory = "non" | "onesOnly" | "both";

View File

@@ -1,147 +0,0 @@
// Typst document generator for addition worksheets
import type { AdditionProblem, WorksheetConfig } from './types'
import { generateTypstHelpers, generateProblemStackFunction } from './typstHelpers'
/**
* Chunk array into pages of specified size
*/
function chunkProblems(problems: AdditionProblem[], pageSize: number): AdditionProblem[][] {
const pages: AdditionProblem[][] = []
for (let i = 0; i < problems.length; i += pageSize) {
pages.push(problems.slice(i, i + pageSize))
}
return pages
}
/**
* Generate Typst source code for a single page
*/
function generatePageTypst(
config: WorksheetConfig,
pageProblems: AdditionProblem[],
problemOffset: number,
rowsPerPage: number
): string {
const problemsTypst = pageProblems.map((p) => ` (a: ${p.a}, b: ${p.b}),`).join('\n')
// Calculate actual number of rows on this page
const actualRows = Math.ceil(pageProblems.length / config.cols)
// Use smaller margins to maximize space
const margin = 0.4
const contentWidth = config.page.wIn - margin * 2
const contentHeight = config.page.hIn - margin * 2
// Calculate grid spacing based on ACTUAL rows on this page
const headerHeight = 0.35 // inches for header
const availableHeight = contentHeight - headerHeight
const problemBoxHeight = availableHeight / actualRows
const problemBoxWidth = contentWidth / config.cols
// Calculate cell size to fill the entire problem box
// Without ten-frames: 5 rows (carry, first number, second number, line, answer)
// With ten-frames: 5 rows + ten-frames row (0.8 * cellSize for square cells)
// Total with ten-frames: 5.8 rows, reduced breathing room to maximize size
const cellSize = config.showTenFrames ? problemBoxHeight / 6.0 : problemBoxHeight / 4.5
return String.raw`
// addition-worksheet-page.typ (auto-generated)
#set page(
width: ${config.page.wIn}in,
height: ${config.page.hIn}in,
margin: ${margin}in,
fill: white
)
#set text(size: ${config.fontSize}pt, font: "New Computer Modern Math")
// Single non-breakable block to ensure one page
#block(breakable: false)[
#let grid-stroke = ${config.showCellBorder ? '(thickness: 1pt, dash: "dashed", paint: gray.darken(20%))' : 'none'}
#let heavy-stroke = 0.8pt
#let show-carries = ${config.showCarryBoxes ? 'true' : 'false'}
#let show-answers = ${config.showAnswerBoxes ? 'true' : 'false'}
#let show-colors = ${config.showPlaceValueColors ? 'true' : 'false'}
#let show-numbers = ${config.showProblemNumbers ? 'true' : 'false'}
#let show-ten-frames = ${config.showTenFrames ? 'true' : 'false'}
#let show-ten-frames-for-all = ${config.showTenFramesForAll ? 'true' : 'false'}
${generateTypstHelpers(cellSize)}
${generateProblemStackFunction(cellSize)}
#let problem-box(problem, index) = {
let a = problem.a
let b = problem.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)
box(
inset: 0pt,
width: ${problemBoxWidth}in,
height: ${problemBoxHeight}in
)[
#align(center + horizon)[
#problem-stack(a, b, aT, aO, bT, bO, index)
]
]
}
#let problems = (
${problemsTypst}
)
// Compact header - name on left, date on right
#grid(
columns: (1fr, 1fr),
align: (left, right),
text(size: 0.75em, weight: "bold")[${config.name}],
text(size: 0.65em)[${config.date}]
)
#v(${headerHeight}in - 0.25in)
// Problem grid - exactly ${actualRows} rows × ${config.cols} columns
#grid(
columns: ${config.cols},
column-gutter: 0pt,
row-gutter: 0pt,
stroke: grid-stroke,
..for r in range(0, ${actualRows}) {
for c in range(0, ${config.cols}) {
let idx = r * ${config.cols} + c
if idx < problems.len() {
(problem-box(problems.at(idx), ${problemOffset} + idx),)
} else {
(box(width: ${problemBoxWidth}in, height: ${problemBoxHeight}in),)
}
}
}
)
] // End of constrained block
`
}
/**
* Generate Typst source code for the worksheet (returns array of page sources)
*/
export function generateTypstSource(
config: WorksheetConfig,
problems: AdditionProblem[]
): string[] {
// Use the problemsPerPage directly from config (primary state)
const problemsPerPage = config.problemsPerPage
const rowsPerPage = problemsPerPage / config.cols
// Chunk problems into discrete pages
const pages = chunkProblems(problems, problemsPerPage)
// Generate separate Typst source for each page
return pages.map((pageProblems, pageIndex) =>
generatePageTypst(config, pageProblems, pageIndex * problemsPerPage, rowsPerPage)
)
}

View File

@@ -1,300 +0,0 @@
// Shared Typst helper functions and components for addition worksheets
// Used by both full worksheets and compact examples
export interface DisplayOptions {
showCarryBoxes: boolean
showAnswerBoxes: boolean
showPlaceValueColors: boolean
showProblemNumbers: boolean
showCellBorder: boolean
showTenFrames: boolean
showTenFramesForAll: boolean
fontSize: number
}
/**
* Generate Typst helper functions (ten-frames, diagonal boxes, etc.)
* These are shared between full worksheets and examples
*/
export function generateTypstHelpers(cellSize: number): string {
return String.raw`
// Place value colors (light pastels)
#let color-ones = rgb(227, 242, 253) // Light blue
#let color-tens = rgb(232, 245, 233) // Light green
#let color-hundreds = rgb(255, 249, 196) // Light yellow
#let color-none = white // No color
// Ten-frame helper - stacked 2 frames vertically, sized to fit cell width
#let ten-frame-spacing = 0pt
#let ten-frame-cell-stroke = 0.4pt
#let ten-frame-cell-color = rgb(0, 0, 0, 30%)
#let ten-frame-outer-stroke = 0.8pt
#let ten-frames-stacked(cell-width, top-color, bottom-color) = {
let cell-w = cell-width / 5
let cell-h = cell-w // Square cells
stack(
dir: ttb,
spacing: ten-frame-spacing,
// Top ten-frame (carry to next place value)
box(stroke: ten-frame-outer-stroke + black, inset: 0pt)[
#grid(
columns: 5, rows: 2, gutter: 0pt, stroke: none,
..for i in range(0, 10) {
(box(width: cell-w, height: cell-h, fill: top-color, stroke: ten-frame-cell-stroke + ten-frame-cell-color)[],)
}
)
],
// Bottom ten-frame (current place value overflow)
box(stroke: ten-frame-outer-stroke + black, inset: 0pt)[
#grid(
columns: 5, rows: 2, gutter: 0pt, stroke: none,
..for i in range(0, 10) {
(box(width: cell-w, height: cell-h, fill: bottom-color, stroke: ten-frame-cell-stroke + ten-frame-cell-color)[],)
}
)
]
)
}
// Diagonal-split box for carry cells
// Shows the transition from one place value to another
// source-color: color of the place value where the carry comes FROM (right side)
// dest-color: color of the place value where the carry goes TO (left side)
#let diagonal-split-box(cell-size, source-color, dest-color) = {
box(width: cell-size, height: cell-size, stroke: 0.5pt)[
// Bottom-right triangle (source place value)
#place(
bottom + right,
polygon(
fill: source-color,
stroke: none,
(0pt, 0pt), // bottom-left corner of triangle
(cell-size, 0pt), // bottom-right corner
(cell-size, cell-size) // top-right corner
)
)
// Top-left triangle (destination place value)
#place(
top + left,
polygon(
fill: dest-color,
stroke: none,
(0pt, 0pt), // top-left corner
(cell-size, cell-size), // bottom-right corner of triangle
(0pt, cell-size) // bottom-left corner
)
)
]
}
`
}
/**
* Generate Typst function for rendering problem stack/grid
* This is the SINGLE SOURCE OF TRUTH for problem rendering layout
* Used by both full worksheets and preview examples
*/
export function generateProblemStackFunction(cellSize: number): string {
const cellSizeIn = `${cellSize}in`
const cellSizePt = cellSize * 72
return String.raw`
// Problem rendering function for addition worksheets
// Returns the stack/grid structure for rendering a single 2-digit addition problem
#let problem-stack(a, b, aT, aO, bT, bO, index-or-none) = {
stack(
dir: ttb,
spacing: 0pt,
if show-numbers and index-or-none != none {
align(top + left)[
#box(inset: (left: 0.08in, top: 0.05in))[
#text(size: ${(cellSizePt * 0.6).toFixed(1)}pt, weight: "bold", font: "New Computer Modern Math")[\##(index-or-none + 1).]
]
]
},
grid(
columns: (0.5em, ${cellSizeIn}, ${cellSizeIn}, ${cellSizeIn}),
gutter: 0pt,
[],
// Hundreds carry box: shows carry FROM tens (green) TO hundreds (yellow)
if show-carries {
if show-colors {
diagonal-split-box(${cellSizeIn}, color-tens, color-hundreds)
} else {
box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[]
}
} else { v(${cellSizeIn}) },
// Tens carry box: shows carry FROM ones (blue) TO tens (green)
if show-carries {
if show-colors {
diagonal-split-box(${cellSizeIn}, color-ones, color-tens)
} else {
box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt)[]
}
} else { v(${cellSizeIn}) },
[],
[],
[],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-tens } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(aT)]]],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-ones } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(aO)]]],
box(width: ${cellSizeIn}, height: ${cellSizeIn})[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[+]]],
[],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-tens } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(bT)]]],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: if show-colors { color-ones } else { color-none })[#align(center + horizon)[#text(size: ${(cellSizePt * 0.8).toFixed(1)}pt)[#str(bO)]]],
// Line row
[],
line(length: ${cellSizeIn}, stroke: heavy-stroke),
line(length: ${cellSizeIn}, stroke: heavy-stroke),
line(length: ${cellSizeIn}, stroke: heavy-stroke),
// Ten-frames row with overlaid line on top
..if show-ten-frames {
let carry = if (aO + bO) >= 10 { 1 } else { 0 }
let tens-regroup = (aT + bT + carry) >= 10
let ones-regroup = (aO + bO) >= 10
let needs-ten-frames = show-ten-frames-for-all or tens-regroup or ones-regroup
if needs-ten-frames {
(
[],
[], // Empty cell for hundreds column
if show-ten-frames-for-all or tens-regroup {
box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { color-hundreds } else { color-none }, if show-colors { color-tens } else { color-none })]
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
]
h(2.5pt)
} else {
v(${cellSizeIn} * 0.8)
},
if show-ten-frames-for-all or ones-regroup {
box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.8)[
#align(center + top)[#ten-frames-stacked(${cellSizeIn} * 0.90, if show-colors { color-tens } else { color-none }, if show-colors { color-ones } else { color-none })]
#place(top, line(length: ${cellSizeIn} * 0.90, stroke: heavy-stroke))
]
} else {
v(${cellSizeIn} * 0.8)
},
)
} else {
()
}
} else {
()
},
// Answer boxes
[],
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-hundreds } else { color-none })[] } else { v(${cellSizeIn}) },
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-tens } else { color-none })[] } else { v(${cellSizeIn}) },
if show-answers { box(width: ${cellSizeIn}, height: ${cellSizeIn}, stroke: 0.5pt, fill: if show-colors { color-ones } else { color-none })[] } else { v(${cellSizeIn}) },
)
)
}
`
}
/**
* DEPRECATED: Old generateProblemTypst function - use generateProblemStackFunction() instead
* This function is kept for backwards compatibility but should not be used
* Generate Typst code for rendering a single addition problem
* This is the core rendering logic shared between worksheets and examples
*/
export function generateProblemTypst(
addend1: number,
addend2: number,
cellSize: number,
options: DisplayOptions,
problemNumber?: number
): string {
const cellSizeIn = `${cellSize}in`
const cellSizePt = cellSize * 72
return String.raw`
#let a = ${addend1}
#let b = ${addend2}
#let aH = calc.floor(a / 100)
#let aT = calc.floor(calc.rem(a, 100) / 10)
#let aO = calc.rem(a, 10)
#let bH = calc.floor(b / 100)
#let bT = calc.floor(calc.rem(b, 100) / 10)
#let bO = calc.rem(b, 10)
#stack(
dir: ttb,
spacing: 0pt,
${
options.showProblemNumbers && problemNumber !== undefined
? `align(top + left)[
#box(inset: (left: 0.08in, top: 0.05in))[
#text(size: ${(cellSizePt * 0.6).toFixed(1)}pt, weight: "bold", font: "New Computer Modern Math")[\\#${problemNumber}.]
]
],`
: ''
}
grid(
columns: (0.5em, ${cellSizeIn}, ${cellSizeIn}, ${cellSizeIn}),
gutter: 0pt,
[],
// Hundreds carry box: shows carry FROM tens (green) TO hundreds (yellow)
${
options.showCarryBoxes
? options.showPlaceValueColors
? 'diagonal-split-box(' + cellSizeIn + ', color-tens, color-hundreds),'
: 'box(width: ' + cellSizeIn + ', height: ' + cellSizeIn + ', stroke: 0.5pt)[],'
: 'v(' + cellSizeIn + '),'
}
// Tens carry box: shows carry FROM ones (blue) TO tens (green)
${
options.showCarryBoxes
? options.showPlaceValueColors
? 'diagonal-split-box(' + cellSizeIn + ', color-ones, color-tens),'
: 'box(width: ' + cellSizeIn + ', height: ' + cellSizeIn + ', stroke: 0.5pt)[],'
: 'v(' + cellSizeIn + '),'
}
[],
// First addend
[],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-hundreds' : 'color-none'})[#align(center + horizon)[#if aH > 0 [#aH] else [#h(0pt)]]],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-tens' : 'color-none'})[#align(center + horizon)[#aT]],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-ones' : 'color-none'})[#align(center + horizon)[#aO]],
// Second addend with + sign
[+],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-hundreds' : 'color-none'})[#align(center + horizon)[#if bH > 0 [#bH] else [#h(0pt)]]],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-tens' : 'color-none'})[#align(center + horizon)[#bT]],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: ${options.showPlaceValueColors ? 'color-ones' : 'color-none'})[#align(center + horizon)[#bO]],
// Horizontal line
[],
box(width: ${cellSizeIn}, height: 1pt, inset: 0pt)[#line(length: 100%, stroke: 0.8pt)],
box(width: ${cellSizeIn}, height: 1pt, inset: 0pt)[#line(length: 100%, stroke: 0.8pt)],
box(width: ${cellSizeIn}, height: 1pt, inset: 0pt)[#line(length: 100%, stroke: 0.8pt)],
// Answer boxes (or blank space)
${
options.showAnswerBoxes
? `[],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: color-none, stroke: grid-stroke, inset: 0pt)[],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: color-none, stroke: grid-stroke, inset: 0pt)[],
box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: color-none, stroke: grid-stroke, inset: 0pt)[],`
: ''
}
)${
options.showTenFrames || options.showTenFramesForAll
? `,
v(4pt),
box(inset: 2pt)[
#ten-frames-stacked(${cellSizeIn}, color-ones, color-tens)
]`
: ''
}
)
`
}

View File

@@ -1,111 +0,0 @@
// Validation logic for worksheet configuration
import type { WorksheetFormState, WorksheetConfig, ValidationResult } from './types'
/**
* Get current date formatted as "Month Day, Year"
*/
function getDefaultDate(): string {
const now = new Date()
return now.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
}
/**
* Validate and create complete config from partial form state
*/
export function validateWorksheetConfig(formState: WorksheetFormState): ValidationResult {
const errors: string[] = []
// Validate total (must be positive, reasonable limit)
const total = formState.total ?? 20
if (total < 1 || total > 100) {
errors.push('Total problems must be between 1 and 100')
}
// Validate cols and auto-calculate rows
const cols = formState.cols ?? 4
if (cols < 1 || cols > 10) {
errors.push('Columns must be between 1 and 10')
}
// Auto-calculate rows to fit all problems
const rows = Math.ceil(total / cols)
// Validate probabilities (0-1 range)
const pAnyStart = formState.pAnyStart ?? 0.75
const pAllStart = formState.pAllStart ?? 0.25
if (pAnyStart < 0 || pAnyStart > 1) {
errors.push('pAnyStart must be between 0 and 1')
}
if (pAllStart < 0 || pAllStart > 1) {
errors.push('pAllStart must be between 0 and 1')
}
if (pAllStart > pAnyStart) {
errors.push('pAllStart cannot be greater than pAnyStart')
}
// Validate fontSize
const fontSize = formState.fontSize ?? 16
if (fontSize < 8 || fontSize > 32) {
errors.push('Font size must be between 8 and 32')
}
// Validate seed (must be positive integer)
const seed = formState.seed ?? Date.now() % 2147483647
if (!Number.isInteger(seed) || seed < 0) {
errors.push('Seed must be a non-negative integer')
}
if (errors.length > 0) {
return { isValid: false, errors }
}
// Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols)
const orientation = formState.orientation || (cols <= 3 ? 'portrait' : 'landscape')
// Get primary state values
const problemsPerPage = formState.problemsPerPage ?? total
const pages = formState.pages ?? 1
// Build complete config with defaults
const config: WorksheetConfig = {
// Primary state
problemsPerPage,
cols,
pages,
// Derived state
total,
rows,
// Other fields
name: formState.name?.trim() || 'Student',
date: formState.date?.trim() || getDefaultDate(),
pAnyStart,
pAllStart,
interpolate: formState.interpolate ?? true,
page: {
wIn: orientation === 'portrait' ? 8.5 : 11,
hIn: orientation === 'portrait' ? 11 : 8.5,
},
margins: {
left: 0.6,
right: 0.6,
top: 1.1,
bottom: 0.7,
},
showCarryBoxes: formState.showCarryBoxes ?? true,
showAnswerBoxes: formState.showAnswerBoxes ?? true,
showPlaceValueColors: formState.showPlaceValueColors ?? true,
showProblemNumbers: formState.showProblemNumbers ?? true,
showCellBorder: formState.showCellBorder ?? true,
showTenFrames: formState.showTenFrames ?? false,
showTenFramesForAll: formState.showTenFramesForAll ?? false,
fontSize,
seed,
}
return { isValid: true, config }
}

View File

@@ -1,154 +0,0 @@
import { z } from 'zod'
/**
* Versioned worksheet config schemas with type-safe validation and migration
*
* ADDING NEW VERSIONS:
* 1. Create new schema (e.g., additionConfigV2Schema)
* 2. Add migration function (e.g., migrateAdditionV1toV2)
* 3. Update CURRENT_VERSION constant
* 4. Add case to migrateAdditionConfig()
*
* ADDING NEW WORKSHEET TYPES:
* 1. Create schema with version field
* 2. Create migration function
* 3. Export parseXXXConfig() helper
*/
// =============================================================================
// ADDITION WORKSHEETS
// =============================================================================
/** Current schema version for addition worksheets */
const ADDITION_CURRENT_VERSION = 1
/**
* Addition worksheet config - Version 1
* Initial schema with ten-frames support
*/
export const additionConfigV1Schema = z.object({
version: z.literal(1),
problemsPerPage: z.number().int().min(1).max(100),
cols: z.number().int().min(1).max(10),
pages: z.number().int().min(1).max(20),
orientation: z.enum(['portrait', 'landscape']),
name: z.string(),
pAnyStart: z.number().min(0).max(1),
pAllStart: z.number().min(0).max(1),
interpolate: z.boolean(),
showCarryBoxes: z.boolean(),
showAnswerBoxes: z.boolean(),
showPlaceValueColors: z.boolean(),
showProblemNumbers: z.boolean(),
showCellBorder: z.boolean(),
showTenFrames: z.boolean(),
showTenFramesForAll: z.boolean(),
fontSize: z.number().int().min(8).max(32),
})
export type AdditionConfigV1 = z.infer<typeof additionConfigV1Schema>
/** Union of all addition config versions (add new versions here) */
export const additionConfigSchema = z.discriminatedUnion('version', [
additionConfigV1Schema,
// additionConfigV2Schema, // Future versions go here
])
export type AdditionConfig = z.infer<typeof additionConfigSchema>
/**
* Default addition config (always latest version)
*/
export const defaultAdditionConfig: AdditionConfigV1 = {
version: 1,
problemsPerPage: 20,
cols: 5,
pages: 1,
orientation: 'landscape',
name: '',
pAnyStart: 0.75,
pAllStart: 0.25,
interpolate: true,
showCarryBoxes: true,
showAnswerBoxes: true,
showPlaceValueColors: true,
showProblemNumbers: true,
showCellBorder: true,
showTenFrames: false,
showTenFramesForAll: false,
fontSize: 16,
}
/**
* Migrate addition config from any version to latest
* @throws {Error} if config is invalid or migration fails
*/
export function migrateAdditionConfig(rawConfig: unknown): AdditionConfigV1 {
// First, try to parse as any known version
const parsed = additionConfigSchema.safeParse(rawConfig)
if (!parsed.success) {
// If parsing fails completely, return defaults
console.warn('Failed to parse addition config, using defaults:', parsed.error)
return defaultAdditionConfig
}
const config = parsed.data
// Migrate to latest version
switch (config.version) {
case 1:
// Already latest version
return config
// Future migrations:
// case 2:
// return migrateAdditionV2toV3(config)
default:
// Unknown version, return defaults
console.warn(`Unknown addition config version: ${(config as any).version}`)
return defaultAdditionConfig
}
}
/**
* Parse and validate addition config from JSON string
* Automatically migrates old versions to latest
*/
export function parseAdditionConfig(jsonString: string): AdditionConfigV1 {
try {
const raw = JSON.parse(jsonString)
return migrateAdditionConfig(raw)
} catch (error) {
console.error('Failed to parse addition config JSON:', error)
return defaultAdditionConfig
}
}
/**
* Serialize addition config to JSON string
* Ensures version field is set to current version
*/
export function serializeAdditionConfig(config: Omit<AdditionConfigV1, 'version'>): string {
const versioned: AdditionConfigV1 = {
...config,
version: ADDITION_CURRENT_VERSION,
}
return JSON.stringify(versioned)
}
// =============================================================================
// FUTURE WORKSHEET TYPES (subtraction, multiplication, etc.)
// =============================================================================
// Example structure for future worksheet types:
//
// export const subtractionConfigV1Schema = z.object({
// version: z.literal(1),
// // ... fields specific to subtraction worksheets
// })
//
// export function parseSubtractionConfig(jsonString: string): SubtractionConfigV1 {
// // ... similar to parseAdditionConfig
// }

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,6 @@
/* Import Panda CSS generated styles */
@import "../../styled-system/styles.css";
/* Layout variables */
:root {
/* Navigation bar heights - used by both the nav itself and content padding */
--app-nav-height-full: 72px;
--app-nav-height-minimal: 92px;
}
/* Utility class for pages with fixed nav */
.with-fixed-nav {
padding-top: var(--app-nav-height, 80px);
}
/* Custom global styles */
body {
font-family:
@@ -57,13 +45,3 @@ body {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}

View File

@@ -2,7 +2,6 @@
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import Link from 'next/link'
import { useMessages, useTranslations } from 'next-intl'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
import { css } from '../../../../styled-system/css'
@@ -10,8 +9,6 @@ import { grid } from '../../../../styled-system/patterns'
export function ArithmeticOperationsGuide() {
const appConfig = useAbacusConfig()
const messages = useMessages() as any
const t = useTranslations('guide.arithmetic')
return (
<div className={css({ maxW: '4xl', mx: 'auto' })}>
@@ -34,7 +31,7 @@ export function ArithmeticOperationsGuide() {
mb: '4',
})}
>
{t('title')}
🧮 Arithmetic Operations
</h2>
<p
className={css({
@@ -42,7 +39,7 @@ export function ArithmeticOperationsGuide() {
opacity: '0.9',
})}
>
{t('subtitle')}
Master addition, subtraction, multiplication, and division on the soroban
</p>
</div>
@@ -69,10 +66,13 @@ export function ArithmeticOperationsGuide() {
gap: '2',
})}
>
{t('addition.title')}
<span></span> Addition
</h3>
<p className={css({ mb: '6', color: 'gray.700' })}>{t('addition.description')}</p>
<p className={css({ mb: '6', color: 'gray.700' })}>
Addition on the soroban follows the principle of moving beads toward the bar to increase
values.
</p>
<div className={css({ mb: '6' })}>
<h4
@@ -83,7 +83,7 @@ export function ArithmeticOperationsGuide() {
color: 'green.600',
})}
>
{t('addition.basicSteps.title')}
Basic Steps:
</h4>
<ol
className={css({
@@ -92,11 +92,12 @@ export function ArithmeticOperationsGuide() {
color: 'gray.700',
})}
>
{(t.raw('addition.basicSteps.steps') as string[]).map((step, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{i + 1}. {step}
</li>
))}
<li className={css({ mb: '2' })}>1. Set the first number on the soroban</li>
<li className={css({ mb: '2' })}>
2. Add the second number by moving beads toward the bar
</li>
<li className={css({ mb: '2' })}>3. Handle carries when a column exceeds 9</li>
<li>4. Read the final result</li>
</ol>
</div>
@@ -117,13 +118,11 @@ export function ArithmeticOperationsGuide() {
mb: '2',
})}
>
{t('addition.example.title')}
Example: 3 + 4 = 7
</h5>
<div className={grid({ columns: 3, gap: '4', alignItems: 'center' })}>
<div className={css({ textAlign: 'center' })}>
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>
{t('addition.example.start')}
</p>
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>Start: 3</p>
<div
className={css({
width: '160px',
@@ -155,9 +154,7 @@ export function ArithmeticOperationsGuide() {
</div>
<div className={css({ textAlign: 'center', fontSize: '2xl' })}>+</div>
<div className={css({ textAlign: 'center' })}>
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>
{t('addition.example.result')}
</p>
<p className={css({ fontSize: 'sm', mb: '2', color: 'green.700' })}>Result: 7</p>
<div
className={css({
width: '160px',
@@ -214,10 +211,12 @@ export function ArithmeticOperationsGuide() {
gap: '2',
})}
>
{t('guidedTutorial.title')}
<span>🎯</span> Guided Addition Tutorial
</h3>
<p className={css({ mb: '6', color: 'gray.700' })}>{t('guidedTutorial.description')}</p>
<p className={css({ mb: '6', color: 'gray.700' })}>
Learn addition step-by-step with interactive guidance, tooltips, and error correction.
</p>
<div
className={css({
@@ -240,10 +239,10 @@ export function ArithmeticOperationsGuide() {
})}
>
<span></span>
<strong>{t('guidedTutorial.editableNote')}</strong>
<strong>This tutorial is now editable!</strong>
</p>
<p className={css({ fontSize: 'xs', color: 'blue.600' })}>
{t('guidedTutorial.editableDesc')}{' '}
You can customize this tutorial using our new tutorial editor system.{' '}
<a
href="/tutorial-editor"
className={css({
@@ -252,13 +251,13 @@ export function ArithmeticOperationsGuide() {
_hover: { color: 'blue.800' },
})}
>
{t('guidedTutorial.editableLink')}
Open in Editor
</a>
</p>
</div>
<TutorialPlayer
tutorial={getTutorialForEditor(messages.tutorial || {})}
tutorial={getTutorialForEditor()}
isDebugMode={false}
showDebugPanel={false}
/>
@@ -287,10 +286,12 @@ export function ArithmeticOperationsGuide() {
gap: '2',
})}
>
{t('subtraction.title')}
<span></span> Subtraction
</h3>
<p className={css({ mb: '6', color: 'gray.700' })}>{t('subtraction.description')}</p>
<p className={css({ mb: '6', color: 'gray.700' })}>
Subtraction involves moving beads away from the bar to decrease values.
</p>
<div className={css({ mb: '6' })}>
<h4
@@ -301,7 +302,7 @@ export function ArithmeticOperationsGuide() {
color: 'red.600',
})}
>
{t('subtraction.basicSteps.title')}
Basic Steps:
</h4>
<ol
className={css({
@@ -310,11 +311,10 @@ export function ArithmeticOperationsGuide() {
color: 'gray.700',
})}
>
{(t.raw('subtraction.basicSteps.steps') as string[]).map((step, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{i + 1}. {step}
</li>
))}
<li className={css({ mb: '2' })}>1. Set the minuend (first number) on the soroban</li>
<li className={css({ mb: '2' })}>2. Subtract by moving beads away from the bar</li>
<li className={css({ mb: '2' })}>3. Handle borrowing when needed</li>
<li>4. Read the final result</li>
</ol>
</div>
@@ -335,13 +335,11 @@ export function ArithmeticOperationsGuide() {
mb: '2',
})}
>
{t('subtraction.example.title')}
Example: 8 - 3 = 5
</h5>
<div className={grid({ columns: 3, gap: '4', alignItems: 'center' })}>
<div className={css({ textAlign: 'center' })}>
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>
{t('subtraction.example.start')}
</p>
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>Start: 8</p>
<div
className={css({
width: '160px',
@@ -373,9 +371,7 @@ export function ArithmeticOperationsGuide() {
</div>
<div className={css({ textAlign: 'center', fontSize: '2xl' })}>-</div>
<div className={css({ textAlign: 'center' })}>
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>
{t('subtraction.example.result')}
</p>
<p className={css({ fontSize: 'sm', mb: '2', color: 'red.700' })}>Result: 5</p>
<div
className={css({
width: '160px',
@@ -432,11 +428,11 @@ export function ArithmeticOperationsGuide() {
gap: '2',
})}
>
{t('multiplicationDivision.title')}
<span></span> Multiplication & Division
</h3>
<p className={css({ mb: '6', color: 'gray.700' })}>
{t('multiplicationDivision.description')}
Advanced operations that combine addition/subtraction with position shifting.
</p>
<div className={grid({ columns: { base: 1, md: 2 }, gap: '6' })}>
@@ -457,7 +453,7 @@ export function ArithmeticOperationsGuide() {
mb: '3',
})}
>
{t('multiplicationDivision.multiplication.title')}
Multiplication
</h4>
<ul
className={css({
@@ -466,13 +462,10 @@ export function ArithmeticOperationsGuide() {
pl: '4',
})}
>
{(t.raw('multiplicationDivision.multiplication.points') as string[]).map(
(point, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{point}
</li>
)
)}
<li className={css({ mb: '2' })}> Break down into repeated addition</li>
<li className={css({ mb: '2' })}> Use position shifts for place values</li>
<li className={css({ mb: '2' })}> Master multiplication tables</li>
<li> Practice with single digits first</li>
</ul>
</div>
@@ -493,7 +486,7 @@ export function ArithmeticOperationsGuide() {
mb: '3',
})}
>
{t('multiplicationDivision.division.title')}
Division
</h4>
<ul
className={css({
@@ -502,11 +495,10 @@ export function ArithmeticOperationsGuide() {
pl: '4',
})}
>
{(t.raw('multiplicationDivision.division.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '2' })}> Use repeated subtraction method</li>
<li className={css({ mb: '2' })}> Estimate quotients carefully</li>
<li className={css({ mb: '2' })}> Handle remainders properly</li>
<li> Check results by multiplication</li>
</ul>
</div>
</div>
@@ -531,7 +523,7 @@ export function ArithmeticOperationsGuide() {
mb: '3',
})}
>
{t('practiceTips.title')}
💡 Master the Fundamentals
</h4>
<p
className={css({
@@ -539,7 +531,7 @@ export function ArithmeticOperationsGuide() {
opacity: '0.9',
})}
>
{t('practiceTips.description')}
Start with simple problems and gradually increase complexity
</p>
<Link
href="/create"
@@ -556,7 +548,7 @@ export function ArithmeticOperationsGuide() {
_hover: { transform: 'translateY(-1px)', shadow: 'lg' },
})}
>
{t('practiceTips.button')}
Practice Arithmetic Operations
</Link>
</div>
</div>

View File

@@ -2,13 +2,11 @@
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { css } from '../../../../styled-system/css'
import { grid, hstack, stack } from '../../../../styled-system/patterns'
export function ReadingNumbersGuide() {
const appConfig = useAbacusConfig()
const t = useTranslations('guide.reading')
return (
<div className={stack({ gap: '12' })}>
@@ -22,7 +20,7 @@ export function ReadingNumbersGuide() {
mb: '4',
})}
>
{t('title')}
🔍 Learning to Read Soroban Numbers
</h2>
<p
className={css({
@@ -33,7 +31,8 @@ export function ReadingNumbersGuide() {
lineHeight: 'relaxed',
})}
>
{t('subtitle')}
Master the fundamentals of reading numbers on the soroban with step-by-step visual
tutorials
</p>
</div>
@@ -62,7 +61,7 @@ export function ReadingNumbersGuide() {
fontSize: 'lg',
})}
>
{t('structure.number')}
1
</div>
<h3
className={css({
@@ -71,7 +70,7 @@ export function ReadingNumbersGuide() {
color: 'gray.900',
})}
>
{t('structure.title')}
Understanding the Structure
</h3>
</div>
@@ -83,7 +82,8 @@ export function ReadingNumbersGuide() {
lineHeight: 'relaxed',
})}
>
{t('structure.description')}
The soroban consists of two main sections divided by a horizontal bar. Understanding
this structure is fundamental to reading any number.
</p>
<div className={grid({ columns: { base: 1, md: 2 }, gap: '8' })}>
@@ -104,7 +104,7 @@ export function ReadingNumbersGuide() {
mb: '3',
})}
>
{t('structure.heaven.title')}
🌅 Heaven Beads (Top)
</h4>
<ul
className={css({
@@ -114,11 +114,10 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('structure.heaven.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '2' })}> Located above the horizontal bar</li>
<li className={css({ mb: '2' })}> Each bead represents 5</li>
<li className={css({ mb: '2' })}> Only one bead per column</li>
<li> When pushed down = active/counted</li>
</ul>
</div>
@@ -139,7 +138,7 @@ export function ReadingNumbersGuide() {
mb: '3',
})}
>
{t('structure.earth.title')}
🌍 Earth Beads (Bottom)
</h4>
<ul
className={css({
@@ -149,11 +148,10 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('structure.earth.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '2' })}> Located below the horizontal bar</li>
<li className={css({ mb: '2' })}> Each bead represents 1</li>
<li className={css({ mb: '2' })}> Four beads per column</li>
<li> When pushed up = active/counted</li>
</ul>
</div>
</div>
@@ -175,7 +173,7 @@ export function ReadingNumbersGuide() {
fontWeight: 'medium',
})}
>
{t('structure.keyConcept')}
💡 Key Concept: Active beads are those touching the horizontal bar
</p>
</div>
</div>
@@ -207,7 +205,7 @@ export function ReadingNumbersGuide() {
fontSize: 'lg',
})}
>
{t('singleDigits.number')}
2
</div>
<h3
className={css({
@@ -216,7 +214,7 @@ export function ReadingNumbersGuide() {
color: 'gray.900',
})}
>
{t('singleDigits.title')}
Reading Single Digits (1-9)
</h3>
</div>
@@ -227,16 +225,17 @@ export function ReadingNumbersGuide() {
lineHeight: 'relaxed',
})}
>
{t('singleDigits.description')}
Let's learn to read single digits by understanding how heaven and earth beads combine to
represent numbers 1 through 9.
</p>
<div className={grid({ columns: { base: 1, lg: 5 }, gap: '6' })}>
{[
{ num: 0, descKey: '0' },
{ num: 1, descKey: '1' },
{ num: 3, descKey: '3' },
{ num: 5, descKey: '5' },
{ num: 7, descKey: '7' },
{ num: 0, desc: 'No beads active - all away from bar' },
{ num: 1, desc: 'One earth bead pushed up' },
{ num: 3, desc: 'Three earth beads pushed up' },
{ num: 5, desc: 'Heaven bead pushed down' },
{ num: 7, desc: 'Heaven bead + two earth beads' },
].map((example) => (
<div
key={example.num}
@@ -245,7 +244,7 @@ export function ReadingNumbersGuide() {
border: '1px solid',
borderColor: 'gray.200',
rounded: 'lg',
p: '2',
p: '4',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
@@ -257,19 +256,27 @@ export function ReadingNumbersGuide() {
fontSize: 'xl',
fontWeight: 'bold',
color: 'brand.600',
mb: '2',
mb: '3',
})}
>
{example.num}
</div>
{/* Aspect ratio container for soroban - roughly 1:3 ratio */}
<div
className={css({
flex: '1',
width: '100%',
aspectRatio: '1/2.8',
maxW: '120px',
bg: 'white',
border: '1px solid',
borderColor: 'gray.300',
rounded: 'md',
mb: '3',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
my: '2',
overflow: 'hidden',
})}
>
<AbacusReact
@@ -278,7 +285,7 @@ export function ReadingNumbersGuide() {
beadShape={appConfig.beadShape}
colorScheme={appConfig.colorScheme}
hideInactiveBeads={appConfig.hideInactiveBeads}
scaleFactor={1.2}
scaleFactor={0.8}
interactive={false}
showNumbers={false}
animated={true}
@@ -291,10 +298,10 @@ export function ReadingNumbersGuide() {
color: 'gray.600',
lineHeight: 'tight',
textAlign: 'center',
mt: '2',
mt: 'auto',
})}
>
{t(`singleDigits.examples.${example.descKey}`)}
{example.desc}
</p>
</div>
))}
@@ -327,7 +334,7 @@ export function ReadingNumbersGuide() {
fontSize: 'lg',
})}
>
{t('multiDigit.number')}
3
</div>
<h3
className={css({
@@ -336,7 +343,7 @@ export function ReadingNumbersGuide() {
color: 'gray.900',
})}
>
{t('multiDigit.title')}
Multi-Digit Numbers
</h3>
</div>
@@ -347,7 +354,8 @@ export function ReadingNumbersGuide() {
lineHeight: 'relaxed',
})}
>
{t('multiDigit.description')}
Reading larger numbers is simply a matter of reading each column from left to right,
with each column representing a different place value.
</p>
<div
@@ -368,7 +376,7 @@ export function ReadingNumbersGuide() {
textAlign: 'center',
})}
>
{t('multiDigit.readingDirection.title')}
📍 Reading Direction & Place Values
</h4>
<div className={grid({ columns: { base: 1, md: 2 }, gap: '6' })}>
<div>
@@ -379,7 +387,7 @@ export function ReadingNumbersGuide() {
color: 'purple.800',
})}
>
{t('multiDigit.readingDirection.readingOrder.title')}
Reading Order:
</h5>
<ul
className={css({
@@ -388,13 +396,9 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('multiDigit.readingDirection.readingOrder.points') as string[]).map(
(point, i) => (
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
{point}
</li>
)
)}
<li className={css({ mb: '1' })}>• Always read from LEFT to RIGHT</li>
<li className={css({ mb: '1' })}>• Each column is one digit</li>
<li>• Combine digits to form the complete number</li>
</ul>
</div>
<div>
@@ -405,7 +409,7 @@ export function ReadingNumbersGuide() {
color: 'purple.800',
})}
>
{t('multiDigit.readingDirection.placeValues.title')}
Place Values:
</h5>
<ul
className={css({
@@ -414,13 +418,9 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('multiDigit.readingDirection.placeValues.points') as string[]).map(
(point, i) => (
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
{point}
</li>
)
)}
<li className={css({ mb: '1' })}>• Rightmost = Ones (1s)</li>
<li className={css({ mb: '1' })}>• Next left = Tens (10s)</li>
<li>• Continue for hundreds, thousands, etc.</li>
</ul>
</div>
</div>
@@ -445,14 +445,20 @@ export function ReadingNumbersGuide() {
textAlign: 'center',
})}
>
{t('multiDigit.examples.title')}
🔢 Multi-Digit Examples
</h4>
<div className={grid({ columns: { base: 1, md: 3 }, gap: '8' })}>
{[
{ num: 23, descKey: '23' },
{ num: 58, descKey: '58' },
{ num: 147, descKey: '147' },
{
num: 23,
desc: 'Two-digit: 2 in tens place + 3 in ones place',
},
{
num: 58,
desc: 'Heaven bead in tens (5) + heaven + earth beads in ones (8)',
},
{ num: 147, desc: 'Three-digit: 1 hundred + 4 tens + 7 ones' },
].map((example) => (
<div
key={example.num}
@@ -461,7 +467,7 @@ export function ReadingNumbersGuide() {
border: '1px solid',
borderColor: 'blue.300',
rounded: 'lg',
p: '2',
p: '4',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
@@ -473,19 +479,27 @@ export function ReadingNumbersGuide() {
fontSize: '2xl',
fontWeight: 'bold',
color: 'blue.600',
mb: '2',
mb: '3',
})}
>
{example.num}
</div>
{/* Larger container for multi-digit numbers */}
<div
className={css({
flex: '1',
width: '100%',
aspectRatio: '3/4',
maxW: '180px',
bg: 'gray.50',
border: '1px solid',
borderColor: 'blue.200',
rounded: 'md',
mb: '3',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
my: '2',
overflow: 'hidden',
})}
>
<AbacusReact
@@ -494,7 +508,7 @@ export function ReadingNumbersGuide() {
beadShape={appConfig.beadShape}
colorScheme={appConfig.colorScheme}
hideInactiveBeads={appConfig.hideInactiveBeads}
scaleFactor={1.2}
scaleFactor={0.9}
interactive={false}
showNumbers={false}
animated={true}
@@ -507,10 +521,9 @@ export function ReadingNumbersGuide() {
color: 'blue.700',
lineHeight: 'relaxed',
textAlign: 'center',
mt: '2',
})}
>
{t(`multiDigit.examples.${example.descKey}`)}
{example.desc}
</p>
</div>
))}
@@ -544,7 +557,7 @@ export function ReadingNumbersGuide() {
fontSize: 'lg',
})}
>
{t('practice.number')}
4
</div>
<h3
className={css({
@@ -553,7 +566,7 @@ export function ReadingNumbersGuide() {
color: 'gray.900',
})}
>
{t('practice.title')}
Practice Strategy
</h3>
</div>
@@ -575,7 +588,7 @@ export function ReadingNumbersGuide() {
mb: '4',
})}
>
{t('practice.learningTips.title')}
🎯 Learning Tips
</h4>
<ul
className={css({
@@ -585,11 +598,12 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('practice.learningTips.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '2' })}>• Start with single digits (0-9)</li>
<li className={css({ mb: '2' })}>
• Practice identifying active vs. inactive beads
</li>
<li className={css({ mb: '2' })}>• Work on speed recognition</li>
<li>• Progress to multi-digit numbers gradually</li>
</ul>
</div>
@@ -610,7 +624,7 @@ export function ReadingNumbersGuide() {
mb: '4',
})}
>
{t('practice.quickRecognition.title')}
⚡ Quick Recognition
</h4>
<ul
className={css({
@@ -620,11 +634,10 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('practice.quickRecognition.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 3 ? '2' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '2' })}>• Numbers 1-4: Only earth beads</li>
<li className={css({ mb: '2' })}>• Number 5: Only heaven bead</li>
<li className={css({ mb: '2' })}>• Numbers 6-9: Heaven + earth beads</li>
<li>• Zero: All beads away from bar</li>
</ul>
</div>
</div>
@@ -645,7 +658,7 @@ export function ReadingNumbersGuide() {
mb: '3',
})}
>
{t('practice.readyToPractice.title')}
🚀 Ready to Practice?
</h4>
<p
className={css({
@@ -653,7 +666,7 @@ export function ReadingNumbersGuide() {
opacity: '0.9',
})}
>
{t('practice.readyToPractice.description')}
Test your newfound knowledge with interactive flashcards
</p>
<Link
href="/create"
@@ -670,7 +683,7 @@ export function ReadingNumbersGuide() {
_hover: { transform: 'translateY(-1px)', shadow: 'lg' },
})}
>
{t('practice.readyToPractice.button')}
Create Practice Flashcards →
</Link>
</div>
</div>
@@ -701,7 +714,7 @@ export function ReadingNumbersGuide() {
fontSize: 'lg',
})}
>
{t('interactive.number')}
5
</div>
<h3
className={css({
@@ -710,7 +723,7 @@ export function ReadingNumbersGuide() {
color: 'gray.900',
})}
>
{t('interactive.title')}
Interactive Practice
</h3>
</div>
@@ -721,7 +734,8 @@ export function ReadingNumbersGuide() {
lineHeight: 'relaxed',
})}
>
{t('interactive.description')}
Try the interactive abacus below! Click on the beads to activate them and watch the
number change in real-time.
</p>
<div
@@ -742,7 +756,7 @@ export function ReadingNumbersGuide() {
textAlign: 'center',
})}
>
{t('interactive.howToUse.title')}
🎮 How to Use the Interactive Abacus
</h4>
<div className={grid({ columns: { base: 1, md: 2 }, gap: '6' })}>
<div>
@@ -753,7 +767,7 @@ export function ReadingNumbersGuide() {
color: 'orange.800',
})}
>
{t('interactive.howToUse.heaven.title')}
Heaven Beads (Top):
</h5>
<ul
className={css({
@@ -762,11 +776,9 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('interactive.howToUse.heaven.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '1' })}>• Worth 5 points each</li>
<li className={css({ mb: '1' })}>• Click to toggle on/off</li>
<li>• Blue when active, gray when inactive</li>
</ul>
</div>
<div>
@@ -777,7 +789,7 @@ export function ReadingNumbersGuide() {
color: 'orange.800',
})}
>
{t('interactive.howToUse.earth.title')}
Earth Beads (Bottom):
</h5>
<ul
className={css({
@@ -786,11 +798,9 @@ export function ReadingNumbersGuide() {
pl: '4',
})}
>
{(t.raw('interactive.howToUse.earth.points') as string[]).map((point, i) => (
<li key={i} className={css({ mb: i < 2 ? '1' : '0' })}>
{point}
</li>
))}
<li className={css({ mb: '1' })}>• Worth 1 point each</li>
<li className={css({ mb: '1' })}>• Click to activate groups</li>
<li>• Green when active, gray when inactive</li>
</ul>
</div>
</div>
@@ -839,7 +849,7 @@ export function ReadingNumbersGuide() {
mb: '3',
})}
>
{t('interactive.readyToPractice.title')}
🚀 Ready to Practice?
</h4>
<p
className={css({
@@ -847,7 +857,7 @@ export function ReadingNumbersGuide() {
opacity: '0.9',
})}
>
{t('interactive.readyToPractice.description')}
Test your newfound knowledge with interactive flashcards
</p>
<Link
href="/create"
@@ -864,7 +874,7 @@ export function ReadingNumbersGuide() {
_hover: { transform: 'translateY(-1px)', shadow: 'lg' },
})}
>
{t('interactive.readyToPractice.button')}
Create Practice Flashcards
</Link>
</div>
</div>

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