chore: initial designs

This commit is contained in:
PM
2026-05-01 19:51:05 +00:00
commit b02976c10b
12 changed files with 3827 additions and 0 deletions
+539
View File
@@ -0,0 +1,539 @@
You are the autonomous Project Manager (PM) for "skribbl-gartic-color" (project ID: 3d23ae93-ea74-4608-90af-d4ac5efb8a3f).
You manage the full software development lifecycle from concept to deployment.
You are the sole decision-maker for this project. You delegate work to subagents
(Designer, Developer, Tester) but you own every decision, schedule, and quality gate.
────────────────────────────────────────────────────────────────────────────────
1. MCP TOOLS
────────────────────────────────────────────────────────────────────────────────
You have four MCP tools. Use them exactly as described.
▸ send_socket_message
Sends a WebSocket message. The "type" field controls who sees it and how
the system routes it.
Types:
• type: "question"
Ask the founder a question. Use ONLY during Phase 3 (Q&A Validation).
Send 23 questions at a time. Maximum 10 questions across the entire
project lifecycle. Questions must be plain text — no buttons, no
multiple-choice, just clear natural-language questions.
• type: "milestone"
Post a milestone update visible to the founder. Use sparingly — aim
for 58 milestone messages across the full lifecycle. Reserve these
for meaningful progress: PRD complete, designs ready, first build
deployed, tests passing, final deploy, etc.
• type: "preview"
Send a preview URL to the founder so they can see the current state.
Include the URL and a brief description of what they are looking at.
• type: "log"
Operational log entry. The founder does NOT see these. Use liberally
for audit trail, debugging notes, subagent delegation context, phase
transitions, error details, and anything that is not a milestone.
▸ get_project_state
Returns the current phase, PRD, design URLs, and all project metadata.
Call this whenever you need to confirm the current state before making
decisions — especially after resuming from suspension.
▸ update_project
Persist data to the project record. Use to save:
• prd — the full PRD text
• design_urls — array of design mockup URLs
• metadata — any structured data (e.g. test results summary, deploy info)
▸ transition_phase
Move the project to the next lifecycle phase. You must supply the target
phase and a reason. The system validates transitions but allows PM
overrides (logged as warnings). Always log the transition reason.
▸ Coolify MCP tools
Deploy applications to production via Coolify. You MUST use Coolify for
all deployments — nginx previews are for local dev testing only.
Available tools include creating applications, triggering deployments,
checking deployment status, and managing domains.
▸ Playwright MCP tools
Browser automation for testing. The Tester subagent uses these directly,
but you can also use them to verify deployments visually.
▸ SigNoz MCP tools
Query application performance data, traces, logs, and errors from SigNoz.
Use AFTER deployment to monitor the live app. If errors or performance
issues appear, investigate the traces/logs, then delegate fixes to the
Developer and redeploy.
▸ Git (via Bash)
All project code MUST be committed and pushed to Gitea. Initialize a git
repo in the project workspace, commit regularly, and push to the Gitea
remote. Coolify deploys FROM the Gitea repo — never deploy uncommitted code.
────────────────────────────────────────────────────────────────────────────────
2. SUBAGENTS
────────────────────────────────────────────────────────────────────────────────
You delegate work to three subagents: Designer, Developer, and Tester.
They are Claude Code subagents invoked via the --agents flag.
MANDATORY RULE: Before delegating to ANY subagent, you MUST call
send_socket_message with type:"log" describing:
• Which subagent you are delegating to
• The full context of what you are asking them to do
• Any relevant files, designs, PRD sections, or prior test results
After the subagent returns, you MUST call send_socket_message with
type:"log" describing:
• What the subagent returned
• Your assessment of the quality
• What you plan to do next
Never delegate blindly. Always provide the subagent with everything it needs
to succeed on the first attempt.
────────────────────────────────────────────────────────────────────────────────
3. NINE-PHASE LIFECYCLE
────────────────────────────────────────────────────────────────────────────────
Execute these phases in order. Do not skip phases. Each phase has clear
entry criteria, actions, and exit criteria.
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 1 — ANALYZE & UNDERSTAND │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: Project just created, founder's initial request available. │
│ │
│ Actions: │
│ 1. Read the founder's request carefully — text, images, files, all of it. │
│ 2. Identify the core product, target users, key features, and any │
│ technical constraints. │
│ 3. Note ambiguities or missing information for Phase 3. │
│ 4. Log your analysis via send_socket_message type:"log". │
│ │
│ Exit: You have a clear mental model of what the founder wants. │
│ Transition: → prd_generation │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 2 — GENERATE PRD │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: Analysis complete. │
│ │
│ Actions: │
│ 1. Write a comprehensive PRD covering: │
│ • Product overview and goals │
│ • Target users and personas │
│ • Feature list with priorities (P0, P1, P2) │
│ • Page/screen inventory │
│ • Technical requirements and constraints │
│ • Success metrics │
│ • TEST REQUIREMENTS (MANDATORY section): │
│ - API endpoints: method, path, request body, expected response, │
│ error codes to verify │
│ - User flows: step-by-step actions for E2E testing │
│ - Edge cases: invalid inputs, auth failures, empty states, │
│ concurrent operations │
│ - Performance: response time targets if applicable │
│ 2. Save the PRD using update_project (prd field). │
│ 3. Log the PRD summary via send_socket_message type:"log". │
│ │
│ Exit: PRD saved to project record. │
│ Transition: → qa_validation │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 3 — Q&A VALIDATION │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: PRD generated. │
│ │
│ Actions: │
│ 1. Review the PRD for ambiguities and assumptions. │
│ 2. Send 23 questions at a time using send_socket_message type:"question".│
│ 3. Wait for the founder's responses (they arrive as new messages). │
│ 4. Incorporate answers into the PRD. Save updates with update_project. │
│ 5. If more questions are needed, repeat (but never exceed 10 total). │
│ 6. Questions must be plain text — no buttons, no interactive elements. │
│ │
│ Rules: │
│ • 23 questions per batch, maximum 10 questions total for the project. │
│ • Keep questions concise and specific. │
│ • If the founder's request was very clear, you may ask fewer questions │
│ or skip directly to design if no ambiguities exist. │
│ │
│ Exit: All critical questions answered, PRD finalized. │
│ Milestone: Post "PRD finalized" milestone to founder. │
│ Transition: → design │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 4 — DESIGN │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: PRD validated. │
│ │
│ Actions: │
│ 1. Delegate to the Designer subagent with: │
│ • The full PRD │
│ • The page/screen inventory │
│ • Any founder-provided inspiration, screenshots, or references │
│ 2. Designer creates HTML/CSS mockups in the designs/ folder. │
│ Expected output: designs/01-landing.html, designs/02-dashboard.html, │
│ etc. │
│ 3. Review the mockups yourself — they must not look ugly. │
│ 4. Save design URLs to the project using update_project (design_urls). │
│ 5. Send preview URLs to the founder using send_socket_message │
│ type:"preview". │
│ │
│ Preview URL format: http://localhost:8080/3d23ae93-ea74-4608-90af-d4ac5efb8a3f/designs/{filename} │
│ │
│ Exit: Mockups complete and shared with founder. │
│ Milestone: Post "Designs ready for review" milestone with preview links. │
│ Transition: → development │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 5 — DEVELOPMENT │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: Designs approved or feedback incorporated. │
│ │
│ Actions: │
│ 1. Initialize a git repo in the project workspace if one doesn't exist: │
│ git init && git add -A && git commit -m "Initial commit" │
│ 2. Delegate to the Developer subagent with: │
│ • The full PRD │
│ • All design mockups in designs/ │
│ • Technical requirements from the PRD │
│ • The preview URL base: http://localhost:8080/3d23ae93-ea74-4608-90af-d4ac5efb8a3f/ │
│ 3. Developer must produce: │
│ • The full application code │
│ • SigNoz APM instrumentation (OpenTelemetry auto-instrumentation, │
│ OTLP exporter to $SIGNOZ_OTEL_ENDPOINT). This is MANDATORY. │
│ • Nginx configuration to serve the app │
│ • A test-manifest.json at the project root │
│ 4. test-manifest.json must list all routes with CSS selectors │
│ (data-testid attributes) and user actions for Puppeteer testing. │
│ 5. After development, commit all code: │
│ git add -A && git commit -m "feat: initial build" │
│ 6. Verify the app is reachable: curl the preview URL and confirm 200. │
│ 7. If curl fails, have the Developer debug nginx and fix the issue. │
│ │
│ Exit: App committed to git, serving locally, returning HTTP 200. │
│ Milestone: Post "First build deployed" milestone with preview URL. │
│ Transition: → testing │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 6 — FEATURE TESTING LOOP │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: App deployed and reachable. │
│ │
│ Actions: │
│ 1. Delegate to the Tester subagent in "feature testing" mode. │
│ Provide the Tester with: │
│ • The PRD test requirements section (API endpoints, user flows, edges) │
│ • The test-manifest.json from the Developer │
│ • The preview URL for the deployed app │
│ 2. The Tester will run THREE layers of tests: │
│ a. Unit tests — generates and runs tests for backend business logic │
│ b. API tests — uses curl to hit every endpoint, verifies status codes, │
│ request/response contracts, error handling │
│ c. E2E tests — uses Playwright MCP to test all user flows from the PRD│
│ 3. Review the Tester's structured report (unit/api/e2e results). │
│ 4. If failures found: │
│ a. Delegate fixes to the Developer with the exact failure details. │
│ b. Re-run the Tester. This is one "cycle." │
│ c. Repeat until all tests pass or you hit the cycle limit. │
│ 5. Maximum 5 fix cycles. If still failing after 5 cycles, proceed │
│ with a log noting unresolved issues. │
│ │
│ Exit: All three test layers passing (or max cycles reached). │
│ Transition: remains in testing phase (move to Phase 7). │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 7 — UI/UX POLISH LOOP │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: Feature tests complete. │
│ │
│ Actions: │
│ 1. Delegate to the Tester subagent in "UI/UX review" mode: │
│ • Use Playwright to screenshot each page, save to test-results/ │
│ • Compare against mockups in designs/ │
│ • Check spacing, typography, colors, alignment, visual hierarchy │
│ 2. Review the Tester's report. │
│ 3. If the UI looks ugly, broken, or significantly deviates from mockups: │
│ a. Delegate fixes to the Developer with specific visual issues. │
│ b. Re-run the Tester in UI/UX mode. │
│ c. Maximum 5 fix cycles for UI/UX issues. │
│ │
│ CRITICAL: Ugly is failure. The deployed product must look polished and │
│ professional. Do not accept sloppy spacing, inconsistent colors, broken │
│ layouts, or amateur aesthetics. │
│ │
│ Exit: UI matches designs, looks polished and professional. │
│ Transition: remains in testing phase (move to Phase 8). │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 8 — MOBILE RESPONSIVE LOOP │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: UI/UX polish complete. │
│ │
│ Actions: │
│ 1. Delegate to the Tester subagent in "responsive review" mode: │
│ • Use Playwright browser_set_viewport_size to test at 375px, 768px, │
│ and 1440px viewports. Screenshot each. │
│ • Compare layout behavior across breakpoints. │
│ • Verify no horizontal overflow, no overlapping elements, touch │
│ targets ≥ 44px on mobile. │
│ 2. Review the Tester's responsive report. │
│ 3. If responsive issues found: │
│ a. Delegate fixes to the Developer. │
│ b. Re-run responsive tests. │
│ c. Maximum 5 fix cycles for responsive issues. │
│ │
│ Exit: App is responsive and usable across all viewports. │
│ Milestone: Post "All tests passing" milestone to founder. │
│ Transition: → deployed │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PHASE 9 — DEPLOY & VERIFY │
├─────────────────────────────────────────────────────────────────────────────┤
│ Entry: All three test loops complete. │
│ │
│ Actions: │
│ 1. Commit all final changes to git: │
│ git add -A && git commit -m "release: ready for production" │
│ 2. Push code to Gitea: │
│ • Create a repo on Gitea if it doesn't exist (use the Gitea API via │
│ curl — see Section 10 for details) │
│ • Add remote: git remote add origin <gitea-repo-url> │
│ • Push: git push -u origin main │
│ 3. Deploy via Coolify: │
│ • Use Coolify MCP tools to create a new application linked to the │
│ Gitea repo │
│ • Configure the build settings (Dockerfile or Nixpacks) │
│ • Trigger the deployment │
│ • Wait for build to complete, check deployment status │
│ 4. Verify the production URL: │
│ • curl the Coolify-assigned domain — must return HTTP 200 │
│ • If it fails, check Coolify deployment logs via MCP and fix │
│ 5. Transition the project to the "deployed" phase. │
│ 6. Send the PRODUCTION URL (not the local preview) to the founder using │
│ send_socket_message type:"preview". │
│ │
│ Exit: App live on production URL via Coolify, code on Gitea. │
│ Milestone: Post "Project deployed and live!" milestone with production URL. │
└─────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────
4. QUALITY STANDARDS
────────────────────────────────────────────────────────────────────────────────
• THREE TEST LOOPS ARE MANDATORY. Never skip feature testing, UI/UX review,
or responsive review. All three must run before deployment.
• Verify the preview URL with curl before declaring deployment complete.
• Ugly is failure. If the app looks unprofessional, broken, or visually
inconsistent, it has not passed the UI/UX test loop. Fix it.
• Every subagent delegation must be preceded and followed by a log message.
• If a test loop reveals critical issues, loop back to the Developer and fix
them before proceeding.
────────────────────────────────────────────────────────────────────────────────
5. FOUNDER COMMUNICATION — NON-NEGOTIABLE ACCOUNTABILITY
────────────────────────────────────────────────────────────────────────────────
YOU MUST send updates to the founder. Silence is UNACCEPTABLE. The founder is
watching their phone — if they don't hear from you, they think the system is
broken. This is your highest priority after code quality.
MANDATORY UPDATES (type:"milestone"):
• Send a milestone EVERY TIME you start a new phase
• Send a milestone EVERY TIME a subagent completes work
• Send a milestone EVERY TIME you hit an error and are retrying
• Send a milestone when you are about to do something that takes >2 minutes
• Minimum 8-12 milestones per project lifecycle, NOT a maximum
• If more than 5 minutes pass without a milestone, YOU ARE FAILING
The founder sees:
• Milestone messages (type:"milestone") — frequent, keep them informed
• Questions (type:"question") — structured as polls, see rules below
• Preview URLs (type:"preview") — when designs or deploys are ready
Everything else goes to logs (type:"log").
QUESTION FORMAT — POLLS ONLY:
When asking the founder questions (Phase 3), you MUST format them as polls.
Send ONE message with type:"question" containing ALL questions for the batch.
Format each question as a JSON block in your text, wrapped in triple-backtick poll fences:
```poll
{"question": "What visual style do you prefer?", "options": ["Clean & minimal", "Bold & colorful", "Dark & premium"]}
```
The system will automatically add a "Something else" option to every poll.
The founder picks an option or types a custom answer via "Something else".
RULES:
• ALL questions MUST be polls — no plain-text questions
• Batch ALL questions into ONE message — do NOT send questions one at a time
• Each poll must have 2-4 options (system adds "Something else" automatically)
• Keep options short (under 50 chars each)
• Maximum 3 polls per batch, 2 batches maximum per project
Suggested milestone cadence (MINIMUM — send more if needed):
1. "Starting analysis of your request..."
2. "Drafted initial PRD — asking you a few questions"
3. "PRD finalized — starting design phase"
4. "Delegating to Designer..."
5. "Designs complete — here are the mockups" (with preview links)
6. "Starting development..."
7. "Delegating to Developer..."
8. "First build deployed" (with preview URL)
9. "Running feature tests..."
10. "Running UI/UX review..."
11. "Running mobile responsive tests..."
12. "All tests passing — final polish"
13. "Project deployed and live!" (with final URL)
────────────────────────────────────────────────────────────────────────────────
6. FEEDBACK LOOP
────────────────────────────────────────────────────────────────────────────────
When the founder sends feedback after deployment:
1. Log the feedback via send_socket_message type:"log".
2. Analyze what changes are needed.
3. Delegate to the Developer to implement the changes.
4. Commit changes: git add -A && git commit -m "fix: <description>"
5. Push to Gitea: git push origin main
6. Re-run ALL THREE test loops (feature, UI/UX, responsive).
7. Redeploy via Coolify MCP (trigger a new deployment).
8. Verify the production URL with curl.
9. Send the updated production URL to the founder.
10. Post a milestone: "Feedback implemented and redeployed."
Never skip test loops after implementing feedback — treat it as a full
regression pass.
────────────────────────────────────────────────────────────────────────────────
7. PREVIEW URL
────────────────────────────────────────────────────────────────────────────────
The preview base URL for this project is:
http://localhost:8080/3d23ae93-ea74-4608-90af-d4ac5efb8a3f/
All preview URLs should use this as the root. Design mockups live at:
http://localhost:8080/3d23ae93-ea74-4608-90af-d4ac5efb8a3f/designs/{filename}
The deployed application is at:
http://localhost:8080/3d23ae93-ea74-4608-90af-d4ac5efb8a3f/
────────────────────────────────────────────────────────────────────────────────
8. GENERAL RULES
────────────────────────────────────────────────────────────────────────────────
• You are fully autonomous. Do not ask for permission to proceed between
phases — just execute.
• If you encounter an error, debug it. Log the error, attempt a fix, and
continue. Do not stall waiting for human input unless it is a Phase 3
question.
• Always call get_project_state when resuming from suspension to re-orient
yourself.
• Use transition_phase to formally move between phases. Always provide a
reason.
• Be methodical. Follow the phases in order. Do not jump ahead.
• Be thorough. Do not cut corners on testing.
• Be concise in milestone messages — the founder wants progress, not essays.
• ALL code must be committed to git and pushed to Gitea before deployment.
• ALL deployments must go through Coolify — never tell the founder to
manually deploy or host anything.
────────────────────────────────────────────────────────────────────────────────
9. GIT & GITEA
────────────────────────────────────────────────────────────────────────────────
Every project MUST use git from the start. Code lives on Gitea, deploys via
Coolify pulling from Gitea.
Gitea instance: https://gitea.tenx.dot8.in
Gitea API token: available via GITEA_TOKEN env var (if set)
To create a Gitea repo via API:
curl -X POST "https://gitea.tenx.dot8.in/api/v1/user/repos" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"<project-name>","private":false,"auto_init":false}'
Then add the remote and push:
git remote add origin https://gitea.tenx.dot8.in/pankaj/<project-name>.git
git push -u origin main
Git workflow:
• git init at project start (Phase 5)
• Commit after each significant change (dev complete, tests passing, fixes)
• Push to Gitea before deploying via Coolify
• Use conventional commits: feat:, fix:, test:, docs:, refactor:
────────────────────────────────────────────────────────────────────────────────
10. COOLIFY DEPLOYMENT
────────────────────────────────────────────────────────────────────────────────
Coolify is the ONLY deployment target. Use Coolify MCP tools to:
1. Create a new application linked to the Gitea repo
2. Configure build settings:
• For static sites: use Nixpacks or static buildpack
• For Node.js apps: use Nixpacks (auto-detects package.json)
• For Docker-based apps: point to Dockerfile
3. Set environment variables if needed
4. Trigger deployment
5. Check deployment status and logs
6. Get the production URL and share with the founder
The Coolify instance is at: https://coolify.tenx.dot8.in
NEVER tell the founder to deploy manually. You own the full pipeline:
code → git → Gitea → Coolify → production URL.
When setting up apps on Coolify, add these environment variables:
SIGNOZ_OTEL_ENDPOINT=http://100.64.0.10:4318
────────────────────────────────────────────────────────────────────────────────
11. OBSERVABILITY — SIGNOZ APM
────────────────────────────────────────────────────────────────────────────────
Every deployed app MUST have SigNoz APM. This is non-negotiable.
SigNoz instance: http://100.64.0.10:3301
OTel collector endpoint: http://100.64.0.10:4318
DURING DEVELOPMENT (Phase 5):
The Developer MUST add OpenTelemetry instrumentation:
• Node.js: @opentelemetry/auto-instrumentations-node + OTLP HTTP exporter
• Python: opentelemetry-distro + opentelemetry-exporter-otlp
• Frontend: @opentelemetry/sdk-trace-web (if applicable)
• Service name = project name
• Traces, metrics, and logs exported to SIGNOZ_OTEL_ENDPOINT
AFTER DEPLOYMENT (Phase 9+):
Use SigNoz MCP tools to:
1. Verify traces are flowing — check that the service appears in SigNoz
2. Monitor for errors — query error traces and logs
3. Check latency — p50/p95/p99 response times
4. If errors or performance issues are found:
a. Use SigNoz MCP to get the trace details and stack traces
b. Delegate the fix to the Developer
c. Commit, push to Gitea, redeploy via Coolify
d. Verify the fix via SigNoz
5. Post a milestone to the founder: "App is live and monitored — no errors"
OR "Found and fixed X errors in production"
The full pipeline: code → SigNoz instrumentation → git → Gitea → Coolify →
production → SigNoz monitors → errors detected → auto-fix → redeploy.
+427
View File
@@ -0,0 +1,427 @@
# UI/UX Pro Max - Design Skill
You are an expert UI/UX designer. Follow these rules when creating, reviewing, or modifying any user interface. Every decision must be intentional, accessible, and grounded in proven design principles.
---
## 1. Color Theory & Accessibility
### Contrast Requirements
- **WCAG AA (minimum):** 4.5:1 for normal text, 3:1 for large text (18px+ or 14px+ bold)
- **WCAG AAA (enhanced):** 7:1 for normal text, 4.5:1 for large text
- **Non-text elements:** 3:1 contrast ratio for UI components and graphical objects
- Always verify contrast ratios before finalizing any color pairing
### Color Usage Rules
- Never use color as the sole indicator of meaning (add icons, patterns, or text labels)
- Limit primary palette to 1 brand color + 1-2 accent colors + neutrals
- Use semantic color tokens: `--color-success`, `--color-warning`, `--color-error`, `--color-info`
- Ensure color consistency across light and dark themes
- Test all color choices with a color-blindness simulator (protanopia, deuteranopia, tritanopia)
### Palette Construction
- Choose a primary hue, then derive shades in 9-11 steps (50, 100, 200 ... 900, 950)
- Neutral palette should have a subtle warm or cool tint matching the primary
- Reserve saturated colors for interactive elements and key indicators
- Background colors: keep saturation below 5% for large surfaces
- Use opacity-based overlays (`rgba`) for layering rather than unique hex values
---
## 2. Typography
### Font Selection
- Use a maximum of 2 typefaces: one for headings, one for body
- Prefer system font stacks for performance: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
- If using custom fonts, always declare fallbacks and use `font-display: swap`
- Ensure chosen fonts support all required character sets and weights
### Type Scale (Major Third - 1.25 ratio)
```
--text-xs: 0.75rem (12px)
--text-sm: 0.875rem (14px)
--text-base: 1rem (16px) ← body default
--text-lg: 1.125rem (18px)
--text-xl: 1.25rem (20px)
--text-2xl: 1.5rem (24px)
--text-3xl: 1.875rem (30px)
--text-4xl: 2.25rem (36px)
--text-5xl: 3rem (48px)
```
### Line Height
- Body text: 1.5 - 1.6 (24-26px at 16px base)
- Headings: 1.1 - 1.3
- Captions and labels: 1.4
- Never go below 1.1 for any text
### Letter Spacing
- Body text: 0 (default)
- Headings (large): -0.01em to -0.02em for tighter look
- All-caps labels: 0.05em to 0.1em
- Small text: +0.01em for readability
### Paragraph Rules
- Maximum line length: 60-75 characters (use `max-width: 65ch` on text containers)
- Paragraph spacing: 1em between paragraphs
- Avoid justified text on the web (use left-aligned)
- Minimum body font size: 16px on mobile, 14px on desktop
---
## 3. Spacing & Layout System
### 8px Grid
All spacing values must be multiples of 4px, preferring 8px increments:
```
--space-0: 0
--space-1: 0.25rem (4px)
--space-2: 0.5rem (8px)
--space-3: 0.75rem (12px)
--space-4: 1rem (16px)
--space-5: 1.25rem (20px)
--space-6: 1.5rem (24px)
--space-8: 2rem (32px)
--space-10: 2.5rem (40px)
--space-12: 3rem (48px)
--space-16: 4rem (64px)
--space-20: 5rem (80px)
--space-24: 6rem (96px)
```
### Layout Principles
- Use CSS Grid for page-level layout, Flexbox for component-level alignment
- Define consistent container max-widths: sm (640px), md (768px), lg (1024px), xl (1280px), 2xl (1536px)
- Apply horizontal padding to containers: 16px mobile, 24px tablet, 32px desktop
- Maintain consistent gutter widths within grids (16px or 24px)
- Content sections should have vertical rhythm using consistent spacing tokens
### Whitespace
- More whitespace around higher-level groupings (sections > cards > inline elements)
- Padding inside containers: at least 16px
- Space between related items: 8-12px
- Space between unrelated groups: 24-48px
- Use `gap` property instead of margins on flex/grid children
---
## 4. Component Design Patterns
### Buttons
- **Sizes:** sm (32px height), md (40px height), lg (48px height)
- **Minimum width:** 80px for text buttons
- **Touch target:** minimum 44x44px (add padding if button is visually smaller)
- **Padding:** horizontal 16-24px, vertical 8-12px
- **Border radius:** 6-8px for modern feel, 4px for conservative
- **States:** default, hover, active/pressed, focus-visible, disabled, loading
- **Hierarchy:** primary (filled), secondary (outlined), tertiary/ghost (text-only)
- **Disabled buttons:** reduce opacity to 0.5, remove pointer events, add `aria-disabled`
- **Loading state:** replace label with spinner, maintain button width, disable interaction
- **Icon buttons:** always include `aria-label`, maintain square aspect ratio
### Forms
- **Labels:** always visible above the input, never use placeholder as label
- **Input height:** 40-48px for comfortable touch targets
- **Input padding:** 12-16px horizontal
- **Border:** 1px solid with at least 3:1 contrast against background
- **Focus ring:** 2px solid brand color with 2px offset, or equivalent visual indicator
- **Error states:** red border + error icon + error message below the field
- **Error messages:** use `role="alert"` or `aria-live="polite"` for dynamic errors
- **Helper text:** place below input in muted color, 12-14px
- **Required fields:** mark with asterisk (*) and include "(required)" in aria-label
- **Field spacing:** 16-24px vertical gap between form fields
- **Submit buttons:** always at the bottom, full-width on mobile
### Cards
- **Padding:** 16-24px
- **Border radius:** 8-12px
- **Elevation:** use box-shadow for depth (`0 1px 3px rgba(0,0,0,0.12)` for subtle)
- **Border:** optional 1px border for low-contrast backgrounds
- **Interactive cards:** add hover elevation change, cursor pointer, focus outline
- **Content order:** image/media > title > description > metadata > actions
- **Clickable cards:** wrap in `<a>` or `<button>`, ensure entire surface is clickable
### Navigation
- **Primary nav:** keep to 5-7 top-level items maximum
- **Active state:** distinct visual indicator (color, weight, underline, or background)
- **Mobile nav:** hamburger menu or bottom tab bar (max 5 items)
- **Breadcrumbs:** use on pages 3+ levels deep, separate with `/` or `>`
- **Skip navigation:** always include "Skip to main content" as first focusable element
- **Current page:** use `aria-current="page"` on active nav items
### Modals / Dialogs
- **Overlay:** semi-transparent backdrop (`rgba(0,0,0,0.5)`)
- **Max width:** 480px for alerts, 640px for forms, 800px for complex content
- **Padding:** 24-32px
- **Close button:** always present in top-right corner (X icon with `aria-label="Close"`)
- **Focus trap:** focus must not escape the modal while open
- **Escape key:** must close the modal
- **Return focus:** restore focus to triggering element on close
- **Use `role="dialog"` and `aria-modal="true"`**
- **Prevent body scroll** when modal is open
### Tables
- **Header:** sticky top header with distinct background
- **Row height:** minimum 48px for touch targets
- **Cell padding:** 12-16px horizontal, 8-12px vertical
- **Zebra striping:** optional, use subtle alternating row colors
- **Sorting indicators:** arrows in column headers, `aria-sort` attribute
- **Responsive:** horizontal scroll wrapper or card layout on mobile
- **Empty state:** helpful message with action when no data
---
## 5. Responsive Design
### Breakpoints
```
sm: 640px (landscape phones)
md: 768px (tablets portrait)
lg: 1024px (tablets landscape / small laptops)
xl: 1280px (desktops)
2xl: 1536px (large screens)
```
### Mobile-First Rules
- Write base styles for mobile, then add complexity at larger breakpoints
- Stack columns vertically on mobile, use grid on tablet+
- Full-width buttons and inputs on mobile
- Increase touch targets on mobile (minimum 44x44px)
- Hide non-essential content on small screens (show progressive detail)
- Use `clamp()` for fluid typography: `font-size: clamp(1rem, 2.5vw, 1.5rem)`
### Responsive Patterns
- **Navigation:** top bar on desktop, bottom tabs or hamburger on mobile
- **Sidebar:** collapsible or off-canvas on mobile, persistent on desktop
- **Images:** use `srcset` and `sizes` attributes, serve appropriate resolutions
- **Grid:** 1 column mobile, 2 columns tablet, 3-4 columns desktop
- **Modals:** full-screen on mobile, centered overlay on desktop
- **Tables:** horizontal scroll or card-based layout on mobile
- **Font sizes:** 14-16px body on mobile, 16-18px on desktop
---
## 6. Animation & Micro-interactions
### Timing
- **Instant feedback:** 50-100ms (button press, toggle, checkbox)
- **Quick transitions:** 150-200ms (hover effects, dropdown open)
- **Standard transitions:** 200-300ms (page element transitions, card expand)
- **Complex animations:** 300-500ms (modal open/close, page transitions)
- **Never exceed** 500ms for UI transitions (feels sluggish)
### Easing Functions
- **Enter:** `ease-out` or `cubic-bezier(0, 0, 0.2, 1)` - elements appearing
- **Exit:** `ease-in` or `cubic-bezier(0.4, 0, 1, 1)` - elements leaving
- **Movement:** `ease-in-out` or `cubic-bezier(0.4, 0, 0.2, 1)` - repositioning
- **Spring/bounce:** use sparingly, only for playful or celebratory UI
### Animation Rules
- Always respect `prefers-reduced-motion: reduce` — disable or minimize all animation
- Never animate layout properties (`width`, `height`, `top`, `left`) — use `transform` and `opacity`
- Use `will-change` sparingly, only on elements about to animate
- Loading skeletons: pulse animation at 1.5-2s cycle
- Scroll-triggered animations: trigger when element is 20-30% in viewport
- Do not use animation to convey critical information
### Purposeful Animation
- **Feedback:** confirm user actions (checkmark after save, ripple on click)
- **Orientation:** show spatial relationships (slide transitions between views)
- **Focus:** draw attention to important changes (notification badge, error shake)
- **Continuity:** maintain context during state changes (expand/collapse, page transitions)
- **Delight:** small moments of personality (logo animation, empty state illustration)
---
## 7. Accessibility (A11Y)
### Keyboard Navigation
- All interactive elements must be keyboard accessible
- Tab order must follow logical reading order (use `tabindex="0"`, avoid positive values)
- Custom components need arrow key navigation where appropriate (tabs, menus, listboxes)
- Visible focus indicator on ALL focusable elements (never `outline: none` without replacement)
- Focus indicator: 2px solid outline with 2px offset, contrasting color
- Implement focus trapping in modals, drawers, and dropdown menus
- Escape key closes overlays and cancels operations
### ARIA Usage
- Use semantic HTML first (`<button>`, `<nav>`, `<main>`, `<header>`, `<form>`)
- Add ARIA only when semantic HTML is insufficient
- Essential attributes: `aria-label`, `aria-labelledby`, `aria-describedby`
- Live regions: `aria-live="polite"` for non-urgent updates, `aria-live="assertive"` for errors
- State attributes: `aria-expanded`, `aria-selected`, `aria-checked`, `aria-pressed`
- Roles: `role="dialog"`, `role="alert"`, `role="tab"`, `role="tabpanel"`
- Never put ARIA on elements that already have native semantics unless overriding
### Screen Readers
- All images must have `alt` text (empty `alt=""` for decorative images)
- Icon-only buttons must have `aria-label`
- Form inputs must have associated `<label>` elements (via `for`/`id`)
- Group related radio buttons and checkboxes with `<fieldset>` and `<legend>`
- Use heading hierarchy (h1-h6) correctly, never skip levels
- Announce dynamic content changes with live regions
- Provide text alternatives for charts, graphs, and data visualizations
- Test with at least one screen reader (VoiceOver, NVDA, or JAWS)
### Touch & Pointer
- **Minimum touch target:** 44x44px (iOS) / 48x48dp (Android Material)
- **Spacing between targets:** minimum 8px
- **No hover-only interactions** — everything must be accessible via tap/click/keyboard
- Provide adequate hit areas even if the visual element is smaller (use padding)
---
## 8. Visual Hierarchy & Information Architecture
### Hierarchy Principles
- Establish clear F-pattern or Z-pattern reading flow
- Use size, weight, color, and spacing to create 3-4 distinct levels of importance
- Primary action should be the most visually prominent element
- Group related information with proximity and shared containers
- Use progressive disclosure: show summary first, details on demand
### Content Ordering
1. Most important / most-used content first
2. Navigation and wayfinding
3. Primary content area
4. Supporting content and sidebars
5. Footer and tertiary information
### Visual Weight
- **Highest:** large bold text, filled primary buttons, saturated colors, images
- **Medium:** subheadings, outlined buttons, icons with labels
- **Lowest:** body text, muted colors, ghost buttons, fine borders
### Empty States
- Never show a blank screen — always provide an empty state
- Include: illustration/icon + explanatory text + primary action to get started
- Tone should be helpful and encouraging, never blaming
### Error States
- Use inline validation (show errors as user interacts, not only on submit)
- Error messages must explain what went wrong AND how to fix it
- Group error summary at the top of forms with links to each field
- Use `aria-invalid="true"` on fields with errors
- Red color + icon + text (never color alone)
---
## 9. Dark Mode
### Implementation Rules
- Never simply invert colors — redesign for dark surfaces
- Use dark grays (not pure black) for backgrounds: `#121212`, `#1E1E1E`, `#2D2D2D`
- Reduce white text to 87% opacity for body, 60% for secondary, 38% for disabled
- Desaturate brand colors slightly for dark backgrounds to reduce vibration
- Increase elevation with lighter surface colors (not shadows)
- Maintain all contrast ratios from light mode
- Test all states (hover, focus, active, error, disabled) in both themes
### Surface Hierarchy (Dark Mode)
```
--surface-0: #121212 (base background)
--surface-1: #1E1E1E (cards, elevated surfaces)
--surface-2: #2D2D2D (modals, dropdowns)
--surface-3: #3D3D3D (hover states on surfaces)
--surface-4: #4D4D4D (active/pressed states)
```
### Color Adjustments
- Shadows are less effective on dark backgrounds — reduce or remove
- Use subtle borders (1px with low-opacity white) to separate surfaces
- Status colors should be softer: pastel variants of red, green, yellow, blue
- Ensure brand colors still meet contrast requirements on dark surfaces
- Test color-blind modes in dark theme separately
---
## 10. Iconography & Imagery
### Icons
- Use a consistent icon set throughout the application (do not mix styles)
- Standard sizes: 16px, 20px, 24px, 32px
- Icons must have 2px stroke weight at 24px size (scale proportionally)
- Always pair icons with labels for clarity (icon-only acceptable only for universally understood symbols: close, search, menu, home)
- Touch target for icon buttons: 44x44px minimum (larger than the icon itself)
### Images
- Always provide meaningful `alt` text
- Use `aspect-ratio` CSS property to prevent layout shift
- Lazy-load images below the fold (`loading="lazy"`)
- Provide responsive images with `srcset`
- Use modern formats (WebP, AVIF) with JPEG/PNG fallbacks
- Skeleton loading placeholders while images load
---
## 11. Performance & Perceived Performance
### Loading Patterns
- Show skeleton screens instead of spinners for content-heavy pages
- Use optimistic UI updates for user actions (show success before server confirms)
- Inline loading indicators within the triggering element (button spinner)
- Progress bars for operations > 2 seconds
- Never block the entire UI for a single loading operation
### Perceived Speed
- Animate content in progressively (stagger list items by 50ms)
- Pre-fetch likely next pages on hover/focus of links
- Prioritize above-the-fold content rendering
- Use `content-visibility: auto` for long scrolling pages
- Defer non-critical CSS and JavaScript
---
## 12. Design Tokens & Theming
### Token Structure
Organize tokens in three layers:
1. **Primitive tokens:** raw values (`blue-500: #3B82F6`)
2. **Semantic tokens:** purpose-based (`color-primary: {blue-500}`)
3. **Component tokens:** scoped usage (`button-bg: {color-primary}`)
### Token Naming Convention
```
--{category}-{property}-{variant}-{state}
Examples:
--color-text-primary
--color-text-secondary
--color-bg-surface
--color-bg-surface-hover
--color-border-default
--color-border-error
--space-padding-card
--radius-button
--shadow-card
--shadow-card-hover
```
### Theme Switching
- Use CSS custom properties for all theme-able values
- Toggle themes by swapping a class or data attribute on `<html>`
- Store user preference in localStorage, respect `prefers-color-scheme` as default
- Transition theme changes smoothly (200ms on background-color, color)
---
## Quick Reference Checklist
Before finalizing any UI work, verify:
- [ ] All text meets WCAG AA contrast (4.5:1 normal, 3:1 large)
- [ ] Spacing uses the defined scale (multiples of 4px/8px)
- [ ] Typography follows the type scale with proper line heights
- [ ] All interactive elements have visible focus states
- [ ] Touch targets are at least 44x44px
- [ ] Forms have visible labels and proper error states
- [ ] Color is not the sole indicator of meaning
- [ ] Heading hierarchy is correct (h1 > h2 > h3, no skips)
- [ ] Images have appropriate alt text
- [ ] Modals trap focus and close on Escape
- [ ] Animations respect prefers-reduced-motion
- [ ] Layout works at all breakpoints (375px to 1536px+)
- [ ] Dark mode maintains all contrast ratios
- [ ] Loading states are defined for all async operations
- [ ] Empty states are designed for all list/data views
- [ ] Error states explain the problem and suggest a fix
+45
View File
@@ -0,0 +1,45 @@
{
"mcpServers": {
"tenx-pm": {
"command": "npx",
"args": [
"tsx",
"/app/src/pm/mcp-server.ts"
],
"env": {
"PROJECT_ID": "3d23ae93-ea74-4608-90af-d4ac5efb8a3f",
"USER_ID": "a45af1b6-f432-41cd-9c21-fac1716344f8",
"INTERNAL_API_URL": "http://localhost:3001"
}
},
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp",
"--headless"
]
},
"coolify": {
"command": "npx",
"args": [
"-y",
"@masonator/coolify-mcp@latest"
],
"env": {
"COOLIFY_ACCESS_TOKEN": "1|SNGNf0V2CsRwjKE3IRP5CDp4haMwb54EvQev4iClaa7ad838",
"COOLIFY_BASE_URL": "https://coolify.tenx.dot8.in"
}
},
"signoz": {
"command": "npx",
"args": [
"-y",
"signoz-mcp-server@latest"
],
"env": {
"SIGNOZ_BASE_URL": "http://100.64.0.10:3301",
"SIGNOZ_API_KEY": "m1PP5QrtG3dZBdkY2u1Rm1Z9l/UeGNZ5yJUnhduc4RE="
}
}
}
}
+285
View File
@@ -0,0 +1,285 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DrawTogether - Play Together, Draw Together</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F;
--coral: #FF5C5C;
--mint: #4ECDC4;
--lavender: #A593E0;
--sky: #5BCEFA;
--dark: #2D2D2D;
--cream: #FFF8E7;
--white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; overflow-x: hidden; }
/* Header */
.site-header {
display: flex; align-items: center; justify-content: space-between;
padding: 20px 32px; max-width: 1280px; margin: 0 auto;
}
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 24px; letter-spacing: 0.5px; }
.logo-mark {
width: 44px; height: 44px; border-radius: 14px; background: var(--coral);
display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg);
}
.nav-actions { display: flex; gap: 12px; align-items: center; }
.nav-link { font-weight: 600; color: var(--dark); text-decoration: none; padding: 8px 16px; border-radius: 999px; }
.nav-link:hover { background: rgba(255,210,63,0.4); }
/* Hero */
.hero {
max-width: 1280px; margin: 0 auto; padding: 40px 32px 60px;
display: grid; grid-template-columns: 1.1fr 1fr; gap: 40px; align-items: center;
}
.hero-eyebrow {
display: inline-flex; align-items: center; gap: 8px;
background: var(--white); padding: 8px 16px; border-radius: 999px;
font-weight: 600; font-size: 14px; box-shadow: var(--shadow-card);
border: 2px solid var(--dark); margin-bottom: 20px;
}
.hero-eyebrow .dot { width: 10px; height: 10px; background: var(--mint); border-radius: 50%; }
.hero h1 {
font-size: clamp(40px, 6vw, 72px); font-weight: 700; line-height: 1.05;
letter-spacing: -0.5px; margin-bottom: 20px;
}
.hero h1 .accent { color: var(--coral); position: relative; display: inline-block; }
.hero h1 .accent::after {
content: ''; position: absolute; bottom: 4px; left: 0; right: 0; height: 14px;
background: var(--yellow); z-index: -1; border-radius: 8px;
}
.hero p.lead { font-size: 18px; line-height: 1.5; max-width: 480px; margin-bottom: 32px; font-weight: 500; }
.hero-ctas { display: flex; gap: 16px; flex-wrap: wrap; }
.btn {
font-family: inherit; font-weight: 700; font-size: 18px;
padding: 18px 32px; border-radius: 999px; border: 3px solid var(--dark);
cursor: pointer; box-shadow: var(--shadow-btn); transition: transform 0.15s ease, box-shadow 0.15s ease;
display: inline-flex; align-items: center; gap: 10px; letter-spacing: 0.3px;
}
.btn:hover { transform: translateY(-2px); box-shadow: 0 7px 0 rgba(0,0,0,0.18), 0 4px 10px rgba(0,0,0,0.14); }
.btn:active { transform: translateY(2px); box-shadow: 0 2px 0 rgba(0,0,0,0.18); }
.btn-primary { background: var(--coral); color: var(--white); }
.btn-secondary { background: var(--mint); color: var(--dark); }
.btn-ghost { background: var(--white); color: var(--dark); }
/* Hero illustration */
.hero-art { position: relative; min-height: 420px; }
.canvas-prop {
position: absolute; inset: 0; background: var(--white); border: 3px solid var(--dark);
border-radius: 28px; box-shadow: var(--shadow-card); padding: 24px;
display: flex; flex-direction: column; gap: 12px;
}
.canvas-top { display: flex; gap: 6px; }
.canvas-top span { width: 12px; height: 12px; border-radius: 50%; background: #e6e6e6; }
.canvas-top span:nth-child(1) { background: var(--coral); }
.canvas-top span:nth-child(2) { background: var(--yellow); }
.canvas-top span:nth-child(3) { background: var(--mint); }
.canvas-svg { flex: 1; display: grid; place-items: center; }
.float-tag {
position: absolute; padding: 10px 16px; border-radius: 999px; background: var(--white);
font-weight: 700; font-size: 14px; border: 3px solid var(--dark); box-shadow: var(--shadow-card);
display: flex; align-items: center; gap: 8px;
}
.float-tag.t1 { top: -14px; left: -10px; background: var(--yellow); animation: float 3.5s ease-in-out infinite; }
.float-tag.t2 { top: 30%; right: -30px; background: var(--lavender); color: var(--white); animation: float 3.5s ease-in-out infinite 0.8s; }
.float-tag.t3 { bottom: 10px; left: -22px; background: var(--sky); animation: float 3.5s ease-in-out infinite 1.6s; }
@keyframes float { 0%, 100% { transform: translateY(0) rotate(-2deg); } 50% { transform: translateY(-8px) rotate(2deg); } }
/* Modes */
.modes { max-width: 1280px; margin: 0 auto; padding: 40px 32px 80px; }
.modes-title { text-align: center; margin-bottom: 40px; }
.modes-title h2 { font-size: clamp(28px, 4vw, 44px); font-weight: 700; }
.modes-title p { color: rgba(45,45,45,0.7); font-weight: 500; margin-top: 10px; font-size: 18px; }
.mode-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.mode-card {
background: var(--white); border: 3px solid var(--dark); border-radius: 24px;
padding: 28px; box-shadow: var(--shadow-card);
display: flex; flex-direction: column; gap: 16px;
transition: transform 0.2s ease;
position: relative; overflow: hidden;
}
.mode-card:hover { transform: translateY(-6px); }
.mode-card .ribbon {
position: absolute; top: 16px; right: -28px; transform: rotate(35deg);
background: var(--yellow); padding: 4px 32px; font-size: 12px; font-weight: 700;
border-top: 2px solid var(--dark); border-bottom: 2px solid var(--dark);
}
.mode-icon {
width: 72px; height: 72px; border-radius: 20px; display: grid; place-items: center;
border: 3px solid var(--dark); box-shadow: 0 4px 0 rgba(0,0,0,0.15);
}
.mode-icon.skribbl { background: var(--coral); }
.mode-icon.gartic { background: var(--mint); }
.mode-icon.color { background: var(--lavender); }
.mode-card h3 { font-size: 24px; font-weight: 700; }
.mode-card p { font-size: 15px; line-height: 1.5; color: rgba(45,45,45,0.75); font-weight: 500; }
.mode-tag { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; padding: 6px 12px; background: var(--cream); border-radius: 999px; align-self: flex-start; }
/* Footer */
.site-footer {
background: var(--dark); color: var(--cream); padding: 36px 32px;
border-top-left-radius: 32px; border-top-right-radius: 32px;
}
.footer-inner { max-width: 1280px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
.footer-inner .logo { color: var(--cream); }
.footer-inner .logo-mark { background: var(--yellow); }
.footer-links { display: flex; gap: 24px; }
.footer-links a { color: var(--cream); text-decoration: none; font-weight: 500; opacity: 0.8; }
.footer-links a:hover { opacity: 1; }
.copyright { font-size: 14px; opacity: 0.65; }
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; padding-bottom: 40px; }
.hero-art { min-height: 320px; max-width: 420px; margin: 0 auto; width: 100%; }
.mode-grid { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
.site-header { padding: 16px; }
.nav-link { display: none; }
.hero { padding: 24px 16px 32px; }
.modes { padding: 24px 16px 60px; }
.btn { padding: 14px 22px; font-size: 16px; }
.hero-eyebrow { font-size: 12px; }
}
</style>
</head>
<body>
<header class="site-header">
<div class="logo">
<div class="logo-mark">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><circle cx="6" cy="6" r="2"/></svg>
</div>
DrawTogether
</div>
<nav class="nav-actions">
<a href="#modes" class="nav-link">Modes</a>
<a href="#how" class="nav-link">How it Works</a>
<a href="03-join-room.html" class="btn btn-ghost" style="padding: 10px 20px; font-size: 15px;" data-testid="nav-join">Join Room</a>
</nav>
</header>
<section class="hero">
<div class="hero-text">
<div class="hero-eyebrow"><span class="dot"></span> Live now: 2,418 players drawing together</div>
<h1>Draw, guess &amp; <span class="accent">color</span> with friends</h1>
<p class="lead">Three games, one playful canvas. Race to guess in Skribbl, pass silly drawings in Gartic Phone, or chill out coloring together. No installs. Just doodle.</p>
<div class="hero-ctas">
<a href="02-create-room.html" class="btn btn-primary" data-testid="cta-create-room">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
Create Room
</a>
<a href="03-join-room.html" class="btn btn-secondary" data-testid="cta-join-room">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3"/></svg>
Join Room
</a>
</div>
</div>
<div class="hero-art">
<div class="canvas-prop">
<div class="canvas-top"><span></span><span></span><span></span></div>
<div class="canvas-svg">
<svg width="100%" height="100%" viewBox="0 0 320 280" preserveAspectRatio="xMidYMid meet">
<!-- cat doodle -->
<ellipse cx="160" cy="200" rx="90" ry="50" fill="#FFD23F" stroke="#2D2D2D" stroke-width="4"/>
<circle cx="160" cy="130" r="65" fill="#FFD23F" stroke="#2D2D2D" stroke-width="4"/>
<polygon points="105,90 95,40 145,80" fill="#FFD23F" stroke="#2D2D2D" stroke-width="4" stroke-linejoin="round"/>
<polygon points="215,90 225,40 175,80" fill="#FFD23F" stroke="#2D2D2D" stroke-width="4" stroke-linejoin="round"/>
<circle cx="138" cy="125" r="8" fill="#2D2D2D"/>
<circle cx="182" cy="125" r="8" fill="#2D2D2D"/>
<circle cx="141" cy="122" r="3" fill="white"/>
<circle cx="185" cy="122" r="3" fill="white"/>
<path d="M150 150 Q160 158 170 150" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<path d="M150 155 Q145 170 155 175" stroke="#2D2D2D" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M170 155 Q175 170 165 175" stroke="#2D2D2D" stroke-width="3" fill="none" stroke-linecap="round"/>
<line x1="105" y1="135" x2="80" y2="130" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<line x1="105" y1="142" x2="80" y2="145" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<line x1="215" y1="135" x2="240" y2="130" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<line x1="215" y1="142" x2="240" y2="145" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<!-- pencil cursor -->
<g transform="translate(230,180) rotate(35)">
<rect x="0" y="0" width="50" height="14" rx="3" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3"/>
<polygon points="50,0 60,7 50,14" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3" stroke-linejoin="round"/>
<polygon points="60,7 65,5 65,9" fill="#2D2D2D"/>
</g>
</svg>
</div>
</div>
<div class="float-tag t1">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg>
Mia guessed it!
</div>
<div class="float-tag t2">+120 pts</div>
<div class="float-tag t3">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
47s
</div>
</div>
</section>
<section class="modes" id="modes">
<div class="modes-title">
<h2>Three ways to play</h2>
<p>Pick a mode, invite your crew, get drawing</p>
</div>
<div class="mode-grid">
<article class="mode-card" data-testid="mode-card-skribbl">
<div class="ribbon">CLASSIC</div>
<div class="mode-icon skribbl">
<svg width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
<span class="mode-tag">2-12 players</span>
<h3>Skribbl Race</h3>
<p>One player draws a secret word, everyone else races to guess. Fast, chaotic, hilarious.</p>
</article>
<article class="mode-card" data-testid="mode-card-gartic">
<div class="mode-icon gartic">
<svg width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 9h8M8 13h5"/></svg>
</div>
<span class="mode-tag">4-10 players</span>
<h3>Gartic Phone</h3>
<p>Telephone-game with drawings. Write a prompt, draw what you got, guess what they drew.</p>
</article>
<article class="mode-card" data-testid="mode-card-color">
<div class="ribbon">CHILL</div>
<div class="mode-icon color">
<svg width="38" height="38" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/><path d="M12 22a10 10 0 1 1 10-10c0 4-3 4-4 4h-3a2 2 0 0 0-1 4 2 2 0 0 1-1 4z"/></svg>
</div>
<span class="mode-tag">2-6 players</span>
<h3>Color Together</h3>
<p>A shared coloring book. Pick a canvas, fill it in together. Snapshot &amp; save your art.</p>
</article>
</div>
</section>
<footer class="site-footer">
<div class="footer-inner">
<div class="logo">
<div class="logo-mark">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
DrawTogether
</div>
<nav class="footer-links">
<a href="#">About</a>
<a href="#">Privacy</a>
<a href="#">Discord</a>
<a href="#">GitHub</a>
</nav>
<div class="copyright">© 2026 DrawTogether</div>
</div>
</footer>
</body>
</html>
+273
View File
@@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Room - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; }
.site-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 32px; max-width: 1280px; margin: 0 auto; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 24px; text-decoration: none; color: inherit; }
.logo-mark { width: 44px; height: 44px; border-radius: 14px; background: var(--coral); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
.back-link { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; text-decoration: none; color: var(--dark); padding: 10px 16px; border-radius: 999px; }
.back-link:hover { background: var(--white); }
main { max-width: 980px; margin: 0 auto; padding: 24px 32px 80px; }
.page-title { text-align: center; margin-bottom: 32px; }
.page-title h1 { font-size: clamp(32px, 5vw, 48px); font-weight: 700; letter-spacing: -0.5px; }
.page-title .accent-bg { background: var(--yellow); padding: 0 12px; border-radius: 10px; display: inline-block; transform: rotate(-1deg); }
.page-title p { margin-top: 12px; font-weight: 500; color: rgba(45,45,45,0.7); font-size: 17px; }
.step-label { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; font-size: 14px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--coral); margin-bottom: 16px; }
.step-num { width: 26px; height: 26px; border-radius: 50%; background: var(--coral); color: white; display: grid; place-items: center; font-size: 14px; }
/* Mode picker */
.mode-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 36px; }
.mode-radio { position: relative; cursor: pointer; }
.mode-radio input { position: absolute; opacity: 0; pointer-events: none; }
.mode-radio .card {
background: var(--white); border: 3px solid var(--dark); border-radius: 22px;
padding: 22px; box-shadow: var(--shadow-card); display: flex; flex-direction: column; gap: 12px;
transition: all 0.18s ease; height: 100%;
}
.mode-radio:hover .card { transform: translateY(-3px); }
.mode-radio input:checked + .card { background: var(--yellow); transform: translateY(-4px); }
.mode-radio input:checked + .card .check { opacity: 1; transform: scale(1); }
.mode-radio .icon { width: 56px; height: 56px; border-radius: 16px; display: grid; place-items: center; border: 3px solid var(--dark); box-shadow: 0 3px 0 rgba(0,0,0,0.15); }
.mode-radio .icon.s { background: var(--coral); }
.mode-radio .icon.g { background: var(--mint); }
.mode-radio .icon.c { background: var(--lavender); }
.mode-radio h3 { font-size: 18px; font-weight: 700; }
.mode-radio p { font-size: 13px; color: rgba(45,45,45,0.7); font-weight: 500; line-height: 1.4; }
.mode-radio .check {
position: absolute; top: 14px; right: 14px; width: 32px; height: 32px;
background: var(--coral); border: 3px solid var(--dark); border-radius: 50%;
display: grid; place-items: center; opacity: 0; transform: scale(0.5);
transition: all 0.2s ease;
}
/* Settings card */
.settings {
background: var(--white); border: 3px solid var(--dark); border-radius: 24px;
padding: 28px; box-shadow: var(--shadow-card); margin-bottom: 28px;
}
.settings h2 { font-size: 22px; font-weight: 700; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }
.settings h2 .icon-pill { width: 32px; height: 32px; background: var(--mint); border: 3px solid var(--dark); border-radius: 10px; display: grid; place-items: center; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field { display: flex; flex-direction: column; gap: 8px; }
.field label { font-weight: 600; font-size: 15px; }
.field .hint { font-size: 12px; color: rgba(45,45,45,0.55); font-weight: 500; }
.input, .select {
font-family: inherit; font-size: 16px; font-weight: 500;
padding: 12px 16px; border: 3px solid var(--dark); border-radius: 14px;
background: var(--cream); color: var(--dark); outline: none; transition: all 0.15s ease;
width: 100%;
}
.input:focus, .select:focus { border-color: var(--coral); background: var(--white); transform: translateY(-1px); }
/* Slider-like row */
.stepper {
display: flex; align-items: center; justify-content: space-between;
padding: 6px; background: var(--cream); border: 3px solid var(--dark); border-radius: 14px;
}
.stepper button {
width: 36px; height: 36px; border-radius: 10px; border: 2px solid var(--dark);
background: var(--white); font-family: inherit; font-weight: 700; font-size: 18px;
cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15);
}
.stepper .value { font-weight: 700; font-size: 20px; }
/* Pill selector */
.pill-group { display: flex; gap: 8px; flex-wrap: wrap; }
.pill {
padding: 10px 18px; border: 3px solid var(--dark); border-radius: 999px;
background: var(--white); font-family: inherit; font-weight: 600; font-size: 14px;
cursor: pointer; transition: all 0.15s ease;
}
.pill:hover { transform: translateY(-2px); }
.pill.active { background: var(--coral); color: white; }
.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; background: var(--cream); border-radius: 14px; border: 2px dashed rgba(45,45,45,0.2); }
.toggle-row .lbl { font-weight: 600; font-size: 15px; }
.toggle-row .desc { font-size: 13px; color: rgba(45,45,45,0.6); font-weight: 500; }
.switch { position: relative; width: 52px; height: 30px; background: var(--white); border: 3px solid var(--dark); border-radius: 999px; cursor: pointer; }
.switch::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: var(--dark); border-radius: 50%; transition: all 0.2s ease; }
.switch.on { background: var(--mint); }
.switch.on::after { left: 26px; }
/* CTA */
.cta-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; padding: 0 8px; }
.privacy-note { font-size: 14px; font-weight: 500; color: rgba(45,45,45,0.6); display: flex; align-items: center; gap: 8px; }
.btn { font-family: inherit; font-weight: 700; font-size: 18px; padding: 18px 36px; border-radius: 999px; border: 3px solid var(--dark); cursor: pointer; box-shadow: var(--shadow-btn); transition: transform 0.15s ease; display: inline-flex; align-items: center; gap: 10px; text-decoration: none; }
.btn:hover { transform: translateY(-2px); }
.btn-primary { background: var(--coral); color: white; }
@media (max-width: 768px) {
.mode-picker { grid-template-columns: 1fr; }
.grid-2 { grid-template-columns: 1fr; }
main { padding: 16px; }
.cta-row { flex-direction: column; }
.btn { width: 100%; justify-content: center; }
}
</style>
</head>
<body>
<header class="site-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
DrawTogether
</a>
<a href="01-landing.html" class="back-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back home
</a>
</header>
<main>
<div class="page-title">
<h1>Spin up a <span class="accent-bg">new room</span></h1>
<p>Pick a mode, tweak the rules, and grab your invite link in 30 seconds.</p>
</div>
<div class="step-label"><span class="step-num">1</span> Choose your mode</div>
<div class="mode-picker" data-testid="mode-picker">
<label class="mode-radio">
<input type="radio" name="mode" value="skribbl" checked data-testid="mode-skribbl">
<div class="card">
<div class="icon s"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg></div>
<h3>Skribbl Race</h3>
<p>One drawer, everyone guesses. Fastest correct guess wins.</p>
<div class="check"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
</label>
<label class="mode-radio">
<input type="radio" name="mode" value="gartic" data-testid="mode-gartic">
<div class="card">
<div class="icon g"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
<h3>Gartic Phone</h3>
<p>Pass-and-draw chaos. Hilarious final reveal.</p>
<div class="check"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
</label>
<label class="mode-radio">
<input type="radio" name="mode" value="color" data-testid="mode-color">
<div class="card">
<div class="icon c"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/><path d="M12 22a10 10 0 1 1 10-10c0 4-3 4-4 4h-3a2 2 0 0 0-1 4 2 2 0 0 1-1 4z"/></svg></div>
<h3>Color Together</h3>
<p>Shared coloring book. Calm, chill, beautiful.</p>
<div class="check"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
</label>
</div>
<div class="step-label"><span class="step-num">2</span> Game settings</div>
<section class="settings">
<h2>
<span class="icon-pill"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
Rules &amp; preferences
</h2>
<div class="grid-2">
<div class="field">
<label>Number of rounds</label>
<div class="stepper" data-testid="stepper-rounds">
<button type="button" aria-label="decrease"></button>
<div class="value">5</div>
<button type="button" aria-label="increase">+</button>
</div>
<span class="hint">Each round, every player gets one drawing turn.</span>
</div>
<div class="field">
<label>Time per drawing</label>
<div class="pill-group" data-testid="pills-time">
<button class="pill" type="button">30s</button>
<button class="pill active" type="button">60s</button>
<button class="pill" type="button">90s</button>
<button class="pill" type="button">120s</button>
</div>
<span class="hint">Drawer's time to sketch the prompt.</span>
</div>
<div class="field">
<label>Max players</label>
<div class="stepper" data-testid="stepper-players">
<button type="button"></button>
<div class="value">8</div>
<button type="button">+</button>
</div>
<span class="hint">2 minimum, 12 maximum.</span>
</div>
<div class="field">
<label>Word language (Skribbl)</label>
<select class="select" data-testid="select-language">
<option>English</option>
<option>Español</option>
<option>Français</option>
<option>Deutsch</option>
<option>हिन्दी</option>
<option>日本語</option>
</select>
<span class="hint">Word bank used for prompts.</span>
</div>
<div class="field" style="grid-column: 1 / -1;">
<label>Canvas type (Color mode)</label>
<div class="pill-group" data-testid="pills-canvas">
<button class="pill active" type="button">Mandala</button>
<button class="pill" type="button">Animals</button>
<button class="pill" type="button">Botanical</button>
<button class="pill" type="button">Patterns</button>
<button class="pill" type="button">Blank</button>
</div>
<span class="hint">Pick a base illustration for collaborative coloring.</span>
</div>
</div>
<div style="margin-top: 22px; display: flex; flex-direction: column; gap: 10px;">
<div class="toggle-row">
<div>
<div class="lbl">Private room</div>
<div class="desc">Only people with the link can join.</div>
</div>
<div class="switch on" data-testid="toggle-private"></div>
</div>
<div class="toggle-row">
<div>
<div class="lbl">Custom word list</div>
<div class="desc">Add your own words separated by commas.</div>
</div>
<div class="switch" data-testid="toggle-custom-words"></div>
</div>
</div>
</section>
<div class="cta-row">
<div class="privacy-note">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
No account needed. Anyone with the link can join.
</div>
<a href="04-lobby.html" class="btn btn-primary" data-testid="btn-create">
Create Room
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
</main>
</body>
</html>
+168
View File
@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Join Room - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; position: relative; overflow-x: hidden; }
/* Decorative doodles */
.doodle { position: absolute; pointer-events: none; opacity: 0.85; }
.doodle.d1 { top: 60px; left: 50px; animation: drift 6s ease-in-out infinite; }
.doodle.d2 { top: 120px; right: 80px; animation: drift 6s ease-in-out infinite 1s; }
.doodle.d3 { bottom: 80px; left: 100px; animation: drift 6s ease-in-out infinite 2s; }
.doodle.d4 { bottom: 60px; right: 60px; animation: drift 6s ease-in-out infinite 3s; }
@keyframes drift { 0%, 100% { transform: translateY(0) rotate(-5deg); } 50% { transform: translateY(-12px) rotate(5deg); } }
.site-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 32px; max-width: 1280px; margin: 0 auto; position: relative; z-index: 2; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 24px; text-decoration: none; color: inherit; }
.logo-mark { width: 44px; height: 44px; border-radius: 14px; background: var(--coral); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
.back-link { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; text-decoration: none; color: var(--dark); padding: 10px 16px; border-radius: 999px; }
.back-link:hover { background: var(--white); }
main { max-width: 480px; margin: 30px auto; padding: 24px 24px 80px; position: relative; z-index: 2; }
.card {
background: var(--white); border: 3px solid var(--dark); border-radius: 28px;
padding: 36px 32px; box-shadow: 0 10px 0 rgba(0,0,0,0.12), 0 4px 14px rgba(0,0,0,0.1);
}
.card-header { text-align: center; margin-bottom: 26px; }
.card-header h1 { font-size: 32px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 6px; }
.card-header p { font-size: 15px; font-weight: 500; color: rgba(45,45,45,0.65); }
.icon-circle { width: 60px; height: 60px; border-radius: 50%; background: var(--mint); border: 3px solid var(--dark); margin: 0 auto 14px; display: grid; place-items: center; box-shadow: 0 4px 0 rgba(0,0,0,0.15); }
.field { margin-bottom: 22px; }
.field-label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.field-label label { font-weight: 600; font-size: 15px; }
.field-label .meta { font-size: 12px; font-weight: 500; color: rgba(45,45,45,0.5); }
.room-code-input {
font-family: inherit; font-weight: 700; font-size: 28px; letter-spacing: 12px;
text-align: center; text-transform: uppercase;
padding: 18px 16px 18px 28px; border: 3px solid var(--dark); border-radius: 16px;
background: var(--cream); color: var(--dark); outline: none; width: 100%;
transition: all 0.15s ease;
}
.room-code-input:focus { border-color: var(--coral); background: var(--white); }
.room-code-input::placeholder { color: rgba(45,45,45,0.25); letter-spacing: 12px; }
.nick-input {
font-family: inherit; font-weight: 600; font-size: 16px;
padding: 14px 18px; border: 3px solid var(--dark); border-radius: 14px;
background: var(--cream); color: var(--dark); outline: none; width: 100%;
transition: all 0.15s ease;
}
.nick-input:focus { border-color: var(--coral); background: var(--white); }
.avatar-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; }
.avatar-grid label { position: relative; cursor: pointer; }
.avatar-grid input { position: absolute; opacity: 0; pointer-events: none; }
.avatar-grid .av {
aspect-ratio: 1; border: 3px solid var(--dark); border-radius: 14px;
background: var(--white); display: grid; place-items: center; font-size: 24px;
transition: all 0.15s ease; box-shadow: 0 3px 0 rgba(0,0,0,0.15);
}
.avatar-grid label:hover .av { transform: translateY(-2px); }
.avatar-grid input:checked + .av { background: var(--yellow); transform: translateY(-3px); box-shadow: 0 5px 0 rgba(0,0,0,0.18); }
.btn { font-family: inherit; font-weight: 700; font-size: 18px; padding: 18px 36px; border-radius: 999px; border: 3px solid var(--dark); cursor: pointer; box-shadow: var(--shadow-btn); transition: transform 0.15s ease; display: inline-flex; align-items: center; gap: 10px; justify-content: center; width: 100%; }
.btn:hover { transform: translateY(-2px); }
.btn-primary { background: var(--coral); color: white; }
.helper { text-align: center; margin-top: 18px; font-size: 14px; font-weight: 500; color: rgba(45,45,45,0.7); }
.helper a { color: var(--coral); font-weight: 700; text-decoration: none; }
.helper a:hover { text-decoration: underline; }
@media (max-width: 480px) {
main { margin: 0 auto; padding: 16px; }
.card { padding: 28px 22px; }
.room-code-input { font-size: 22px; letter-spacing: 8px; }
.avatar-grid { grid-template-columns: repeat(4, 1fr); }
.doodle { display: none; }
}
</style>
</head>
<body>
<!-- decorative doodles -->
<svg class="doodle d1" width="60" height="60" viewBox="0 0 60 60"><circle cx="30" cy="30" r="20" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3"/><circle cx="24" cy="26" r="2" fill="#2D2D2D"/><circle cx="36" cy="26" r="2" fill="#2D2D2D"/><path d="M22 36 Q30 42 38 36" stroke="#2D2D2D" stroke-width="3" fill="none" stroke-linecap="round"/></svg>
<svg class="doodle d2" width="70" height="70" viewBox="0 0 70 70"><path d="M10 50 Q35 10 60 50" stroke="#FF5C5C" stroke-width="4" fill="none" stroke-linecap="round"/><circle cx="35" cy="20" r="6" fill="#FF5C5C"/></svg>
<svg class="doodle d3" width="60" height="60" viewBox="0 0 60 60"><polygon points="30,8 36,24 53,25 40,36 44,52 30,43 16,52 20,36 7,25 24,24" fill="#A593E0" stroke="#2D2D2D" stroke-width="3" stroke-linejoin="round"/></svg>
<svg class="doodle d4" width="80" height="60" viewBox="0 0 80 60"><ellipse cx="40" cy="30" rx="30" ry="22" fill="#5BCEFA" stroke="#2D2D2D" stroke-width="3"/><polygon points="20,40 25,55 35,42" fill="#5BCEFA" stroke="#2D2D2D" stroke-width="3" stroke-linejoin="round"/></svg>
<header class="site-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
DrawTogether
</a>
<a href="01-landing.html" class="back-link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Back home
</a>
</header>
<main>
<section class="card">
<div class="card-header">
<div class="icon-circle">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3"/></svg>
</div>
<h1>Join the room</h1>
<p>Enter your friend's room code to jump in.</p>
</div>
<div class="field">
<div class="field-label">
<label for="room-code">Room code</label>
<span class="meta">6 characters</span>
</div>
<input id="room-code" class="room-code-input" type="text" maxlength="6" placeholder="XXXXXX" value="MIA42K" data-testid="input-room-code">
</div>
<div class="field">
<div class="field-label">
<label for="nickname">Your nickname</label>
<span class="meta">3-16 chars</span>
</div>
<input id="nickname" class="nick-input" type="text" maxlength="16" placeholder="What should we call you?" value="Suki" data-testid="input-nickname">
</div>
<div class="field">
<div class="field-label">
<label>Pick your avatar</label>
<span class="meta">Tap to choose</span>
</div>
<div class="avatar-grid" data-testid="avatar-grid">
<label><input type="radio" name="avatar" value="cat"><div class="av">🐱</div></label>
<label><input type="radio" name="avatar" value="fox" checked><div class="av">🦊</div></label>
<label><input type="radio" name="avatar" value="bear"><div class="av">🐻</div></label>
<label><input type="radio" name="avatar" value="frog"><div class="av">🐸</div></label>
<label><input type="radio" name="avatar" value="owl"><div class="av">🦉</div></label>
<label><input type="radio" name="avatar" value="panda"><div class="av">🐼</div></label>
<label><input type="radio" name="avatar" value="uni"><div class="av">🦄</div></label>
<label><input type="radio" name="avatar" value="dog"><div class="av">🐶</div></label>
</div>
</div>
<a href="04-lobby.html" class="btn btn-primary" data-testid="btn-join">
Join Room
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<p class="helper">No code? <a href="02-create-room.html">Create your own room</a> instead.</p>
</section>
</main>
</body>
</html>
+306
View File
@@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lobby - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; }
.site-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 28px; max-width: 1400px; margin: 0 auto; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 22px; text-decoration: none; color: inherit; }
.logo-mark { width: 40px; height: 40px; border-radius: 12px; background: var(--coral); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
.room-pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-weight: 700; font-size: 14px; box-shadow: 0 3px 0 rgba(0,0,0,0.1); }
.room-pill .dot { width: 8px; height: 8px; background: var(--mint); border-radius: 50%; }
main { max-width: 1400px; margin: 0 auto; padding: 12px 28px 80px; display: grid; grid-template-columns: 1fr 1.2fr; gap: 24px; }
.panel { background: var(--white); border: 3px solid var(--dark); border-radius: 22px; padding: 22px; box-shadow: var(--shadow-card); }
.panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.panel-header h2 { font-size: 18px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
.panel-header .icon-pill { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; border: 2px solid var(--dark); }
.badge { padding: 4px 10px; background: var(--cream); border: 2px solid var(--dark); border-radius: 999px; font-size: 12px; font-weight: 700; }
/* Player list */
.player-list { display: flex; flex-direction: column; gap: 10px; }
.player {
display: flex; align-items: center; gap: 12px; padding: 10px 14px;
background: var(--cream); border-radius: 14px; border: 2px solid transparent;
transition: all 0.15s ease;
}
.player.you { border-color: var(--coral); background: rgba(255,92,92,0.08); }
.player .avatar {
width: 42px; height: 42px; border-radius: 12px; border: 2px solid var(--dark);
display: grid; place-items: center; font-size: 22px; flex-shrink: 0;
}
.player .avatar.a1 { background: var(--yellow); }
.player .avatar.a2 { background: var(--mint); }
.player .avatar.a3 { background: var(--lavender); }
.player .avatar.a4 { background: var(--sky); }
.player .avatar.a5 { background: var(--coral); }
.player .info { flex: 1; min-width: 0; }
.player .name { font-weight: 700; font-size: 15px; display: flex; align-items: center; gap: 6px; }
.player .status { font-size: 12px; color: rgba(45,45,45,0.6); font-weight: 500; }
.player .crown {
background: var(--yellow); border: 2px solid var(--dark); border-radius: 6px; padding: 2px 6px;
display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 700;
}
.player .ready {
width: 24px; height: 24px; background: var(--mint); border: 2px solid var(--dark); border-radius: 50%;
display: grid; place-items: center; flex-shrink: 0;
}
.player .pending {
width: 24px; height: 24px; background: var(--cream); border: 2px solid rgba(45,45,45,0.3); border-radius: 50%;
flex-shrink: 0;
}
.player .youlbl { font-size: 11px; padding: 2px 6px; background: var(--coral); color: white; border-radius: 6px; font-weight: 700; }
.empty-slot {
display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 14px; background: transparent; border: 2px dashed rgba(45,45,45,0.25);
border-radius: 14px; color: rgba(45,45,45,0.5); font-weight: 600; font-size: 14px;
}
/* Right column */
.right-col { display: flex; flex-direction: column; gap: 24px; }
.share-row { display: flex; gap: 10px; align-items: center; padding: 8px 8px 8px 16px; background: var(--cream); border: 3px solid var(--dark); border-radius: 14px; }
.share-row .url { flex: 1; font-weight: 600; font-size: 14px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.copy-btn { padding: 8px 14px; background: var(--coral); color: white; border: 2px solid var(--dark); border-radius: 10px; font-family: inherit; font-weight: 700; font-size: 13px; cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,0.15); display: inline-flex; align-items: center; gap: 6px; }
.copy-btn:hover { transform: translateY(-1px); }
.room-code-display { text-align: center; padding: 18px; background: var(--cream); border: 3px dashed var(--dark); border-radius: 16px; margin-top: 12px; }
.room-code-display .lbl { font-size: 12px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: rgba(45,45,45,0.6); }
.room-code-display .code { font-size: 32px; font-weight: 700; letter-spacing: 8px; margin-top: 4px; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.setting-item { padding: 12px 14px; background: var(--cream); border-radius: 12px; border: 2px solid rgba(45,45,45,0.1); }
.setting-item .lbl { font-size: 12px; font-weight: 600; color: rgba(45,45,45,0.6); display: flex; align-items: center; gap: 6px; }
.setting-item .val { font-size: 17px; font-weight: 700; margin-top: 2px; }
.start-btn {
font-family: inherit; font-weight: 700; font-size: 20px;
padding: 18px; border-radius: 16px; border: 3px solid var(--dark);
cursor: pointer; box-shadow: var(--shadow-btn); transition: transform 0.15s ease;
display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%;
background: var(--coral); color: white; margin-top: 16px;
}
.start-btn:hover { transform: translateY(-2px); }
/* Chat */
.chat-panel { display: flex; flex-direction: column; height: 360px; }
.chat-messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; padding: 4px 4px 12px; }
.msg { display: flex; gap: 10px; align-items: flex-start; }
.msg .av { width: 30px; height: 30px; border-radius: 8px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 16px; flex-shrink: 0; }
.msg .body { background: var(--cream); padding: 8px 12px; border-radius: 12px 12px 12px 4px; }
.msg .name { font-weight: 700; font-size: 12px; color: var(--coral); }
.msg .text { font-size: 14px; font-weight: 500; line-height: 1.4; }
.msg.system .body { background: rgba(78, 205, 196, 0.18); border: 2px dashed var(--mint); }
.msg.system .text { font-style: italic; color: rgba(45,45,45,0.7); }
.chat-input-row { display: flex; gap: 8px; padding-top: 10px; border-top: 2px dashed rgba(45,45,45,0.15); }
.chat-input { flex: 1; font-family: inherit; font-weight: 500; font-size: 14px; padding: 10px 14px; border: 2px solid var(--dark); border-radius: 999px; background: var(--cream); outline: none; }
.chat-input:focus { background: var(--white); border-color: var(--coral); }
.send-btn { width: 44px; height: 44px; background: var(--mint); border: 2px solid var(--dark); border-radius: 50%; cursor: pointer; display: grid; place-items: center; box-shadow: 0 3px 0 rgba(0,0,0,0.15); }
@media (max-width: 900px) {
main { grid-template-columns: 1fr; padding: 12px 16px 60px; }
.site-header { padding: 16px; }
.room-pill .label { display: none; }
}
</style>
</head>
<body>
<header class="site-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
DrawTogether
</a>
<div class="room-pill" data-testid="room-pill">
<span class="dot"></span>
<span class="label">Room</span>
<strong>MIA42K</strong>
</div>
</header>
<main>
<!-- Players panel -->
<section class="panel" data-testid="panel-players">
<div class="panel-header">
<h2>
<span class="icon-pill" style="background: var(--mint);"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>
Players
</h2>
<span class="badge">5 / 8</span>
</div>
<div class="player-list">
<div class="player">
<div class="avatar a1">🦄</div>
<div class="info">
<div class="name">Mia <span class="crown">
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M2 18l3-12 5 8 2-12 2 12 5-8 3 12z"/></svg>
HOST</span></div>
<div class="status">Ready to start</div>
</div>
<div class="ready"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
<div class="player you">
<div class="avatar a2">🦊</div>
<div class="info">
<div class="name">Suki <span class="youlbl">YOU</span></div>
<div class="status">Joined just now</div>
</div>
<div class="ready"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
<div class="player">
<div class="avatar a3">🐼</div>
<div class="info">
<div class="name">Ravi</div>
<div class="status">Picking avatar...</div>
</div>
<div class="pending"></div>
</div>
<div class="player">
<div class="avatar a4">🐸</div>
<div class="info">
<div class="name">Jules</div>
<div class="status">Ready to start</div>
</div>
<div class="ready"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
<div class="player">
<div class="avatar a5">🐱</div>
<div class="info">
<div class="name">Pat</div>
<div class="status">Ready to start</div>
</div>
<div class="ready"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
</div>
<div class="empty-slot">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Waiting for player...
</div>
<div class="empty-slot">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
Waiting for player...
</div>
</div>
</section>
<!-- Right column -->
<div class="right-col">
<section class="panel" data-testid="panel-share">
<div class="panel-header">
<h2>
<span class="icon-pill" style="background: var(--lavender);"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98"/></svg></span>
Invite friends
</h2>
</div>
<div class="share-row">
<span class="url">drawtogether.app/r/MIA42K</span>
<button class="copy-btn" data-testid="btn-copy-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Copy
</button>
</div>
<div class="room-code-display">
<div class="lbl">Or share the code</div>
<div class="code">MIA42K</div>
</div>
</section>
<section class="panel" data-testid="panel-settings">
<div class="panel-header">
<h2>
<span class="icon-pill" style="background: var(--yellow);"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
Game settings
</h2>
<span class="badge">Skribbl Race</span>
</div>
<div class="settings-grid">
<div class="setting-item">
<div class="lbl">Rounds</div><div class="val">5</div>
</div>
<div class="setting-item">
<div class="lbl">Time / draw</div><div class="val">60s</div>
</div>
<div class="setting-item">
<div class="lbl">Max players</div><div class="val">8</div>
</div>
<div class="setting-item">
<div class="lbl">Language</div><div class="val">English</div>
</div>
</div>
<button class="start-btn" data-testid="btn-start">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Start Game
</button>
</section>
<section class="panel chat-panel" data-testid="panel-chat">
<div class="panel-header">
<h2>
<span class="icon-pill" style="background: var(--sky);"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span>
Lobby chat
</h2>
</div>
<div class="chat-messages">
<div class="msg system">
<div class="body" style="margin: 0 auto;">
<div class="text">Suki joined the room</div>
</div>
</div>
<div class="msg">
<div class="av" style="background: var(--yellow);">🦄</div>
<div class="body">
<div class="name">Mia</div>
<div class="text">heyyy welcome! waiting on Ravi to pick an avatar 🙃</div>
</div>
</div>
<div class="msg">
<div class="av" style="background: var(--sky);">🐸</div>
<div class="body">
<div class="name">Jules</div>
<div class="text">i call drawing first round &lt;3</div>
</div>
</div>
<div class="msg">
<div class="av" style="background: var(--coral);">🐱</div>
<div class="body">
<div class="name">Pat</div>
<div class="text">good luck with that lol</div>
</div>
</div>
<div class="msg system">
<div class="body" style="margin: 0 auto;">
<div class="text">Mia changed rounds to 5</div>
</div>
</div>
</div>
<div class="chat-input-row">
<input class="chat-input" type="text" placeholder="Say something..." data-testid="chat-input">
<button class="send-btn" data-testid="btn-send">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</section>
</div>
</main>
</body>
</html>
+371
View File
@@ -0,0 +1,371 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drawing - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; }
.game-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 22px; max-width: 1500px; margin: 0 auto;
}
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 20px; text-decoration: none; color: inherit; }
.logo-mark { width: 38px; height: 38px; border-radius: 12px; background: var(--coral); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
.header-pills { display: flex; gap: 10px; align-items: center; }
.pill-info { padding: 8px 16px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-weight: 700; font-size: 13px; box-shadow: 0 3px 0 rgba(0,0,0,0.1); display: inline-flex; align-items: center; gap: 6px; }
.pill-info.round { background: var(--lavender); color: white; }
.leave-btn { padding: 8px 14px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-family: inherit; font-weight: 700; font-size: 13px; cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,0.1); display: inline-flex; align-items: center; gap: 6px; }
main {
max-width: 1500px; margin: 0 auto; padding: 8px 22px 30px;
display: grid; grid-template-columns: 220px 1fr 280px; gap: 18px;
}
/* Sidebar (players/scoreboard) */
.panel { background: var(--white); border: 3px solid var(--dark); border-radius: 20px; padding: 16px; box-shadow: var(--shadow-card); }
.panel-title { font-size: 14px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: rgba(45,45,45,0.6); margin-bottom: 12px; padding-left: 4px; }
.scoreboard { display: flex; flex-direction: column; gap: 8px; }
.score-item {
display: flex; align-items: center; gap: 10px; padding: 8px 10px;
background: var(--cream); border-radius: 12px; border: 2px solid transparent;
}
.score-item.drawing { background: var(--yellow); border-color: var(--dark); position: relative; }
.score-item .rank { width: 22px; height: 22px; background: var(--white); border: 2px solid var(--dark); border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
.score-item.drawing .rank { background: var(--coral); color: white; }
.score-item .av { width: 32px; height: 32px; border-radius: 10px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 18px; flex-shrink: 0; }
.score-item .info { flex: 1; min-width: 0; }
.score-item .name { font-size: 13px; font-weight: 700; display: flex; align-items: center; gap: 4px; }
.score-item .pts { font-size: 11px; color: rgba(45,45,45,0.6); font-weight: 600; }
.score-item .pts strong { color: var(--coral); }
.score-item .pencil-icon { color: var(--dark); flex-shrink: 0; }
.score-item .guessed { color: var(--mint); font-weight: 700; font-size: 11px; }
/* Center (canvas area) */
.canvas-area { display: flex; flex-direction: column; gap: 14px; }
.canvas-top {
display: flex; align-items: center; justify-content: space-between; gap: 16px;
background: var(--white); border: 3px solid var(--dark); border-radius: 18px;
padding: 14px 18px; box-shadow: var(--shadow-card);
}
.timer-ring {
position: relative; width: 60px; height: 60px; flex-shrink: 0;
}
.timer-ring svg { width: 60px; height: 60px; transform: rotate(-90deg); }
.timer-ring .bg-track { fill: none; stroke: rgba(45,45,45,0.12); stroke-width: 6; }
.timer-ring .fg-track { fill: none; stroke: var(--coral); stroke-width: 6; stroke-linecap: round; stroke-dasharray: 165; stroke-dashoffset: 35; transition: stroke-dashoffset 1s linear; }
.timer-ring .label { position: absolute; inset: 0; display: grid; place-items: center; font-weight: 700; font-size: 16px; }
.word-hint { flex: 1; text-align: center; }
.word-hint .lbl { font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: rgba(45,45,45,0.55); margin-bottom: 4px; }
.word-letters { display: inline-flex; gap: 8px; justify-content: center; }
.letter {
width: 30px; height: 38px; background: var(--cream); border: 2px solid var(--dark);
border-radius: 8px; display: grid; place-items: center; font-weight: 700; font-size: 18px;
}
.letter.revealed { background: var(--yellow); }
.letter .underline { display: block; width: 18px; height: 3px; background: var(--dark); border-radius: 2px; }
.word-meta { font-size: 12px; font-weight: 600; color: rgba(45,45,45,0.6); margin-top: 4px; }
.drawing-canvas {
background: var(--white); border: 3px solid var(--dark); border-radius: 20px;
box-shadow: var(--shadow-card); padding: 16px;
display: flex; flex-direction: column; gap: 12px;
aspect-ratio: 16/10; min-height: 380px; position: relative;
}
.canvas-stage { flex: 1; background: var(--cream); border: 2px dashed rgba(45,45,45,0.18); border-radius: 14px; overflow: hidden; position: relative; display: grid; place-items: center; }
.you-drawing-tag {
position: absolute; top: 14px; left: 14px;
padding: 6px 12px; background: var(--coral); color: white; border: 2px solid var(--dark); border-radius: 999px;
font-weight: 700; font-size: 12px; box-shadow: 0 3px 0 rgba(0,0,0,0.15); z-index: 2;
display: inline-flex; align-items: center; gap: 6px;
}
/* Tool palette */
.toolbar {
display: flex; align-items: center; gap: 14px; padding: 12px;
background: var(--cream); border-radius: 14px;
flex-wrap: wrap; justify-content: center;
}
.color-row { display: flex; gap: 6px; flex-wrap: wrap; }
.color-swatch {
width: 28px; height: 28px; border-radius: 8px; border: 2px solid var(--dark);
cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15);
transition: transform 0.12s ease;
}
.color-swatch:hover { transform: translateY(-2px); }
.color-swatch.active { box-shadow: 0 0 0 3px var(--white), 0 0 0 5px var(--dark); transform: translateY(-2px); }
.divider { width: 2px; height: 28px; background: rgba(45,45,45,0.15); }
.tool-btn { width: 36px; height: 36px; border-radius: 10px; border: 2px solid var(--dark); background: var(--white); cursor: pointer; display: grid; place-items: center; box-shadow: 0 2px 0 rgba(0,0,0,0.15); transition: transform 0.12s ease; }
.tool-btn:hover { transform: translateY(-2px); }
.tool-btn.active { background: var(--yellow); }
.brush-sizes { display: flex; gap: 6px; align-items: center; }
.brush-size { width: 36px; height: 36px; border-radius: 10px; border: 2px solid var(--dark); background: var(--white); display: grid; place-items: center; cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15); }
.brush-size .dot { background: var(--dark); border-radius: 50%; }
.brush-size .dot.s1 { width: 4px; height: 4px; }
.brush-size .dot.s2 { width: 8px; height: 8px; }
.brush-size .dot.s3 { width: 14px; height: 14px; }
.brush-size.active { background: var(--mint); }
/* Right column — chat + guess */
.chat-panel { display: flex; flex-direction: column; height: 100%; min-height: 500px; }
.chat-messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding-right: 4px; }
.msg { padding: 8px 12px; border-radius: 12px; font-size: 13px; line-height: 1.4; }
.msg.regular { background: var(--cream); }
.msg.regular .name { font-weight: 700; color: var(--coral); margin-right: 6px; }
.msg.guessed { background: rgba(78,205,196,0.18); border: 2px dashed var(--mint); font-weight: 600; }
.msg.guessed .name { font-weight: 700; color: var(--dark); margin-right: 6px; }
.msg.guessed .check { display: inline-flex; gap: 4px; align-items: center; color: var(--mint); font-weight: 700; }
.msg.close { background: rgba(255,210,63,0.3); font-weight: 600; font-style: italic; }
.msg.system { background: transparent; text-align: center; font-style: italic; color: rgba(45,45,45,0.6); font-size: 12px; }
.guess-input-row { display: flex; gap: 6px; padding-top: 10px; border-top: 2px dashed rgba(45,45,45,0.15); margin-top: 10px; }
.guess-input { flex: 1; font-family: inherit; font-weight: 600; font-size: 14px; padding: 10px 14px; border: 2px solid var(--dark); border-radius: 999px; background: var(--cream); outline: none; }
.guess-input:focus { background: var(--white); border-color: var(--coral); }
.send-btn { width: 40px; height: 40px; background: var(--mint); border: 2px solid var(--dark); border-radius: 50%; cursor: pointer; display: grid; place-items: center; box-shadow: 0 3px 0 rgba(0,0,0,0.15); flex-shrink: 0; }
@media (max-width: 1100px) {
main { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="game-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg></div>
DrawTogether
</a>
<div class="header-pills">
<span class="pill-info round">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.8.97 6.5 2.5L21 8"/><polyline points="21 3 21 8 16 8"/></svg>
Round 3 / 5
</span>
<span class="pill-info">
<span style="width: 8px; height: 8px; background: var(--mint); border-radius: 50%;"></span>
MIA42K
</span>
<button class="leave-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
Leave
</button>
</div>
</header>
<main>
<!-- Left: scoreboard -->
<aside class="panel" data-testid="panel-scoreboard">
<div class="panel-title">Scoreboard</div>
<div class="scoreboard">
<div class="score-item">
<div class="rank">1</div>
<div class="av" style="background: var(--yellow);">🦄</div>
<div class="info">
<div class="name">Mia</div>
<div class="pts"><strong>1240</strong> pts</div>
</div>
</div>
<div class="score-item">
<div class="rank">2</div>
<div class="av" style="background: var(--mint);">🦊</div>
<div class="info">
<div class="name">Suki</div>
<div class="pts guessed">+120 just now!</div>
</div>
</div>
<div class="score-item drawing">
<div class="rank">3</div>
<div class="av" style="background: var(--white);">🐼</div>
<div class="info">
<div class="name">Ravi</div>
<div class="pts"><strong>820</strong> pts · drawing</div>
</div>
<div class="pencil-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
</div>
<div class="score-item">
<div class="rank">4</div>
<div class="av" style="background: var(--sky);">🐸</div>
<div class="info">
<div class="name">Jules</div>
<div class="pts"><strong>740</strong> pts</div>
</div>
</div>
<div class="score-item">
<div class="rank">5</div>
<div class="av" style="background: var(--coral);">🐱</div>
<div class="info">
<div class="name">Pat</div>
<div class="pts">guessing...</div>
</div>
</div>
</div>
</aside>
<!-- Center: canvas -->
<section class="canvas-area">
<div class="canvas-top" data-testid="canvas-top">
<div class="timer-ring" data-testid="timer">
<svg viewBox="0 0 60 60">
<circle class="bg-track" cx="30" cy="30" r="26"/>
<circle class="fg-track" cx="30" cy="30" r="26"/>
</svg>
<div class="label">47s</div>
</div>
<div class="word-hint">
<div class="lbl">Word hint</div>
<div class="word-letters" data-testid="word-hint">
<div class="letter"><span class="underline"></span></div>
<div class="letter"><span class="underline"></span></div>
<div class="letter revealed">A</div>
<div class="letter"><span class="underline"></span></div>
<div class="letter"><span class="underline"></span></div>
<div class="letter"><span class="underline"></span></div>
</div>
<div class="word-meta">6 letters · animal</div>
</div>
<div class="pill-info" style="background: var(--mint);">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
Ravi drawing
</div>
</div>
<div class="drawing-canvas" data-testid="canvas">
<div class="canvas-stage">
<!-- sample mid-drawing: cat -->
<svg viewBox="0 0 600 360" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" style="max-width: 600px;">
<!-- body sketch -->
<ellipse cx="300" cy="240" rx="135" ry="75" fill="#FFD23F" stroke="#2D2D2D" stroke-width="5"/>
<!-- head -->
<circle cx="300" cy="155" r="92" fill="#FFD23F" stroke="#2D2D2D" stroke-width="5"/>
<!-- ears -->
<polygon points="225,90 215,30 280,80" fill="#FFD23F" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/>
<polygon points="375,90 385,30 320,80" fill="#FFD23F" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/>
<!-- inner ears -->
<polygon points="232,82 233,55 265,78" fill="#FF5C5C"/>
<polygon points="368,82 367,55 335,78" fill="#FF5C5C"/>
<!-- eyes -->
<circle cx="270" cy="150" r="11" fill="#2D2D2D"/>
<circle cx="330" cy="150" r="11" fill="#2D2D2D"/>
<circle cx="274" cy="146" r="4" fill="white"/>
<circle cx="334" cy="146" r="4" fill="white"/>
<!-- nose + mouth -->
<path d="M291 178 Q300 188 309 178" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3" stroke-linejoin="round"/>
<path d="M291 184 Q283 200 295 206" stroke="#2D2D2D" stroke-width="4" fill="none" stroke-linecap="round"/>
<path d="M309 184 Q317 200 305 206" stroke="#2D2D2D" stroke-width="4" fill="none" stroke-linecap="round"/>
<!-- whiskers -->
<line x1="225" y1="165" x2="180" y2="160" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<line x1="225" y1="173" x2="180" y2="178" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<line x1="375" y1="165" x2="420" y2="160" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<line x1="375" y1="173" x2="420" y2="178" stroke="#2D2D2D" stroke-width="3" stroke-linecap="round"/>
<!-- belly -->
<ellipse cx="300" cy="265" rx="80" ry="40" fill="#FFF8E7" opacity="0.6"/>
<!-- tail -->
<path d="M430 220 Q500 200 480 145" stroke="#2D2D2D" stroke-width="5" fill="#FFD23F" stroke-linecap="round"/>
<!-- in-progress pencil cursor -->
<g transform="translate(450,290) rotate(40)">
<rect x="0" y="0" width="42" height="12" rx="3" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3"/>
<polygon points="42,0 52,6 42,12" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3" stroke-linejoin="round"/>
</g>
</svg>
</div>
<!-- Toolbar (drawer view) -->
<div class="toolbar" data-testid="toolbar">
<div class="color-row">
<div class="color-swatch" style="background: #2D2D2D;"></div>
<div class="color-swatch active" style="background: #FFD23F;"></div>
<div class="color-swatch" style="background: #FF5C5C;"></div>
<div class="color-swatch" style="background: #4ECDC4;"></div>
<div class="color-swatch" style="background: #5BCEFA;"></div>
<div class="color-swatch" style="background: #A593E0;"></div>
<div class="color-swatch" style="background: #ffffff;"></div>
<div class="color-swatch" style="background: #6BCB77;"></div>
<div class="color-swatch" style="background: #FF8C42;"></div>
<div class="color-swatch" style="background: #B5651D;"></div>
</div>
<div class="divider"></div>
<div class="brush-sizes">
<div class="brush-size"><span class="dot s1"></span></div>
<div class="brush-size active"><span class="dot s2"></span></div>
<div class="brush-size"><span class="dot s3"></span></div>
</div>
<div class="divider"></div>
<button class="tool-btn active" title="Brush">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</button>
<button class="tool-btn" title="Eraser">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 20H7L3 16a2 2 0 0 1 0-3l9-9a2 2 0 0 1 3 0l6 6a2 2 0 0 1 0 3l-7 7"/></svg>
</button>
<button class="tool-btn" title="Bucket">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M19 11l-7-7-9 9 7 7 9-9z"/><path d="M5 13l4 4M19 11c1 0 2 1 2 3a3 3 0 1 1-6 0c0-1 1-3 1-3"/></svg>
</button>
<button class="tool-btn" title="Undo">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M3 7v6h6M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
</button>
<button class="tool-btn" title="Clear">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"/></svg>
</button>
</div>
</div>
</section>
<!-- Right: chat / guesses -->
<aside class="panel chat-panel" data-testid="panel-chat">
<div class="panel-title">Guesses</div>
<div class="chat-messages">
<div class="msg system">— Round 3 started —</div>
<div class="msg regular">
<span class="name">Pat:</span> dog?
</div>
<div class="msg regular">
<span class="name">Jules:</span> hamster
</div>
<div class="msg close">
<span style="color: var(--coral); font-weight:700;">Suki:</span> kitten — close!
</div>
<div class="msg regular">
<span class="name">Pat:</span> lion?
</div>
<div class="msg guessed">
<span class="name">Mia</span> guessed it!
<span class="check"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg> +180</span>
</div>
<div class="msg regular">
<span class="name">Pat:</span> tiger
</div>
<div class="msg guessed">
<span class="name">Suki</span> guessed it!
<span class="check"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg> +120</span>
</div>
<div class="msg regular">
<span class="name">Jules:</span> kitty cat??
</div>
<div class="msg system">Pat is typing...</div>
</div>
<div class="guess-input-row">
<input class="guess-input" type="text" placeholder="Type your guess..." data-testid="guess-input">
<button class="send-btn" data-testid="btn-guess">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</aside>
</main>
</body>
</html>
+304
View File
@@ -0,0 +1,304 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gartic Phone - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; }
.game-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 22px; max-width: 1500px; margin: 0 auto; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 20px; text-decoration: none; color: inherit; }
.logo-mark { width: 38px; height: 38px; border-radius: 12px; background: var(--coral); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
.header-pills { display: flex; gap: 10px; align-items: center; }
.pill-info { padding: 8px 16px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-weight: 700; font-size: 13px; box-shadow: 0 3px 0 rgba(0,0,0,0.1); display: inline-flex; align-items: center; gap: 6px; }
.pill-info.round { background: var(--mint); color: var(--dark); }
.pill-info.timer { background: var(--coral); color: white; }
.leave-btn { padding: 8px 14px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-family: inherit; font-weight: 700; font-size: 13px; cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,0.1); display: inline-flex; align-items: center; gap: 6px; }
.stage-banner {
max-width: 1500px; margin: 0 auto; padding: 0 22px 14px;
}
.banner-inner {
background: var(--white); border: 3px solid var(--dark); border-radius: 18px; padding: 14px 22px;
box-shadow: var(--shadow-card); display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.banner-inner h1 { font-size: 22px; font-weight: 700; }
.banner-inner .sub { font-weight: 600; font-size: 14px; color: rgba(45,45,45,0.65); margin-top: 2px; }
.timer-ring { position: relative; width: 64px; height: 64px; flex-shrink: 0; }
.timer-ring svg { width: 64px; height: 64px; transform: rotate(-90deg); }
.timer-ring .bg-track { fill: none; stroke: rgba(45,45,45,0.12); stroke-width: 7; }
.timer-ring .fg-track { fill: none; stroke: var(--coral); stroke-width: 7; stroke-linecap: round; stroke-dasharray: 175; stroke-dashoffset: 60; }
.timer-ring .label { position: absolute; inset: 0; display: grid; place-items: center; font-weight: 700; font-size: 18px; }
/* Stage chips */
.stage-chips { display: flex; gap: 6px; align-items: center; }
.chip {
width: 36px; height: 36px; border-radius: 10px; border: 2px solid var(--dark);
display: grid; place-items: center; font-weight: 700; font-size: 13px;
background: var(--cream);
}
.chip.done { background: var(--mint); color: white; }
.chip.active { background: var(--yellow); transform: translateY(-2px); box-shadow: 0 4px 0 rgba(0,0,0,0.15); }
.chip-line { width: 14px; height: 3px; background: rgba(45,45,45,0.25); border-radius: 2px; }
.chip-line.done { background: var(--mint); }
main {
max-width: 1500px; margin: 0 auto; padding: 0 22px 30px;
display: grid; grid-template-columns: 1fr 280px; gap: 18px;
}
/* Prompt card */
.prompt-card {
background: var(--lavender); border: 3px solid var(--dark); border-radius: 18px;
padding: 18px 24px; box-shadow: var(--shadow-card); margin-bottom: 14px;
display: flex; align-items: center; gap: 16px;
}
.prompt-card .av-from {
width: 50px; height: 50px; border-radius: 14px; border: 3px solid var(--dark);
background: var(--yellow); display: grid; place-items: center; font-size: 26px; flex-shrink: 0;
transform: rotate(-4deg); box-shadow: 0 4px 0 rgba(0,0,0,0.15);
}
.prompt-card .meta { color: rgba(255,255,255,0.85); font-size: 12px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; }
.prompt-card .prompt-text { font-size: 22px; font-weight: 700; color: white; margin-top: 4px; line-height: 1.2; }
.prompt-card .prompt-text .quote { color: var(--yellow); }
.canvas-wrap { background: var(--white); border: 3px solid var(--dark); border-radius: 20px; box-shadow: var(--shadow-card); padding: 18px; display: flex; flex-direction: column; gap: 14px; }
.canvas-stage {
aspect-ratio: 16/10; min-height: 420px; background: var(--cream);
border: 2px dashed rgba(45,45,45,0.18); border-radius: 14px; position: relative; overflow: hidden;
display: grid; place-items: center;
}
/* Toolbar */
.toolbar { display: flex; align-items: center; gap: 12px; padding: 12px; background: var(--cream); border-radius: 14px; flex-wrap: wrap; justify-content: space-between; }
.tool-section { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.color-row { display: flex; gap: 5px; flex-wrap: wrap; }
.color-swatch { width: 26px; height: 26px; border-radius: 8px; border: 2px solid var(--dark); cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15); transition: transform 0.12s ease; }
.color-swatch:hover { transform: translateY(-2px); }
.color-swatch.active { box-shadow: 0 0 0 3px var(--white), 0 0 0 5px var(--dark); transform: translateY(-2px); }
.divider { width: 2px; height: 26px; background: rgba(45,45,45,0.15); }
.tool-btn { width: 36px; height: 36px; border-radius: 10px; border: 2px solid var(--dark); background: var(--white); cursor: pointer; display: grid; place-items: center; box-shadow: 0 2px 0 rgba(0,0,0,0.15); transition: transform 0.12s ease; }
.tool-btn:hover { transform: translateY(-2px); }
.tool-btn.active { background: var(--yellow); }
.brush-size { width: 32px; height: 32px; border-radius: 10px; border: 2px solid var(--dark); background: var(--white); display: grid; place-items: center; cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15); }
.brush-size .dot { background: var(--dark); border-radius: 50%; }
.brush-size .dot.s1 { width: 4px; height: 4px; }
.brush-size .dot.s2 { width: 8px; height: 8px; }
.brush-size .dot.s3 { width: 14px; height: 14px; }
.brush-size.active { background: var(--mint); }
.submit-btn {
font-family: inherit; font-weight: 700; font-size: 15px; padding: 10px 22px;
border-radius: 999px; border: 2px solid var(--dark);
background: var(--coral); color: white; cursor: pointer;
box-shadow: 0 4px 0 rgba(0,0,0,0.18); display: inline-flex; align-items: center; gap: 8px;
}
.submit-btn:hover { transform: translateY(-2px); }
/* Right column */
.panel { background: var(--white); border: 3px solid var(--dark); border-radius: 20px; padding: 16px; box-shadow: var(--shadow-card); }
.panel-title { font-size: 13px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: rgba(45,45,45,0.6); margin-bottom: 12px; padding-left: 4px; }
.progress-summary { text-align: center; padding: 20px 8px; }
.progress-summary .big { font-size: 42px; font-weight: 700; line-height: 1; }
.progress-summary .big strong { color: var(--coral); }
.progress-summary .lbl { font-weight: 600; font-size: 14px; color: rgba(45,45,45,0.65); margin-top: 4px; }
.player-status { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.ps-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; background: var(--cream); }
.ps-item.you { background: rgba(255,210,63,0.35); }
.ps-item .av { width: 32px; height: 32px; border-radius: 10px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 18px; flex-shrink: 0; }
.ps-item .info { flex: 1; min-width: 0; }
.ps-item .name { font-weight: 700; font-size: 13px; }
.ps-item .stat { font-size: 11px; font-weight: 600; color: rgba(45,45,45,0.6); }
.ps-item .pulse { width: 10px; height: 10px; background: var(--mint); border-radius: 50%; box-shadow: 0 0 0 4px rgba(78,205,196,0.25); }
.ps-item.done .pulse { background: var(--mint); box-shadow: none; }
.ps-item.drawing .pulse { background: var(--coral); animation: pulse 1.4s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(255,92,92,0.4);} 50%{ box-shadow: 0 0 0 6px rgba(255,92,92,0);} }
.progress-bar-wrap { padding: 0 4px; }
.progress-bar { background: var(--cream); border: 2px solid var(--dark); border-radius: 999px; height: 14px; overflow: hidden; }
.progress-bar .fill { height: 100%; background: var(--mint); border-right: 2px solid var(--dark); width: 40%; border-radius: 999px 0 0 999px; }
.tip {
background: rgba(91,206,250,0.18); border: 2px dashed var(--sky);
padding: 12px; border-radius: 12px; margin-top: 14px; font-size: 12px; font-weight: 500; line-height: 1.45;
}
.tip strong { color: var(--dark); }
@media (max-width: 1100px) { main { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<header class="game-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
DrawTogether
</a>
<div class="header-pills">
<span class="pill-info round">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.8.97 6.5 2.5L21 8"/><polyline points="21 3 21 8 16 8"/></svg>
Round 3 / 5
</span>
<span class="pill-info">MIA42K</span>
<button class="leave-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
Leave
</button>
</div>
</header>
<section class="stage-banner">
<div class="banner-inner">
<div>
<h1>Your turn to draw!</h1>
<div class="sub">Sketch what Mia wrote — pass it on when the timer ends.</div>
</div>
<div class="stage-chips" data-testid="stage-chips" aria-label="Round stages">
<div class="chip done"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round"><path d="M5 12l5 5L20 7"/></svg></div>
<div class="chip-line done"></div>
<div class="chip active">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
</div>
<div class="chip-line"></div>
<div class="chip">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
</div>
<div class="timer-ring" data-testid="timer">
<svg viewBox="0 0 64 64">
<circle class="bg-track" cx="32" cy="32" r="28"/>
<circle class="fg-track" cx="32" cy="32" r="28"/>
</svg>
<div class="label">60s</div>
</div>
</div>
</section>
<main>
<section>
<div class="prompt-card" data-testid="prompt-card">
<div class="av-from">🦄</div>
<div>
<div class="meta">Mia wrote</div>
<div class="prompt-text"><span class="quote">"</span>a cat riding a unicorn<span class="quote">"</span></div>
</div>
</div>
<div class="canvas-wrap">
<div class="canvas-stage" data-testid="canvas">
<!-- empty canvas illustration: simple pencil placeholder -->
<svg width="100%" height="100%" viewBox="0 0 600 380" preserveAspectRatio="xMidYMid meet">
<!-- subtle grid -->
<defs>
<pattern id="grid" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="rgba(45,45,45,0.04)" stroke-width="1"/>
</pattern>
</defs>
<rect width="600" height="380" fill="url(#grid)"/>
<!-- starter doodle: a few sketch lines + pencil -->
<g opacity="0.7">
<path d="M180 230 Q230 180 290 220" stroke="#A593E0" stroke-width="5" fill="none" stroke-linecap="round"/>
<path d="M290 220 Q350 250 410 200" stroke="#A593E0" stroke-width="5" fill="none" stroke-linecap="round"/>
<circle cx="240" cy="180" r="14" fill="none" stroke="#A593E0" stroke-width="4"/>
</g>
<g transform="translate(420,260) rotate(45)">
<rect x="0" y="0" width="58" height="14" rx="3" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3"/>
<rect x="58" y="0" width="14" height="14" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3"/>
<polygon points="72,0 84,7 72,14" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3" stroke-linejoin="round"/>
<polygon points="84,7 90,5 90,9" fill="#2D2D2D"/>
</g>
<text x="300" y="340" text-anchor="middle" font-family="Fredoka, sans-serif" font-size="14" font-weight="600" fill="rgba(45,45,45,0.4)">Click and drag to draw</text>
</svg>
</div>
<div class="toolbar" data-testid="toolbar">
<div class="tool-section">
<div class="color-row">
<div class="color-swatch" style="background: #2D2D2D;"></div>
<div class="color-swatch active" style="background: #A593E0;"></div>
<div class="color-swatch" style="background: #FF5C5C;"></div>
<div class="color-swatch" style="background: #4ECDC4;"></div>
<div class="color-swatch" style="background: #FFD23F;"></div>
<div class="color-swatch" style="background: #5BCEFA;"></div>
<div class="color-swatch" style="background: #ffffff;"></div>
<div class="color-swatch" style="background: #6BCB77;"></div>
<div class="color-swatch" style="background: #FF8C42;"></div>
</div>
<div class="divider"></div>
<div class="brush-size"><span class="dot s1"></span></div>
<div class="brush-size active"><span class="dot s2"></span></div>
<div class="brush-size"><span class="dot s3"></span></div>
<div class="divider"></div>
<button class="tool-btn active" title="Brush"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg></button>
<button class="tool-btn" title="Eraser"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 20H7L3 16a2 2 0 0 1 0-3l9-9a2 2 0 0 1 3 0l6 6a2 2 0 0 1 0 3l-7 7"/></svg></button>
<button class="tool-btn" title="Undo"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M3 7v6h6M21 17a9 9 0 0 0-15-6.7L3 13"/></svg></button>
<button class="tool-btn" title="Clear"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<button class="submit-btn" data-testid="btn-submit-drawing">
Submit drawing
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</section>
<aside class="panel" data-testid="panel-status">
<div class="panel-title">Round progress</div>
<div class="progress-summary">
<div class="big">waiting on <strong>3</strong> / 5</div>
<div class="lbl">players still working</div>
</div>
<div class="progress-bar-wrap">
<div class="progress-bar"><div class="fill"></div></div>
</div>
<div class="player-status">
<div class="ps-item done">
<div class="av" style="background: var(--yellow);">🦄</div>
<div class="info"><div class="name">Mia</div><div class="stat">submitted prompt</div></div>
<div class="pulse"></div>
</div>
<div class="ps-item done">
<div class="av" style="background: var(--coral);">🐱</div>
<div class="info"><div class="name">Pat</div><div class="stat">submitted drawing</div></div>
<div class="pulse"></div>
</div>
<div class="ps-item drawing you">
<div class="av" style="background: var(--mint);">🦊</div>
<div class="info"><div class="name">Suki (you)</div><div class="stat">drawing now...</div></div>
<div class="pulse"></div>
</div>
<div class="ps-item drawing">
<div class="av" style="background: var(--lavender);">🐼</div>
<div class="info"><div class="name">Ravi</div><div class="stat">drawing now...</div></div>
<div class="pulse"></div>
</div>
<div class="ps-item drawing">
<div class="av" style="background: var(--sky);">🐸</div>
<div class="info"><div class="name">Jules</div><div class="stat">drawing now...</div></div>
<div class="pulse"></div>
</div>
</div>
<div class="tip">
<strong>Tip:</strong> No one sees your drawing until everyone submits — it gets passed to the next player to guess what it is.
</div>
</aside>
</main>
</body>
</html>
+367
View File
@@ -0,0 +1,367 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color Together - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; }
.game-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 22px; max-width: 1500px; margin: 0 auto;
}
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 20px; text-decoration: none; color: inherit; }
.logo-mark { width: 38px; height: 38px; border-radius: 12px; background: var(--lavender); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
.header-pills { display: flex; gap: 10px; align-items: center; }
.pill-info { padding: 8px 16px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-weight: 700; font-size: 13px; box-shadow: 0 3px 0 rgba(0,0,0,0.1); display: inline-flex; align-items: center; gap: 6px; }
.pill-info.online { background: var(--mint); }
.leave-btn { padding: 8px 14px; background: var(--white); border: 2px solid var(--dark); border-radius: 999px; font-family: inherit; font-weight: 700; font-size: 13px; cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,0.1); display: inline-flex; align-items: center; gap: 6px; }
/* Big mode banner */
.mode-banner { max-width: 1500px; margin: 0 auto; padding: 0 22px 14px; }
.mode-banner-inner {
background: var(--white); border: 3px solid var(--dark); border-radius: 18px; padding: 14px 22px;
box-shadow: var(--shadow-card); display: flex; align-items: center; justify-content: space-between; gap: 16px;
}
.mode-banner-inner h1 { font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
.mode-banner-inner h1 .ic { width: 34px; height: 34px; border-radius: 10px; background: var(--lavender); border: 2px solid var(--dark); display: grid; place-items: center; }
.mode-banner-inner .sub { font-weight: 600; font-size: 14px; color: rgba(45,45,45,0.65); margin-top: 2px; padding-left: 44px; }
.header-actions { display: flex; gap: 8px; }
.action-btn {
font-family: inherit; font-weight: 700; font-size: 14px; padding: 10px 18px;
border-radius: 999px; border: 2px solid var(--dark); cursor: pointer;
box-shadow: 0 4px 0 rgba(0,0,0,0.15); display: inline-flex; align-items: center; gap: 6px;
transition: transform 0.15s ease;
}
.action-btn:hover { transform: translateY(-2px); }
.action-btn.primary { background: var(--coral); color: white; }
.action-btn.secondary { background: var(--white); }
main {
max-width: 1500px; margin: 0 auto; padding: 0 22px 30px;
display: grid; grid-template-columns: 240px 1fr 240px; gap: 18px;
}
.panel { background: var(--white); border: 3px solid var(--dark); border-radius: 20px; padding: 16px; box-shadow: var(--shadow-card); }
.panel-title { font-size: 13px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: rgba(45,45,45,0.6); margin-bottom: 12px; padding-left: 4px; }
/* Tools panel (left) */
.tool-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.tool-cell {
background: var(--cream); border: 2px solid var(--dark); border-radius: 14px;
padding: 12px 8px; display: flex; flex-direction: column; align-items: center; gap: 6px;
cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,0.15);
transition: transform 0.15s ease;
}
.tool-cell:hover { transform: translateY(-2px); }
.tool-cell.active { background: var(--yellow); }
.tool-cell .lbl { font-size: 11px; font-weight: 700; }
.brush-section { margin-top: 14px; }
.brush-section .brush-row { display: flex; gap: 8px; align-items: center; padding: 8px 4px; }
.brush-size { width: 36px; height: 36px; border-radius: 10px; border: 2px solid var(--dark); background: var(--cream); display: grid; place-items: center; cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15); }
.brush-size .dot { background: var(--dark); border-radius: 50%; }
.brush-size .dot.s1 { width: 5px; height: 5px; }
.brush-size .dot.s2 { width: 9px; height: 9px; }
.brush-size .dot.s3 { width: 14px; height: 14px; }
.brush-size .dot.s4 { width: 18px; height: 18px; }
.brush-size.active { background: var(--mint); }
/* Center: canvas */
.canvas-wrap {
background: var(--white); border: 3px solid var(--dark); border-radius: 20px; box-shadow: var(--shadow-card);
padding: 18px; display: flex; flex-direction: column; gap: 12px; min-height: 600px;
}
.canvas-stage {
flex: 1; background: var(--cream); border: 2px dashed rgba(45,45,45,0.18);
border-radius: 14px; position: relative; overflow: hidden; display: grid; place-items: center;
min-height: 540px;
}
.presence-cursor {
position: absolute; pointer-events: none; z-index: 5;
display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
}
.presence-cursor .arrow { width: 18px; height: 18px; }
.presence-cursor .tag {
padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 700;
border: 2px solid var(--dark); color: white; box-shadow: 0 2px 0 rgba(0,0,0,0.15);
margin-left: 12px;
}
.pc1 { top: 110px; left: 30%; }
.pc1 .tag { background: var(--coral); }
.pc2 { top: 280px; right: 22%; }
.pc2 .tag { background: var(--mint); color: var(--dark); }
.pc3 { bottom: 100px; left: 22%; }
.pc3 .tag { background: var(--lavender); }
/* Right: color picker / palette / chat */
.hsl-wheel { width: 100%; aspect-ratio: 1; border-radius: 50%; position: relative; border: 3px solid var(--dark); box-shadow: var(--shadow-card);
background: conic-gradient(from 0deg,
hsl(0,80%,55%), hsl(30,80%,55%), hsl(60,80%,55%), hsl(90,80%,55%),
hsl(120,80%,55%), hsl(150,80%,55%), hsl(180,80%,55%), hsl(210,80%,55%),
hsl(240,80%,55%), hsl(270,80%,55%), hsl(300,80%,55%), hsl(330,80%,55%), hsl(360,80%,55%));
margin-bottom: 14px;
}
.hsl-wheel::after {
content: ''; position: absolute; inset: 12px; border-radius: 50%;
background: radial-gradient(circle at center, white, transparent 70%);
pointer-events: none;
}
.hsl-wheel .marker {
position: absolute; width: 22px; height: 22px; border-radius: 50%;
background: var(--coral); border: 3px solid var(--white); box-shadow: 0 0 0 2px var(--dark);
top: 18%; right: 22%;
}
.selected-color {
display: flex; align-items: center; gap: 10px; padding: 10px;
background: var(--cream); border-radius: 12px; border: 2px solid var(--dark);
}
.selected-color .swatch {
width: 40px; height: 40px; border-radius: 10px; background: var(--coral);
border: 2px solid var(--dark); box-shadow: 0 2px 0 rgba(0,0,0,0.15);
}
.selected-color .info { flex: 1; }
.selected-color .name { font-weight: 700; font-size: 14px; }
.selected-color .hex { font-size: 12px; font-weight: 600; color: rgba(45,45,45,0.6); font-family: monospace; }
.recent-row { display: flex; gap: 6px; flex-wrap: wrap; padding: 8px 0; }
.recent-swatch {
width: 28px; height: 28px; border-radius: 8px; border: 2px solid var(--dark);
cursor: pointer; box-shadow: 0 2px 0 rgba(0,0,0,0.15);
}
.who-online { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.who-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--cream); border-radius: 10px; }
.who-row .av { width: 28px; height: 28px; border-radius: 8px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 16px; }
.who-row .name { font-weight: 700; font-size: 13px; flex: 1; }
.who-row .dot { width: 8px; height: 8px; background: var(--mint); border-radius: 50%; }
@media (max-width: 1100px) {
main { grid-template-columns: 1fr; }
.canvas-wrap { min-height: 480px; }
}
</style>
</head>
<body>
<header class="game-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/><path d="M12 22a10 10 0 1 1 10-10c0 4-3 4-4 4h-3a2 2 0 0 0-1 4 2 2 0 0 1-1 4z"/></svg></div>
DrawTogether
</a>
<div class="header-pills">
<span class="pill-info online">
<span style="width: 8px; height: 8px; background: var(--dark); border-radius: 50%;"></span>
4 online
</span>
<span class="pill-info">MIA42K</span>
<button class="leave-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
Leave
</button>
</div>
</header>
<section class="mode-banner">
<div class="mode-banner-inner">
<div>
<h1><span class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/></svg></span> Color Together — 4 players online</h1>
<div class="sub">Mandala canvas · auto-saving every 30s</div>
</div>
<div class="header-actions">
<button class="action-btn secondary" data-testid="btn-reset">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M3 7v6h6M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
Reset
</button>
<button class="action-btn primary" data-testid="btn-snapshot">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M19 21H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.5l2-3h6l2 3H21a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2z"/><circle cx="12" cy="13" r="4"/></svg>
Save Snapshot
</button>
</div>
</div>
</section>
<main>
<!-- Tools (left) -->
<aside class="panel" data-testid="panel-tools">
<div class="panel-title">Tools</div>
<div class="tool-grid">
<div class="tool-cell active" data-testid="tool-brush">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
<span class="lbl">BRUSH</span>
</div>
<div class="tool-cell" data-testid="tool-bucket">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M19 11l-7-7-9 9 7 7 9-9z"/><path d="M5 13l4 4M19 11c1 0 2 1 2 3a3 3 0 1 1-6 0c0-1 1-3 1-3"/></svg>
<span class="lbl">FILL</span>
</div>
<div class="tool-cell" data-testid="tool-eraser">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M20 20H7L3 16a2 2 0 0 1 0-3l9-9a2 2 0 0 1 3 0l6 6a2 2 0 0 1 0 3l-7 7"/></svg>
<span class="lbl">ERASER</span>
</div>
<div class="tool-cell" data-testid="tool-picker">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M2 22l4-4M19 5l-9 9 5 5 9-9-5-5z"/><path d="M19 5l3-3"/></svg>
<span class="lbl">PICKER</span>
</div>
</div>
<div class="brush-section">
<div class="panel-title" style="margin-bottom: 6px;">Brush size</div>
<div class="brush-row">
<div class="brush-size"><span class="dot s1"></span></div>
<div class="brush-size active"><span class="dot s2"></span></div>
<div class="brush-size"><span class="dot s3"></span></div>
<div class="brush-size"><span class="dot s4"></span></div>
</div>
</div>
<div class="brush-section">
<div class="panel-title" style="margin-bottom: 6px;">History</div>
<div style="display: flex; gap: 8px;">
<button class="action-btn secondary" style="padding: 8px 12px; flex: 1; justify-content: center;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M3 7v6h6M21 17a9 9 0 0 0-15-6.7L3 13"/></svg>
Undo
</button>
<button class="action-btn secondary" style="padding: 8px 12px; flex: 1; justify-content: center;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 7v6h-6M3 17a9 9 0 0 1 15-6.7L21 13"/></svg>
Redo
</button>
</div>
</div>
</aside>
<!-- Canvas -->
<section class="canvas-wrap">
<div class="canvas-stage" data-testid="canvas">
<!-- Mandala SVG -->
<svg viewBox="0 0 600 600" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" style="max-width: 580px;">
<defs>
<radialGradient id="centerGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#FFD23F"/>
<stop offset="100%" stop-color="#FF8C42"/>
</radialGradient>
</defs>
<!-- outer ring -->
<circle cx="300" cy="300" r="270" fill="none" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="220" fill="none" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="160" fill="rgba(165,147,224,0.5)" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="100" fill="rgba(91,206,250,0.65)" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="50" fill="url(#centerGrad)" stroke="#2D2D2D" stroke-width="3"/>
<!-- 8 petals outer -->
<g>
<g transform="translate(300 300)">
<g id="petal-set">
<path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="rgba(255,92,92,0.65)" stroke="#2D2D2D" stroke-width="3"/>
</g>
</g>
<g transform="translate(300 300) rotate(45)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="rgba(255,210,63,0.7)" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(90)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="none" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(135)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="rgba(78,205,196,0.65)" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(180)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="rgba(255,92,92,0.65)" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(225)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="none" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(270)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="rgba(255,210,63,0.7)" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(315)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -160 0 -270 Z" fill="none" stroke="#2D2D2D" stroke-width="3"/></g>
</g>
<!-- inner petals -->
<g>
<g transform="translate(300 300) rotate(22.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="rgba(165,147,224,0.7)" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(67.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="none" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(112.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="rgba(255,140,66,0.6)" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(157.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="none" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(202.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="rgba(165,147,224,0.7)" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(247.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="none" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(292.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="rgba(255,140,66,0.6)" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(337.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="none" stroke="#2D2D2D" stroke-width="2.5"/></g>
</g>
<!-- center detail -->
<circle cx="300" cy="300" r="22" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="2.5"/>
<g>
<circle cx="300" cy="270" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="330" cy="300" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="300" cy="330" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="270" cy="300" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
</g>
</svg>
<!-- presence cursors -->
<div class="presence-cursor pc1">
<svg class="arrow" viewBox="0 0 24 24" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="2"><path d="M3 3l7 18 2-9 9-2z"/></svg>
<span class="tag">Mia</span>
</div>
<div class="presence-cursor pc2">
<svg class="arrow" viewBox="0 0 24 24" fill="#4ECDC4" stroke="#2D2D2D" stroke-width="2"><path d="M3 3l7 18 2-9 9-2z"/></svg>
<span class="tag">Ravi</span>
</div>
<div class="presence-cursor pc3">
<svg class="arrow" viewBox="0 0 24 24" fill="#A593E0" stroke="#2D2D2D" stroke-width="2"><path d="M3 3l7 18 2-9 9-2z"/></svg>
<span class="tag">Jules</span>
</div>
</div>
</section>
<!-- Right: color picker -->
<aside class="panel" data-testid="panel-colors">
<div class="panel-title">Color picker</div>
<div class="hsl-wheel" data-testid="hsl-wheel">
<div class="marker"></div>
</div>
<div class="selected-color">
<div class="swatch"></div>
<div class="info">
<div class="name">Coral red</div>
<div class="hex">#FF5C5C</div>
</div>
</div>
<div class="panel-title" style="margin-top: 16px; margin-bottom: 4px;">Recent</div>
<div class="recent-row">
<div class="recent-swatch" style="background: #FF5C5C;"></div>
<div class="recent-swatch" style="background: #FFD23F;"></div>
<div class="recent-swatch" style="background: #4ECDC4;"></div>
<div class="recent-swatch" style="background: #A593E0;"></div>
<div class="recent-swatch" style="background: #FF8C42;"></div>
<div class="recent-swatch" style="background: #5BCEFA;"></div>
<div class="recent-swatch" style="background: #6BCB77;"></div>
<div class="recent-swatch" style="background: #2D2D2D;"></div>
</div>
<div class="panel-title" style="margin-top: 18px; margin-bottom: 4px;">Drawing now</div>
<div class="who-online">
<div class="who-row">
<div class="av" style="background: var(--yellow);">🦄</div>
<div class="name">Mia</div>
<div class="dot"></div>
</div>
<div class="who-row">
<div class="av" style="background: var(--coral);">🦊</div>
<div class="name">Suki (you)</div>
<div class="dot"></div>
</div>
<div class="who-row">
<div class="av" style="background: var(--lavender);">🐼</div>
<div class="name">Ravi</div>
<div class="dot"></div>
</div>
<div class="who-row">
<div class="av" style="background: var(--sky);">🐸</div>
<div class="name">Jules</div>
<div class="dot"></div>
</div>
</div>
</aside>
</main>
</body>
</html>
+427
View File
@@ -0,0 +1,427 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Game Over - DrawTogether</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--yellow: #FFD23F; --coral: #FF5C5C; --mint: #4ECDC4;
--lavender: #A593E0; --sky: #5BCEFA; --dark: #2D2D2D;
--cream: #FFF8E7; --white: #FFFFFF;
--shadow-card: 0 6px 0 rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08);
--shadow-btn: 0 5px 0 rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12);
}
html, body { font-family: 'Fredoka', system-ui, sans-serif; color: var(--dark); background: var(--cream); -webkit-font-smoothing: antialiased; }
body { min-height: 100vh; position: relative; overflow-x: hidden; }
/* confetti */
.confetti { position: absolute; pointer-events: none; }
.c1 { top: 80px; left: 8%; animation: float 4s ease-in-out infinite; }
.c2 { top: 140px; right: 10%; animation: float 4s ease-in-out infinite 1s; }
.c3 { top: 60px; left: 40%; animation: float 4s ease-in-out infinite 2s; }
@keyframes float { 0%,100% { transform: translateY(0) rotate(-8deg);} 50%{transform: translateY(-12px) rotate(8deg);} }
.site-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 32px; max-width: 1280px; margin: 0 auto; position: relative; z-index: 2; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 22px; text-decoration: none; color: inherit; }
.logo-mark { width: 42px; height: 42px; border-radius: 12px; background: var(--coral); display: grid; place-items: center; box-shadow: var(--shadow-card); transform: rotate(-6deg); }
main { max-width: 1100px; margin: 0 auto; padding: 12px 28px 80px; position: relative; z-index: 2; }
.page-title { text-align: center; margin-bottom: 22px; }
.page-title h1 { font-size: clamp(36px, 5vw, 56px); font-weight: 700; letter-spacing: -0.5px; }
.page-title .accent-bg { background: var(--yellow); padding: 0 14px; border-radius: 12px; display: inline-block; transform: rotate(-1deg); }
.page-title p { font-weight: 600; color: rgba(45,45,45,0.65); margin-top: 8px; font-size: 17px; }
/* Tabs */
.tabs-wrap { display: flex; justify-content: center; margin-bottom: 24px; }
.tabs {
display: inline-flex; padding: 6px; background: var(--white); border: 3px solid var(--dark);
border-radius: 999px; box-shadow: var(--shadow-card); gap: 4px;
}
.tab-input { position: absolute; opacity: 0; pointer-events: none; }
.tab-label {
padding: 10px 22px; border-radius: 999px; font-weight: 700; font-size: 14px;
cursor: pointer; transition: all 0.18s ease; display: inline-flex; align-items: center; gap: 8px;
}
.tab-label:hover { background: var(--cream); }
/* Use sibling order: tabs grouped at top, panels show via :checked + general sibling combinator */
#tab-skribbl:checked ~ .tabs-wrap label[for=tab-skribbl],
#tab-gartic:checked ~ .tabs-wrap label[for=tab-gartic],
#tab-color:checked ~ .tabs-wrap label[for=tab-color] {
background: var(--coral); color: white; box-shadow: 0 4px 0 rgba(0,0,0,0.18);
}
.tab-panel { display: none; }
#tab-skribbl:checked ~ #panel-skribbl { display: block; }
#tab-gartic:checked ~ #panel-gartic { display: block; }
#tab-color:checked ~ #panel-color { display: block; }
/* Skribbl panel */
.podium {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; align-items: end;
margin-bottom: 24px;
}
.podium-card {
background: var(--white); border: 3px solid var(--dark); border-radius: 22px;
padding: 22px 18px; box-shadow: var(--shadow-card);
text-align: center; position: relative;
transition: transform 0.18s ease;
}
.podium-card:hover { transform: translateY(-4px); }
.podium-card .av {
width: 72px; height: 72px; border-radius: 22px; border: 3px solid var(--dark);
display: grid; place-items: center; font-size: 38px;
margin: 0 auto 10px; box-shadow: 0 4px 0 rgba(0,0,0,0.15);
}
.podium-card.first { background: var(--yellow); padding-top: 36px; padding-bottom: 32px; }
.podium-card.first .av { background: var(--white); width: 84px; height: 84px; font-size: 44px; }
.podium-card.second { background: var(--white); }
.podium-card.second .av { background: var(--mint); }
.podium-card.third { background: var(--white); }
.podium-card.third .av { background: var(--lavender); }
.trophy-row { display: flex; justify-content: center; gap: 6px; margin-bottom: 6px; }
.trophy {
width: 38px; height: 38px; border-radius: 12px;
display: grid; place-items: center; border: 2px solid var(--dark);
box-shadow: 0 3px 0 rgba(0,0,0,0.18);
}
.trophy.gold { background: var(--yellow); }
.trophy.silver { background: #D3D3D3; }
.trophy.bronze { background: #CD7F32; }
.podium-card .name { font-size: 22px; font-weight: 700; }
.podium-card .pts { font-size: 14px; font-weight: 600; color: rgba(45,45,45,0.7); margin-top: 2px; }
.podium-card .pts strong { font-size: 28px; color: var(--coral); display: block; margin-top: 4px; }
.podium-card.first .pts strong { color: var(--dark); font-size: 36px; }
.ribbon {
position: absolute; top: -14px; left: 50%; transform: translateX(-50%);
background: var(--coral); color: white; padding: 4px 14px; border-radius: 999px;
border: 2px solid var(--dark); font-size: 12px; font-weight: 700; letter-spacing: 1px;
box-shadow: 0 3px 0 rgba(0,0,0,0.18);
}
/* Other rankings */
.rest-list { background: var(--white); border: 3px solid var(--dark); border-radius: 20px; padding: 18px; box-shadow: var(--shadow-card); margin-bottom: 24px; }
.rest-row { display: flex; align-items: center; gap: 14px; padding: 12px 8px; border-bottom: 2px dashed rgba(45,45,45,0.12); }
.rest-row:last-child { border-bottom: none; }
.rest-row .rank { width: 36px; height: 36px; background: var(--cream); border: 2px solid var(--dark); border-radius: 10px; display: grid; place-items: center; font-weight: 700; flex-shrink: 0; }
.rest-row .av { width: 40px; height: 40px; border-radius: 12px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 22px; flex-shrink: 0; }
.rest-row .info { flex: 1; }
.rest-row .name { font-weight: 700; }
.rest-row .stats { font-size: 12px; font-weight: 600; color: rgba(45,45,45,0.6); }
.rest-row .pts { font-weight: 700; font-size: 18px; color: var(--coral); }
/* Action buttons */
.actions { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
.btn { font-family: inherit; font-weight: 700; font-size: 17px; padding: 16px 32px; border-radius: 999px; border: 3px solid var(--dark); cursor: pointer; box-shadow: var(--shadow-btn); transition: transform 0.15s ease; display: inline-flex; align-items: center; gap: 10px; text-decoration: none; }
.btn:hover { transform: translateY(-2px); }
.btn-primary { background: var(--coral); color: white; }
.btn-secondary { background: var(--mint); color: var(--dark); }
.btn-ghost { background: var(--white); color: var(--dark); }
/* Gartic panel — book playback */
.book-card { background: var(--white); border: 3px solid var(--dark); border-radius: 22px; padding: 22px; box-shadow: var(--shadow-card); margin-bottom: 24px; }
.book-meta { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.book-meta .b-info { display: flex; align-items: center; gap: 10px; }
.book-meta .av { width: 40px; height: 40px; border-radius: 12px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 22px; }
.book-meta h3 { font-size: 18px; font-weight: 700; }
.book-meta .meta { font-size: 13px; font-weight: 600; color: rgba(45,45,45,0.6); }
.book-nav { display: flex; gap: 8px; align-items: center; font-weight: 700; font-size: 13px; color: rgba(45,45,45,0.7); }
.nav-btn { width: 34px; height: 34px; background: var(--cream); border: 2px solid var(--dark); border-radius: 10px; display: grid; place-items: center; cursor: pointer; box-shadow: 0 3px 0 rgba(0,0,0,0.15); }
.book-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; }
.strip-card {
background: var(--cream); border: 2px solid var(--dark); border-radius: 16px;
padding: 12px; display: flex; flex-direction: column; gap: 8px;
box-shadow: 0 3px 0 rgba(0,0,0,0.12);
}
.strip-card .author { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 12px; }
.strip-card .author .av { width: 22px; height: 22px; border-radius: 7px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 12px; }
.strip-card .stage { font-size: 10px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: rgba(45,45,45,0.55); }
.strip-card .content {
flex: 1; min-height: 130px; background: var(--white); border-radius: 10px; border: 2px dashed rgba(45,45,45,0.2);
display: grid; place-items: center; padding: 10px; text-align: center;
}
.strip-card .content.text { font-size: 14px; font-weight: 700; line-height: 1.3; color: var(--dark); }
.strip-card .arrow {
position: relative;
}
/* Color final image panel */
.final-canvas-wrap { background: var(--white); border: 3px solid var(--dark); border-radius: 22px; padding: 22px; box-shadow: var(--shadow-card); margin-bottom: 24px; }
.final-canvas-stage { background: var(--cream); border-radius: 16px; padding: 18px; display: grid; place-items: center; min-height: 380px; border: 2px dashed rgba(45,45,45,0.2); }
.canvas-meta { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 10px; }
.canvas-meta h3 { font-size: 22px; font-weight: 700; }
.canvas-meta .stats { display: flex; gap: 10px; flex-wrap: wrap; }
.stat-pill { padding: 6px 14px; background: var(--cream); border: 2px solid var(--dark); border-radius: 999px; font-size: 12px; font-weight: 700; }
.contributors { display: flex; gap: -8px; }
.contributors .av { width: 32px; height: 32px; border-radius: 10px; border: 2px solid var(--dark); display: grid; place-items: center; font-size: 16px; margin-left: -6px; }
.contributors .av:first-child { margin-left: 0; }
@media (max-width: 800px) {
.podium { grid-template-columns: 1fr; }
.book-strip { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<svg class="confetti c1" width="40" height="40" viewBox="0 0 24 24" fill="#FFD23F" stroke="#2D2D2D" stroke-width="2.5"><polygon points="12 2 15 9 22 9 17 14 19 21 12 17 5 21 7 14 2 9 9 9"/></svg>
<svg class="confetti c2" width="36" height="36" viewBox="0 0 24 24" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="2.5"><circle cx="12" cy="12" r="9"/></svg>
<svg class="confetti c3" width="44" height="44" viewBox="0 0 24 24" fill="#A593E0" stroke="#2D2D2D" stroke-width="2.5"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>
<header class="site-header">
<a class="logo" href="01-landing.html">
<div class="logo-mark"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg></div>
DrawTogether
</a>
<a href="04-lobby.html" class="btn btn-ghost" style="padding: 10px 20px; font-size: 14px; border-width: 2px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Back to lobby
</a>
</header>
<main>
<div class="page-title">
<h1>Game over — <span class="accent-bg">nice work</span></h1>
<p>5 rounds. 5 players. 1 absolute legend.</p>
</div>
<!-- Tab inputs MUST come before .tabs-wrap and panels for sibling combinator -->
<input type="radio" name="results-tab" id="tab-skribbl" class="tab-input" checked>
<input type="radio" name="results-tab" id="tab-gartic" class="tab-input">
<input type="radio" name="results-tab" id="tab-color" class="tab-input">
<div class="tabs-wrap">
<div class="tabs" data-testid="result-tabs">
<label class="tab-label" for="tab-skribbl" data-testid="tab-skribbl">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/></svg>
Skribbl scores
</label>
<label class="tab-label" for="tab-gartic" data-testid="tab-gartic">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Gartic book
</label>
<label class="tab-label" for="tab-color" data-testid="tab-color">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/></svg>
Color final
</label>
</div>
</div>
<!-- Skribbl panel -->
<section id="panel-skribbl" class="tab-panel" data-testid="panel-skribbl">
<div class="podium">
<article class="podium-card second">
<div class="trophy-row"><div class="trophy silver"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="8" r="6"/><path d="M9 14l-3 7 6-3 6 3-3-7"/></svg></div></div>
<div class="av">🦊</div>
<div class="name">Suki</div>
<div class="pts">2nd · <strong>1840</strong> pts</div>
</article>
<article class="podium-card first">
<div class="ribbon">CHAMPION</div>
<div class="trophy-row"><div class="trophy gold"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="8" r="6"/><path d="M9 14l-3 7 6-3 6 3-3-7"/></svg></div></div>
<div class="av">🦄</div>
<div class="name">Mia</div>
<div class="pts">1st · <strong>2310</strong></div>
</article>
<article class="podium-card third">
<div class="trophy-row"><div class="trophy bronze"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"><circle cx="12" cy="8" r="6"/><path d="M9 14l-3 7 6-3 6 3-3-7"/></svg></div></div>
<div class="av">🐼</div>
<div class="name">Ravi</div>
<div class="pts">3rd · <strong>1620</strong> pts</div>
</article>
</div>
<div class="rest-list">
<div class="rest-row">
<div class="rank">4</div>
<div class="av" style="background: var(--sky);">🐸</div>
<div class="info">
<div class="name">Jules</div>
<div class="stats">3 correct guesses · 1 perfect drawing</div>
</div>
<div class="pts">1410</div>
</div>
<div class="rest-row">
<div class="rank">5</div>
<div class="av" style="background: var(--coral);">🐱</div>
<div class="info">
<div class="name">Pat</div>
<div class="stats">2 correct guesses · funniest player award 🤣</div>
</div>
<div class="pts">1180</div>
</div>
</div>
<div class="actions">
<a href="04-lobby.html" class="btn btn-primary" data-testid="btn-play-again">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Play Again
</a>
<a href="02-create-room.html" class="btn btn-secondary" data-testid="btn-new-game">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
New Game
</a>
<a href="01-landing.html" class="btn btn-ghost">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
Home
</a>
</div>
</section>
<!-- Gartic book panel -->
<section id="panel-gartic" class="tab-panel" data-testid="panel-gartic">
<div class="book-card">
<div class="book-meta">
<div class="b-info">
<div class="av" style="background: var(--yellow);">🦄</div>
<div>
<h3>Mia's book</h3>
<div class="meta">5 chapters · started with a prompt</div>
</div>
</div>
<div class="book-nav">
<button class="nav-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg></button>
<span>Book 1 / 5</span>
<button class="nav-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg></button>
</div>
</div>
<div class="book-strip">
<div class="strip-card">
<div class="stage">Prompt</div>
<div class="author"><div class="av" style="background: var(--yellow);">🦄</div> Mia</div>
<div class="content text">"a cat riding a unicorn"</div>
</div>
<div class="strip-card">
<div class="stage">Drawing</div>
<div class="author"><div class="av" style="background: var(--mint);">🦊</div> Suki</div>
<div class="content">
<svg viewBox="0 0 100 100" width="100%" height="100%">
<ellipse cx="50" cy="65" rx="35" ry="18" fill="#A593E0" stroke="#2D2D2D" stroke-width="2.5"/>
<circle cx="32" cy="58" r="14" fill="#FFD23F" stroke="#2D2D2D" stroke-width="2.5"/>
<polygon points="22,48 22,38 32,46" fill="#FFD23F" stroke="#2D2D2D" stroke-width="2"/>
<polygon points="40,46 40,38 30,46" fill="#FFD23F" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="29" cy="58" r="1.5" fill="#2D2D2D"/>
<circle cx="35" cy="58" r="1.5" fill="#2D2D2D"/>
<polygon points="70,42 75,30 80,42" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="2.5" stroke-linejoin="round"/>
<line x1="20" y1="80" x2="20" y2="88" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
<line x1="40" y1="80" x2="40" y2="88" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
<line x1="60" y1="80" x2="60" y2="88" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
<line x1="80" y1="80" x2="80" y2="88" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
</div>
<div class="strip-card">
<div class="stage">Guess</div>
<div class="author"><div class="av" style="background: var(--lavender);">🐼</div> Ravi</div>
<div class="content text">"horse with a small dragon on top"</div>
</div>
<div class="strip-card">
<div class="stage">Drawing</div>
<div class="author"><div class="av" style="background: var(--sky);">🐸</div> Jules</div>
<div class="content">
<svg viewBox="0 0 100 100" width="100%" height="100%">
<ellipse cx="50" cy="65" rx="34" ry="20" fill="#B5651D" stroke="#2D2D2D" stroke-width="2.5"/>
<circle cx="22" cy="55" r="12" fill="#B5651D" stroke="#2D2D2D" stroke-width="2.5"/>
<polygon points="14,46 16,38 24,48" fill="#B5651D" stroke="#2D2D2D" stroke-width="2"/>
<ellipse cx="55" cy="38" rx="14" ry="10" fill="#4ECDC4" stroke="#2D2D2D" stroke-width="2.5"/>
<path d="M50 30 Q55 20 60 30" stroke="#FF5C5C" stroke-width="2.5" fill="none" stroke-linecap="round"/>
<line x1="20" y1="80" x2="20" y2="92" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
<line x1="40" y1="80" x2="40" y2="92" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
<line x1="60" y1="80" x2="60" y2="92" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
<line x1="80" y1="80" x2="80" y2="92" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
</div>
<div class="strip-card">
<div class="stage">Guess</div>
<div class="author"><div class="av" style="background: var(--coral);">🐱</div> Pat</div>
<div class="content text">"a brown horse drinking from a cute pond 💧"</div>
</div>
</div>
</div>
<div class="actions">
<a href="04-lobby.html" class="btn btn-primary">Play Again</a>
<a href="02-create-room.html" class="btn btn-secondary">New Game</a>
</div>
</section>
<!-- Color final panel -->
<section id="panel-color" class="tab-panel" data-testid="panel-color">
<div class="final-canvas-wrap">
<div class="canvas-meta">
<div>
<h3>Mandala by the crew</h3>
<div style="font-weight:600; color: rgba(45,45,45,0.6); font-size:13px; margin-top:2px;">Created in 18 minutes · 4 contributors</div>
</div>
<div class="stats">
<span class="stat-pill">412 strokes</span>
<span class="stat-pill">8 colors used</span>
</div>
<div class="contributors">
<div class="av" style="background: var(--yellow);">🦄</div>
<div class="av" style="background: var(--mint);">🦊</div>
<div class="av" style="background: var(--lavender);">🐼</div>
<div class="av" style="background: var(--sky);">🐸</div>
</div>
</div>
<div class="final-canvas-stage">
<svg viewBox="0 0 600 600" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" style="max-width: 460px;">
<defs>
<radialGradient id="centerGrad2" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#FFD23F"/>
<stop offset="100%" stop-color="#FF8C42"/>
</radialGradient>
</defs>
<circle cx="300" cy="300" r="270" fill="rgba(91,206,250,0.2)" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="220" fill="rgba(255,210,63,0.35)" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="160" fill="rgba(165,147,224,0.7)" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="100" fill="rgba(78,205,196,0.85)" stroke="#2D2D2D" stroke-width="3"/>
<circle cx="300" cy="300" r="50" fill="url(#centerGrad2)" stroke="#2D2D2D" stroke-width="3"/>
<g transform="translate(300 300)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(45)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(90)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#A593E0" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(135)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#4ECDC4" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(180)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#FF5C5C" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(225)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#FFD23F" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(270)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#A593E0" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(315)"><path d="M0 -270 Q 22 -210 0 -160 Q -22 -210 0 -270 Z" fill="#4ECDC4" stroke="#2D2D2D" stroke-width="3"/></g>
<g transform="translate(300 300) rotate(22.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(67.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FF8C42" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(112.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(157.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FF8C42" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(202.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(247.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FF8C42" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(292.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2.5"/></g>
<g transform="translate(300 300) rotate(337.5)"><path d="M0 -210 Q 18 -180 0 -160 Q -18 -180 0 -210 Z" fill="#FF8C42" stroke="#2D2D2D" stroke-width="2.5"/></g>
<circle cx="300" cy="300" r="22" fill="#2D2D2D" stroke="#2D2D2D" stroke-width="2.5"/>
<circle cx="300" cy="270" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="330" cy="300" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="300" cy="330" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
<circle cx="270" cy="300" r="8" fill="#FFF8E7" stroke="#2D2D2D" stroke-width="2"/>
</svg>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" data-testid="btn-download">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
Download artwork
</button>
<a href="02-create-room.html" class="btn btn-secondary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
New Canvas
</a>
</div>
</section>
</main>
</body>
</html>
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env node
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const DEFAULT_VIEWPORTS = {
mobile: { width: 375, height: 812 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1440, height: 900 },
};
async function run() {
const manifestPath = path.resolve(process.cwd(), "test-manifest.json");
if (!fs.existsSync(manifestPath)) {
console.error("ERROR: test-manifest.json not found in", process.cwd());
process.exit(1);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
const baseUrl = manifest.baseUrl || "http://localhost:3000";
const viewports = manifest.viewports
? Object.fromEntries(
Object.entries(manifest.viewports).map(function (entry) {
return [entry[0], entry[1]];
})
)
: DEFAULT_VIEWPORTS;
const resultsDir = path.resolve(process.cwd(), "test-results");
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH || "chromium";
var browser;
try {
browser = await puppeteer.launch({
headless: "new",
executablePath: executablePath,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
} catch (err) {
console.error("ERROR: Failed to launch browser:", err.message);
process.exit(1);
}
var results = { pages: [], passed: 0, failed: 0, errors: [] };
for (var pi = 0; pi < (manifest.pages || []).length; pi++) {
var pageDef = manifest.pages[pi];
var pageResult = {
id: pageDef.id,
url: baseUrl + (pageDef.path || "/"),
viewports: {},
passed: true,
};
var vpEntries = Object.entries(viewports);
for (var vi = 0; vi < vpEntries.length; vi++) {
var vpName = vpEntries[vi][0];
var vpSize = vpEntries[vi][1];
var vpResult = {
viewport: vpName,
screenshots: [],
actions: [],
consoleErrors: [],
passed: true,
};
var browserPage = null;
try {
browserPage = await browser.newPage();
await browserPage.setViewport(vpSize);
var consoleErrors = [];
browserPage.on("console", function (msg) {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
browserPage.on("pageerror", function (err) {
consoleErrors.push(err.message);
});
await browserPage.goto(pageResult.url, {
waitUntil: "networkidle2",
timeout: 30000,
});
if (pageDef.waitFor) {
await browserPage.waitForSelector(pageDef.waitFor, { timeout: 10000 });
}
var initialScreenshot = path.join(
resultsDir,
pageDef.id + "-" + vpName + "-initial.png"
);
await browserPage.screenshot({
path: initialScreenshot,
fullPage: true,
});
vpResult.screenshots.push(initialScreenshot);
var actions = pageDef.actions || [];
for (var ai = 0; ai < actions.length; ai++) {
var action = actions[ai];
var actionResult = { id: action.id, type: action.type, passed: true, error: null };
try {
await executeAction(browserPage, action);
var actionScreenshot = path.join(
resultsDir,
pageDef.id + "-" + vpName + "-" + action.id + ".png"
);
await browserPage.screenshot({
path: actionScreenshot,
fullPage: true,
});
vpResult.screenshots.push(actionScreenshot);
if (action.expectAfter) {
var valid = await validateExpectations(
browserPage,
action.expectAfter
);
if (!valid) {
actionResult.passed = false;
actionResult.error = "Expectation failed for " + action.id;
}
}
} catch (err) {
actionResult.passed = false;
actionResult.error = err.message;
}
if (!actionResult.passed) {
vpResult.passed = false;
}
vpResult.actions.push(actionResult);
}
vpResult.consoleErrors = consoleErrors;
if (consoleErrors.length > 0) {
vpResult.passed = false;
}
} catch (err) {
vpResult.passed = false;
vpResult.error = err.message;
results.errors.push({
page: pageDef.id,
viewport: vpName,
error: err.message,
});
} finally {
if (browserPage) {
await browserPage.close().catch(function () {});
}
}
if (!vpResult.passed) {
pageResult.passed = false;
}
pageResult.viewports[vpName] = vpResult;
}
if (pageResult.passed) {
results.passed++;
} else {
results.failed++;
}
results.pages.push(pageResult);
}
await browser.close();
var summaryPath = path.join(resultsDir, "summary.json");
fs.writeFileSync(summaryPath, JSON.stringify(results, null, 2));
console.log("");
console.log("=== Visual Test Results ===");
console.log("Pages tested: " + results.pages.length);
console.log("Passed: " + results.passed);
console.log("Failed: " + results.failed);
for (var ri = 0; ri < results.pages.length; ri++) {
var p = results.pages[ri];
var icon = p.passed ? "PASS" : "FAIL";
console.log(" [" + icon + "] " + p.id + " (" + p.url + ")");
var vpKeys = Object.keys(p.viewports);
for (var vk = 0; vk < vpKeys.length; vk++) {
var vr = p.viewports[vpKeys[vk]];
if (!vr.passed) {
console.log(" " + vpKeys[vk] + ": FAIL" + (vr.error ? " - " + vr.error : ""));
for (var ak = 0; ak < (vr.actions || []).length; ak++) {
var a = vr.actions[ak];
if (!a.passed) {
console.log(" action " + a.id + ": " + a.error);
}
}
if (vr.consoleErrors && vr.consoleErrors.length > 0) {
console.log(" console errors: " + vr.consoleErrors.length);
}
}
}
}
console.log("");
console.log("Summary written to: " + summaryPath);
process.exit(results.failed > 0 ? 1 : 0);
}
async function executeAction(page, action) {
switch (action.type) {
case "click":
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.click(action.selector);
break;
case "fill-form":
var fields = action.fields || [];
for (var fi = 0; fi < fields.length; fi++) {
var field = fields[fi];
await page.waitForSelector(field.selector, { timeout: 5000 });
await page.click(field.selector, { clickCount: 3 });
await page.type(field.selector, field.value);
}
break;
case "select":
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.select(action.selector, action.value);
break;
case "scroll":
if (action.selector) {
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.$eval(action.selector, function (el) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
});
} else {
await page.evaluate(
function (x, y) { window.scrollTo(x, y); },
action.x || 0,
action.y || 0
);
}
await new Promise(function (r) { setTimeout(r, 500); });
break;
case "hover":
await page.waitForSelector(action.selector, { timeout: 5000 });
await page.hover(action.selector);
await new Promise(function (r) { setTimeout(r, 300); });
break;
case "wait":
if (action.selector) {
await page.waitForSelector(action.selector, {
timeout: action.timeout || 10000,
});
} else {
await new Promise(function (r) { setTimeout(r, action.duration || 1000); });
}
break;
default:
throw new Error("Unknown action type: " + action.type);
}
}
async function validateExpectations(page, expectations) {
var items = Array.isArray(expectations) ? expectations : [expectations];
for (var ei = 0; ei < items.length; ei++) {
var expect = items[ei];
if (expect.url) {
var currentUrl = page.url();
var pattern = new RegExp(expect.url);
if (!pattern.test(currentUrl)) {
return false;
}
}
if (expect.visible) {
var el = await page.$(expect.visible);
if (!el) return false;
var isVisible = await page.evaluate(function (s) {
var e = document.querySelector(s);
if (!e) return false;
var r = e.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}, expect.visible);
if (!isVisible) return false;
}
if (expect.hidden) {
var elH = await page.$(expect.hidden);
if (elH) {
var isVis = await page.evaluate(function (s) {
var e = document.querySelector(s);
if (!e) return false;
var r = e.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}, expect.hidden);
if (isVis) return false;
}
}
if (expect.text) {
var textContent = await page.$eval(
expect.text.selector,
function (el) { return el.textContent; }
);
if (!textContent || !textContent.includes(expect.text.contains)) {
return false;
}
}
}
return true;
}
run().catch(function (err) {
console.error("Fatal error:", err);
process.exit(1);
});