diff --git a/HANDOFF.md b/HANDOFF.md
index 07589d0..843b221 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -4,8 +4,8 @@
> If this file disagrees with the code, trust the code and fix this file.
## 1. Where we are
-- **Last completed step:** 9.3 (error pages, shared toast system, loading states) — TICKED in STATUS.md
-- **Next step:** Phase 9 — 9.4 (Mobile-responsive pass per UI_GUIDE)
+- **Last completed step:** 9.4 (Mobile-responsive pass) — TICKED in STATUS.md
+- **Next step:** Phase 9 — 9.5 (Final full run of "Definition of playable" from NOTES.md §7)
- **Updated:** 2026-10-21
- **Was the last session cut off mid-task?** No
@@ -18,26 +18,23 @@
| `npm run seed:dev` | creates demo user "demo"/"demo123", hero "Aldric" (warrior), starter gear |
## 3. What exists now (files that matter)
-- `public/404.html` — Custom 404 error page (branding: "Page Not Found", Emberfall theme).
-- `public/500.html` — Custom 500 error page (branding: "Something went wrong", retry button).
-- `public/js/toast.js` — Shared toast notification system (window.showToast, 3 types: error/info/success).
-- `public/game.html` — Dashboard with stats panel, tabbed action panel (Hunt/Inventory/Expedition).
-- `public/js/game.js` — hunt UI + inventory UI + expedition UI (uses shared toast.js).
-- `public/shop.html` — Dedicated shop page.
-- `public/js/shop.js` — buy/sell handlers (uses shared toast.js).
-- `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation (all include toast.js).
-- `public/profile.html` — Public hero profile page.
-- `public/js/profile.js` — Fetches GET /api/heroes/:name, renders profile.
+- `public/game.html` — Dashboard with 3-col grid (stats | action | combat), mobile bottom nav (⚔️ Hunt / 🏕️ Trip / 🎒 Items), tabbed action area.
+- `public/js/game.js` — hunt UI + inventory UI + expedition UI + mobile nav init (DOMContentLoaded); uses shared toast.js.
+- `public/css/main.css` — All styles including 3-col desktop grid, mobile bottom nav (< 700px), stacked panels on mobile, spinner, toast, button loading states.
+- `public/404.html` — Custom 404 error page.
+- `public/500.html` — Custom 500 error page.
+- `public/js/toast.js` — Shared toast notification system.
+- `public/shop.html` / `public/js/shop.js` — Shop page (buy/sell).
+- `public/login.html`, `public/register.html`, `public/create-hero.html` — Auth & hero creation.
+- `public/profile.html` / `public/js/profile.js` — Public hero profile page.
- `public/js/auth.js` — Shared API wrapper + showError.
-- `public/css/main.css` — All styles including spinner, toast, button loading states, error page shared CSS.
-- `scripts/seed-dev.js` — Creates demo account + hero + starter gear (idempotent, skips in production).
+- `scripts/seed-dev.js` — Creates demo account + hero + starter gear.
- `src/app.js` — Express app with helmet, rate limiters, catch-all 404, 500 HTML serving.
-- `src/services/*` — All game services (auth, character, hunt, inventory, shop, expedition, leaderboard, profile).
+- `src/services/*` — All game services.
- `src/routes/*` — All route handlers.
-- `src/game/*` — Pure game logic (combat, stats, energy, leveling, rewards).
-- `tests/integration/error-pages.test.js` — 3 tests for 404/500 page serving.
-- `tests/integration/*.test.js` — 90 integration tests across auth, combat, hunt, inventory, shop, expeditions, profile, leaderboard, error-pages.
-- `tests/unit/*.test.js` — 85 unit tests for game logic.
+- `src/game/*` — Pure game logic.
+- `tests/integration/*.test.js` — 90 integration tests.
+- `tests/unit/*.test.js` — 85 unit tests.
## 4. Half-finished / fragile things
- No leaderboard UI yet (Phase 8 API done but no UI).
@@ -45,21 +42,19 @@
- No restart-persistence test for sessions.
- `npm run seed` script in package.json points to nonexistent `scripts/seed.js`.
- Equipment bonuses not yet passed into combat (Issue #5 in STATUS.md).
-- Starter gear (GAME_DESIGN.md §Items) documented but not auto-created on character creation.
+- Starter gear documented but not auto-created on character creation.
- `GET /api/zones` documented in API.md but not implemented (Issue #2).
+- Mobile nav uses emoji icons (⚔️ 🏕️ 🎒) — verify they render acceptably on all platforms.
## 5. Decisions & deviations from the docs
- D-004..D-007 in `docs/DECISIONS.md` (combat rounds, equipment counted once, session store, clock).
+- **D-014:** Mobile bottom nav uses emoji icons (⚔️ Hunt / 🏕️ Trip / 🎒 Items) instead of full text labels to save horizontal space on narrow screens.
+- **D-015:** Desktop tabs hidden via CSS (`display: none`) below 700px; mobile nav shown via CSS; JS `syncMobileNav()` keeps active state in sync.
- Zod query param parsing uses `zod.coerce.number()` because Express query params are strings.
- Inventory response uses `itemId` (item.id) and `inventoryId` (inventory row id) to avoid confusion.
- Consumable effect parsing: regex `^(heal|energy):(\d+)$` on items.effect field.
- Qty decrement avoids CHECK constraint violation: delete directly when qty==1, decrement when qty>1.
-- **D-008:** Shop materials with buy_price=0 sell at `floor(default_rate * character_level / item_min_level)`.
-- **D-009:** `sellItem` checks equipped_slot before material type.
-- **D-010:** `startExpedition` checks active-expedition before energy.
-- **D-011:** `computeEnergy` uses `now()` from utils/time.
-- **D-012:** General `/api` rate limiter at 120 req/min per IP (SECURITY.md spec), 1000 in test mode.
-- **D-013:** All `innerHTML` with dynamic data removed from game.js and profile.js — use createElement/textContent.
+- D-008..D-013 from previous steps.
## 6. Gotchas learned
- `npm test` must be `node --test` with NO path/glob; files must be named `*.test.js`.
@@ -81,10 +76,8 @@
- Express `sendFile` rejects `../` path segments — use `path.resolve()` + `path.join()` for absolute paths.
- `express.static` sends its own 404 responses; catch-all must be placed BEFORE static middleware.
-## 7. Next-step briefing (9.4)
-- **Goal:** Mobile-responsive pass per UI_GUIDE (< 700px: single column, tabs become bottom nav).
-- **Docs:** UI_GUIDE §game.html layout (Mobile section).
-- Current CSS has some `@media (max-width: 600px)` rules but needs comprehensive mobile review.
-- Should: verify all pages render correctly on mobile viewports.
-- Should: tabs → bottom nav on mobile (per UI_GUIDE).
-- Should NOT introduce new dependencies — use vanilla HTML/CSS/JS.
+## 7. Next-step briefing (9.5)
+- **Goal:** Final full run of "Definition of playable" from NOTES.md §7.
+- Walk the complete flow: register → log in → create hero → see stats/energy → hunt → gain XP/gold/loot → level up → equip items → buy/sell in shop → start and claim expedition → see leaderboard → log out → log back in (verify persistence).
+- Document any gaps or broken steps.
+- No code changes expected unless a critical blocker is found.
diff --git a/STATUS.md b/STATUS.md
index a6c9440..789ab5d 100644
--- a/STATUS.md
+++ b/STATUS.md
@@ -8,9 +8,9 @@
## Current Focus
- **Phase:** 9
-- **Step:** 9.4
+- **Step:** 9.5
- **Blocked?** No
-- **Last verified working command:** `npm test` — 175/175 passing (85 unit + 90 integration), 2026-10-21 step 9.3
+- **Last verified working command:** `npm test` — 175/175 passing (85 unit + 90 integration), 2026-10-21 step 9.4
## Project Health
| Item | State |
@@ -110,7 +110,7 @@
- [x] **9.2** Add `npm run seed:dev` creating a demo account and hero.
*Acceptance:* `npm run seed:dev` creates migrations + demo user + hero + starter gear; safe to re-run; skipped in production.
- [x] **9.3** Global error pages / friendly UI error toasts. Loading states.
-- [ ] **9.4** Mobile-responsive pass (see UI_GUIDE).
+- [x] **9.4** Mobile-responsive pass (see UI_GUIDE).
- [ ] **9.5** Final full run of "Definition of playable" from NOTES.md §7. Record results below.
- [ ] **9.6** Update README with real run instructions.
@@ -153,11 +153,12 @@ Severity: `blocker` / `major` / `minor` / `cosmetic`. Status: `open` / `fixing`
## Test Summary
| Suite | Passing | Failing | Last run |
|---|---|---|---|
-| unit | 85 | 0 | 2026-10-21 step 8.3 |
-| integration | 87 | 0 | 2026-10-21 step 8.3 |
+| unit | 85 | 0 | 2026-10-21 step 9.4 |
+| integration | 90 | 0 | 2026-10-21 step 9.4 |
## Session Log
_Format:_ `YYYY-MM-DD | step | what changed | result`
+- 2026-10-21 | 9.4 | Mobile-responsive pass: game.html 3-col grid (stats | action | combat), mobile bottom nav (⚔️ Hunt / 🏕️ Trip / 🎒 Items), desktop tabs hidden < 700px, game-container padding for nav clearance, mobile stat-row stacking, inventory container stacks < 600px; 175/175 tests pass
- 2026-10-21 | 8.3 | Created public profile UI: `public/profile.html` + `public/js/profile.js` (fetches GET /api/heroes/:name via ?name=, displays hero name/class/level/gold/stats/equipment; 404 handling, URL-safe names); 5 integration tests | 172/172 tests pass
- 2026-10-21 | 7.3 | Added expedition UI: GET /api/expeditions/defs endpoint (expeditionService.listDefinitions + expeditions route), CSS styles for expedition elements, game.html expedition tab, game.js expedition UI (renderExpeditionDefs, renderActiveExpedition, handleStartExpedition, handleClaimExpedition, startExpeditionSync), 2 integration tests for defs endpoint | 153/153 tests pass
- 2026-10-21 | 7.2 | Created `src/services/expeditionService.js` (start, getActive, claim), `src/routes/expeditions.js` (3 endpoints), integrated into app.js; 11 integration tests covering start, active, claim, error paths, double-claim, cross-user rejection, time-based finish | 151/151 tests pass
diff --git a/public/css/main.css b/public/css/main.css
index 8a8ee8a..18694a2 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -76,9 +76,10 @@ button:hover { background: #c73650; }
/* Game dashboard layout */
.game-container {
width: 100%;
- max-width: 800px;
+ max-width: 1200px;
margin: 0 auto;
padding: 1rem;
+ padding-bottom: 5rem; /* space for mobile bottom nav */
}
.top-bar {
@@ -99,12 +100,24 @@ button:hover { background: #c73650; }
.main-content {
display: grid;
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: 1fr 1.5fr 1fr;
gap: 1rem;
}
-@media (max-width: 600px) {
- .main-content { grid-template-columns: 1fr; }
+@media (max-width: 700px) {
+ .main-content {
+ grid-template-columns: 1fr;
+ }
+ .game-container {
+ padding-bottom: 5rem;
+ }
+}
+
+/* Hide desktop tabs on mobile */
+@media (max-width: 700px) {
+ #action-panel > .tabs {
+ display: none;
+ }
}
.panel {
@@ -332,6 +345,19 @@ button:hover { background: #c73650; }
}
}
+/* ===== Stat rows (mobile: stack name/value) ===== */
+@media (max-width: 700px) {
+ .stat-row > .label {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ text-align: left;
+ }
+ .stat-row > .label:last-child {
+ text-align: right;
+ }
+}
+
/* ===== Equipment panel ===== */
.equipment-panel h4 {
color: var(--accent, #e2822b);
@@ -661,6 +687,43 @@ button:hover { background: #c73650; }
cursor: not-allowed;
}
+/* ===== Mobile bottom navigation ===== */
+.mobile-bottom-nav {
+ display: none;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: #16213e;
+ border-top: 1px solid #333;
+ z-index: 100;
+ padding: 0.25rem 0.5rem;
+ padding-bottom: env(safe-area-inset-bottom, 0.25rem);
+}
+
+@media (max-width: 700px) {
+ .mobile-bottom-nav {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ }
+}
+
+.mobile-bottom-nav .tab {
+ flex: 1;
+ text-align: center;
+ padding: 0.5rem 0.25rem;
+ font-size: 0.75rem;
+ border-bottom: 2px solid transparent;
+ border-radius: 0;
+ color: var(--muted, #a59a8a);
+}
+
+.mobile-bottom-nav .tab.active {
+ color: var(--accent, #e2822b);
+ border-bottom-color: var(--accent, #e2822b);
+}
+
/* ===== Profile page ===== */
.profile-header {
display: flex;
diff --git a/public/game.html b/public/game.html
index ac12fc4..2c21d7e 100644
--- a/public/game.html
+++ b/public/game.html
@@ -17,9 +17,9 @@
-
+
-
+
Your Hero
Loading...
@@ -72,7 +72,7 @@
-
+
-
+
Combat Log
@@ -112,12 +112,19 @@
-
+
+
+
+
diff --git a/public/js/game.js b/public/js/game.js
index e2a8580..9152fb1 100644
--- a/public/js/game.js
+++ b/public/js/game.js
@@ -21,6 +21,36 @@ function switchTab(tab) {
document.getElementById('inventory-tab').style.display = tab === 'inventory' ? '' : 'none';
}
+/**
+ * Sync mobile bottom nav active state with desktop tabs.
+ */
+function syncMobileNav() {
+ const mobileNav = document.querySelector('.mobile-bottom-nav');
+ if (!mobileNav) return;
+ mobileNav.querySelectorAll('.tab').forEach((t) => {
+ const isActive = t.dataset.tab === currentTab;
+ t.classList.toggle('active', isActive);
+ t.setAttribute('aria-selected', isActive);
+ });
+}
+
+/**
+ * Attach mobile bottom-nav click handlers.
+ */
+function initMobileNav() {
+ const mobileNav = document.querySelector('.mobile-bottom-nav');
+ if (!mobileNav) return;
+ mobileNav.querySelectorAll('.tab').forEach((btn) => {
+ btn.addEventListener('click', () => {
+ switchTab(btn.dataset.tab);
+ });
+ });
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ initMobileNav();
+});
+
/**
* Refresh energy state from the server at a fixed interval.
* Keeps client-side countdown in sync with the authoritative server.