diff --git a/apps/api/internal/migration/sql/008_cosmetics.sql b/apps/api/internal/migration/sql/008_cosmetics.sql
index aae41e8..746762f 100644
--- a/apps/api/internal/migration/sql/008_cosmetics.sql
+++ b/apps/api/internal/migration/sql/008_cosmetics.sql
@@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS cosmetics (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
- type VARCHAR(20) NOT NULL CHECK (type IN ('hair','shirt','pants','shoes','hat','accessory')),
+ type VARCHAR(20) NOT NULL CHECK (type IN ('outfit','hat','accessory')),
unlock_level INTEGER NOT NULL,
css_data JSONB NOT NULL DEFAULT '{}'
);
@@ -13,13 +13,20 @@ CREATE TABLE IF NOT EXISTS player_cosmetics (
unlocked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
+-- Seeds aligned with sprite-data.json themes (keys = sprite color slots)
INSERT INTO cosmetics (name, type, unlock_level, css_data) VALUES
- ('Cabelo Ruivo', 'hair', 2, '{"color":"#8B1a0a"}'),
- ('Camisa Marinheiro', 'shirt', 3, '{"color":"#1e3a5f"}'),
- ('Calças Aventureiro', 'pants', 4, '{"color":"#2d4a1e"}'),
- ('Botas de Capitão', 'shoes', 5, '{"color":"#8B4513"}'),
- ('Camisa Dourada', 'shirt', 7, '{"color":"#d4af37"}'),
- ('Cabelo Platinado', 'hair', 10, '{"color":"#e8e8e8"}')
+ ('Tripulante Iniciante', 'outfit', 1,
+ '{"sprite_id":"lv01-tripulante","h":"#5a3a1d","w":"#5a3a1d","c":"#0ea5e9","C":"#075985","p":"#a16207","P":"#7c4a08","b":"#1e293b"}'),
+ ('Boné Verolme', 'outfit', 3,
+ '{"sprite_id":"lv03-bone-verolme","h":"#1e40af","w":"#fde68a","c":"#0ea5e9","C":"#075985","p":"#a16207","P":"#7c4a08","b":"#1e293b"}'),
+ ('Camiseta Wingspan', 'outfit', 5,
+ '{"sprite_id":"lv05-wingspan","h":"#ffd84d","w":"#fde68a","c":"#10b981","C":"#065f46","p":"#3a1f6e","P":"#2d1857","b":"#0f172a"}'),
+ ('Chapéu de Sol', 'outfit', 7,
+ '{"sprite_id":"lv07-chapeu-sol","h":"#ffd84d","w":"#fde68a","c":"#00f0ff","C":"#0369a1","p":"#3a1f6e","P":"#2d1857","b":"#0f172a"}'),
+ ('Casaco Anti-Vento', 'outfit', 10,
+ '{"sprite_id":"lv10-anti-vento","h":"#1e40af","w":"#fde68a","c":"#a855f7","C":"#581c87","p":"#1e293b","P":"#0f172a","b":"#0f172a"}'),
+ ('Trajado de Capitão', 'outfit', 15,
+ '{"sprite_id":"lv15-capitao","h":"#ffd84d","w":"#dc2626","c":"#dc2626","C":"#7a0e0e","p":"#1c1917","P":"#0c0a09","b":"#0c0a09"}')
ON CONFLICT (name) DO NOTHING;
INSERT INTO schema_migrations (version) VALUES (8) ON CONFLICT DO NOTHING;
diff --git a/apps/web/index.html b/apps/web/index.html
index d5a4d0e..8349d8f 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -5,6 +5,9 @@
Financeiro Carvalho
+
+
+
diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue
index b198a92..a8ccf30 100644
--- a/apps/web/src/App.vue
+++ b/apps/web/src/App.vue
@@ -1,39 +1,29 @@
-
-
+
-
-
diff --git a/apps/web/src/assets/icons.json b/apps/web/src/assets/icons.json
new file mode 100644
index 0000000..d157d42
--- /dev/null
+++ b/apps/web/src/assets/icons.json
@@ -0,0 +1,67 @@
+{
+ "_note": "Each icon is a flat grid of single-char keys. '.' = transparent. Colors are filled at render time from CSS tokens or props.",
+ "heart": {
+ "rows": 8,
+ "cols": 8,
+ "grid": [
+ "........",
+ ".XX..XX.",
+ "XXXXXXXX",
+ "XXXXXXXX",
+ ".XXXXXX.",
+ "..XXXX..",
+ "...XX...",
+ "........"
+ ],
+ "defaultColor": "var(--fc-red, #ff3b6b)"
+ },
+ "coin": {
+ "rows": 8,
+ "cols": 8,
+ "grid": [
+ "..XXXX..",
+ ".XYYYYX.",
+ "XYYSYYYX",
+ "XYYSYYYX",
+ "XYYSYYYX",
+ "XYYSYYYX",
+ ".XYYYYX.",
+ "..XXXX.."
+ ],
+ "colors": {
+ "X": "var(--fc-gold, #ffd84d)",
+ "Y": "var(--fc-gold, #ffd84d)",
+ "S": "#a16207"
+ }
+ },
+ "star": {
+ "rows": 8,
+ "cols": 8,
+ "grid": [
+ "...XX...",
+ "...XX...",
+ "XX.XX.XX",
+ ".XXXXXX.",
+ "..XXXX..",
+ ".XXXXXX.",
+ "XX.XX.XX",
+ "...XX..."
+ ],
+ "defaultColor": "var(--fc-gold, #ffd84d)"
+ },
+ "anchor": {
+ "rows": 8,
+ "cols": 8,
+ "grid": [
+ "...XX...",
+ "..XXXX..",
+ "...XX...",
+ "XXXXXXXX",
+ "...XX...",
+ "X..XX..X",
+ "XXXXXXXX",
+ "..XXXX.."
+ ],
+ "defaultColor": "var(--fc-accent-2, #00f0ff)"
+ }
+}
diff --git a/apps/web/src/assets/sprite-data.json b/apps/web/src/assets/sprite-data.json
new file mode 100644
index 0000000..5319030
--- /dev/null
+++ b/apps/web/src/assets/sprite-data.json
@@ -0,0 +1,160 @@
+{
+ "name": "manoel",
+ "rows": 24,
+ "cols": 17,
+ "_colorSlots": {
+ "L": "outline (immutable)",
+ "s": "skin highlight (immutable)",
+ "S": "skin shade (immutable)",
+ "e": "eye (immutable)",
+ "m": "mouth (immutable)",
+ "h": "hat main",
+ "w": "hat band",
+ "c": "shirt main",
+ "C": "shirt shade",
+ "p": "pants main",
+ "P": "pants shade",
+ "b": "boots"
+ },
+ "_legend": {
+ ".": "transparent",
+ "L": "#10131c",
+ "s": "#f3c79b",
+ "S": "#c98863",
+ "e": "#10131c",
+ "m": "#80484b",
+ "h": "#ffd84d",
+ "w": "#fde68a",
+ "c": "#00f0ff",
+ "C": "#0369a1",
+ "p": "#3a1f6e",
+ "P": "#2d1857",
+ "b": "#0f172a"
+ },
+ "grid": [
+ "....LLLLLLLLL....",
+ "..LLhhhhhhhhhLL..",
+ ".LhhhhhhhhhhhhhL.",
+ "LLhhhhhhhhhhhhhLL",
+ "LwwwwwwwwwwwwwwwL",
+ "LLLLLLLLLLLLLLLLL",
+ "....LssssssssL...",
+ "...LssssssssssL..",
+ "..LsseSssssSesL..",
+ "..LssssssssssL...",
+ "..LSsLmmmmmLSsL..",
+ "...LSssssssSSL...",
+ "....LLsssssLL....",
+ "...LccccccccL....",
+ "..LcccCwwCccccL..",
+ ".LccccCwwCccccL..",
+ ".LccccCwwCccccL..",
+ ".LCCCCCCCCCCCCL..",
+ "..LpppLLLLpppL...",
+ "..LppppppppppL...",
+ "..LppppppppppL...",
+ "..LPPPPPPPPPPL...",
+ "..LLbbLL.LLbbLL..",
+ "...LLLL...LLLL..."
+ ],
+ "cosmetics": [
+ {
+ "id": "default",
+ "name": "Look Equipado (atual)",
+ "unlockLv": 7,
+ "preview": "manoel-default",
+ "theme": {}
+ },
+ {
+ "id": "lv01-tripulante",
+ "name": "Tripulante Iniciante",
+ "unlockLv": 1,
+ "preview": "manoel-lv01",
+ "_notes": "sem chapéu — h/w viram cabelo castanho",
+ "theme": {
+ "h": "#5a3a1d",
+ "w": "#5a3a1d",
+ "c": "#0ea5e9",
+ "C": "#075985",
+ "p": "#a16207",
+ "P": "#7c4a08",
+ "b": "#1e293b"
+ }
+ },
+ {
+ "id": "lv03-bone-verolme",
+ "name": "Boné Verolme",
+ "unlockLv": 3,
+ "preview": "manoel-lv03-bone-verolme",
+ "theme": {
+ "h": "#1e40af",
+ "w": "#fde68a",
+ "c": "#0ea5e9",
+ "C": "#075985",
+ "p": "#a16207",
+ "P": "#7c4a08",
+ "b": "#1e293b"
+ }
+ },
+ {
+ "id": "lv05-wingspan",
+ "name": "Camiseta Wingspan",
+ "unlockLv": 5,
+ "preview": "manoel-lv05-wingspan",
+ "theme": {
+ "h": "#ffd84d",
+ "w": "#fde68a",
+ "c": "#10b981",
+ "C": "#065f46",
+ "p": "#3a1f6e",
+ "P": "#2d1857",
+ "b": "#0f172a"
+ }
+ },
+ {
+ "id": "lv07-chapeu-sol",
+ "name": "Chapéu de Sol",
+ "unlockLv": 7,
+ "preview": "manoel-lv07-chapeu-sol",
+ "theme": {
+ "h": "#ffd84d",
+ "w": "#fde68a",
+ "c": "#00f0ff",
+ "C": "#0369a1",
+ "p": "#3a1f6e",
+ "P": "#2d1857",
+ "b": "#0f172a"
+ }
+ },
+ {
+ "id": "lv10-anti-vento",
+ "name": "Casaco Anti-Vento",
+ "unlockLv": 10,
+ "preview": "manoel-lv10-anti-vento",
+ "theme": {
+ "h": "#1e40af",
+ "w": "#fde68a",
+ "c": "#a855f7",
+ "C": "#581c87",
+ "p": "#1e293b",
+ "P": "#0f172a",
+ "b": "#0f172a"
+ }
+ },
+ {
+ "id": "lv15-capitao",
+ "name": "Trajado de Capitão",
+ "unlockLv": 15,
+ "preview": "manoel-lv15-capitao",
+ "theme": {
+ "h": "#ffd84d",
+ "w": "#dc2626",
+ "c": "#dc2626",
+ "C": "#7a0e0e",
+ "p": "#1c1917",
+ "P": "#0c0a09",
+ "b": "#0c0a09"
+ }
+ }
+ ]
+}
diff --git a/apps/web/src/assets/sprites/manoel-default-4x.png b/apps/web/src/assets/sprites/manoel-default-4x.png
new file mode 100644
index 0000000..03d72dc
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-default-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-default.png b/apps/web/src/assets/sprites/manoel-default.png
new file mode 100644
index 0000000..a391da2
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-default.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv01-4x.png b/apps/web/src/assets/sprites/manoel-lv01-4x.png
new file mode 100644
index 0000000..5be578e
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv01-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv01.png b/apps/web/src/assets/sprites/manoel-lv01.png
new file mode 100644
index 0000000..c010162
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv01.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png
new file mode 100644
index 0000000..fcbae5e
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png
new file mode 100644
index 0000000..c679f2d
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv03-bone-verolme.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png b/apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png
new file mode 100644
index 0000000..b92a448
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv05-wingspan-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv05-wingspan.png b/apps/web/src/assets/sprites/manoel-lv05-wingspan.png
new file mode 100644
index 0000000..c161509
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv05-wingspan.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png
new file mode 100644
index 0000000..03d72dc
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png
new file mode 100644
index 0000000..a391da2
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv07-chapeu-sol.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png b/apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png
new file mode 100644
index 0000000..9143d46
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv10-anti-vento-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv10-anti-vento.png b/apps/web/src/assets/sprites/manoel-lv10-anti-vento.png
new file mode 100644
index 0000000..9938031
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv10-anti-vento.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png b/apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png
new file mode 100644
index 0000000..9a2ef2c
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv15-capitao-4x.png differ
diff --git a/apps/web/src/assets/sprites/manoel-lv15-capitao.png b/apps/web/src/assets/sprites/manoel-lv15-capitao.png
new file mode 100644
index 0000000..04f5295
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-lv15-capitao.png differ
diff --git a/apps/web/src/assets/sprites/manoel-sheet-4x.png b/apps/web/src/assets/sprites/manoel-sheet-4x.png
new file mode 100644
index 0000000..94dd71c
Binary files /dev/null and b/apps/web/src/assets/sprites/manoel-sheet-4x.png differ
diff --git a/apps/web/src/components/AppHud.vue b/apps/web/src/components/AppHud.vue
new file mode 100644
index 0000000..2be94e0
--- /dev/null
+++ b/apps/web/src/components/AppHud.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
CARVALHO
+
FIN.SYS v2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/BottomNav.vue b/apps/web/src/components/BottomNav.vue
new file mode 100644
index 0000000..c6c0edc
--- /dev/null
+++ b/apps/web/src/components/BottomNav.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/apps/web/src/components/CharacterSprite.vue b/apps/web/src/components/CharacterSprite.vue
new file mode 100644
index 0000000..5a79d73
--- /dev/null
+++ b/apps/web/src/components/CharacterSprite.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/HUDStat.vue b/apps/web/src/components/HUDStat.vue
new file mode 100644
index 0000000..14ba543
--- /dev/null
+++ b/apps/web/src/components/HUDStat.vue
@@ -0,0 +1,48 @@
+
+
+
{{ label }}
+
+ {{ value }}
+ {{ sub }}
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/LevelUpModal.vue b/apps/web/src/components/LevelUpModal.vue
new file mode 100644
index 0000000..0245068
--- /dev/null
+++ b/apps/web/src/components/LevelUpModal.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+1
+
LEVEL UP!
+
{{ level }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/MonthSwitcher.vue b/apps/web/src/components/MonthSwitcher.vue
new file mode 100644
index 0000000..9ae334b
--- /dev/null
+++ b/apps/web/src/components/MonthSwitcher.vue
@@ -0,0 +1,34 @@
+
+
+
+ {{ label }}
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/NeonPanel.vue b/apps/web/src/components/NeonPanel.vue
new file mode 100644
index 0000000..08d7326
--- /dev/null
+++ b/apps/web/src/components/NeonPanel.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/XPBar.vue b/apps/web/src/components/XPBar.vue
index 4b084ad..603ac13 100644
--- a/apps/web/src/components/XPBar.vue
+++ b/apps/web/src/components/XPBar.vue
@@ -1,42 +1,45 @@
-
-
- Nv {{ level }}
- {{ xp }} / {{ xpToNext }} XP
-
-
+
diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts
index f668e8c..c44958a 100644
--- a/apps/web/src/main.ts
+++ b/apps/web/src/main.ts
@@ -1,6 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
+import './styles/tokens.css'
import App from './App.vue'
import router from './router'
diff --git a/apps/web/src/stores/game.ts b/apps/web/src/stores/game.ts
index d32bf20..ceb020e 100644
--- a/apps/web/src/stores/game.ts
+++ b/apps/web/src/stores/game.ts
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
+import spriteData from '@/assets/sprite-data.json'
export interface PlayerProfile {
id: number
@@ -76,5 +77,32 @@ export const useGameStore = defineStore('game', () => {
await fetchSummary()
}
- return { summary, loading, fetchSummary, claimQuest, equipCosmetic }
+ // Merge sprite theme from equipped cosmetics.
+ // css_data JSON may contain a "sprite_id" matching sprite-data.json cosmetics,
+ // OR the full theme directly. Falls back to empty (default sprite colors).
+ const equippedTheme = computed
>(() => {
+ if (!summary.value) return {}
+ const equipped = summary.value.equipped_cosmetics
+ if (!equipped.length) return {}
+ return equipped.reduce((acc, c) => {
+ try {
+ const data = JSON.parse(c.css_data)
+ // If css_data has sprite_id, look up in sprite-data.json
+ if (data.sprite_id) {
+ const spr = (spriteData.cosmetics as any[]).find(x => x.id === data.sprite_id)
+ if (spr?.theme) return { ...acc, ...spr.theme }
+ }
+ // Otherwise treat css_data itself as theme (keys h/c/p/b etc.)
+ const themeKeys = ['h','w','c','C','p','P','b']
+ const direct = Object.fromEntries(
+ Object.entries(data).filter(([k]) => themeKeys.includes(k))
+ ) as Record
+ return { ...acc, ...direct }
+ } catch { return acc }
+ }, {} as Record)
+ })
+
+ const justLeveledUp = ref(false)
+
+ return { summary, loading, equippedTheme, justLeveledUp, fetchSummary, claimQuest, equipCosmetic }
})
diff --git a/apps/web/src/styles/tokens.css b/apps/web/src/styles/tokens.css
new file mode 100644
index 0000000..1c5cc83
--- /dev/null
+++ b/apps/web/src/styles/tokens.css
@@ -0,0 +1,236 @@
+/* Financeiro Carvalho · Arcade Neon
+ * Drop this into src/styles/tokens.css and import once in main.ts:
+ * import './styles/tokens.css'
+ *
+ * All other components MUST consume var(--fc-*) instead of literal hex.
+ */
+
+@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700&display=swap');
+
+:root {
+ /* ─────────────── color tokens ─────────────── */
+ --fc-bg: #0a0418;
+ --fc-bg-raised: #150827;
+ --fc-bg-panel: #1c0d33;
+ --fc-panel-edge: #2d1857;
+ --fc-line: #3a1f6e;
+
+ --fc-text: #f4eaff;
+ --fc-text-dim: #8b75b8;
+
+ --fc-accent: #ff2d95; /* magenta — alerts, "today", primary CTA accent */
+ --fc-accent-2: #00f0ff; /* cyan — links, nav, "weekly" */
+ --fc-accent-3: #a855f7; /* purple — structure, glow, default button */
+ --fc-gold: #ffd84d; /* XP, achievements, "monthly" */
+ --fc-green: #39ff7a; /* goal hit, positive value */
+ --fc-red: #ff3b6b; /* goal missed, critical alert */
+
+ /* rgb fragments for shadows / alphas (no color-mix support? use these) */
+ --fc-accent-rgb: 255 45 149;
+ --fc-accent-2-rgb: 0 240 255;
+ --fc-accent-3-rgb: 168 85 247;
+ --fc-gold-rgb: 255 216 77;
+ --fc-green-rgb: 57 255 122;
+ --fc-red-rgb: 255 59 107;
+
+ /* ─────────────── sprite color slots ─────────────── */
+ --fc-sprite-outline: #10131c;
+ --fc-sprite-skin: #f3c79b;
+ --fc-sprite-skin-shade: #c98863;
+ --fc-sprite-eye: #10131c;
+ --fc-sprite-mouth: #80484b;
+ --fc-sprite-hat: #ffd84d;
+ --fc-sprite-band: #fde68a;
+ --fc-sprite-shirt: #00f0ff;
+ --fc-sprite-shirt-shade: #0369a1;
+ --fc-sprite-pants: #3a1f6e;
+ --fc-sprite-pants-shade: #2d1857;
+ --fc-sprite-boots: #0f172a;
+
+ /* ─────────────── sizing ─────────────── */
+ --fc-radius-sm: 2px;
+ --fc-radius: 4px;
+ --fc-radius-lg: 6px;
+
+ --fc-space-1: 4px;
+ --fc-space-2: 8px;
+ --fc-space-3: 12px;
+ --fc-space-4: 16px;
+ --fc-space-5: 24px;
+
+ /* ─────────────── shadows ─────────────── */
+ --fc-shadow-panel:
+ inset 0 1px 0 rgba(255,255,255,.05),
+ 0 0 0 1px rgba(0,0,0,.4),
+ 0 8px 24px rgba(0,0,0,.4);
+
+ --fc-shadow-panel-glow:
+ inset 0 1px 0 rgba(255,255,255,.05),
+ 0 0 0 1px var(--fc-accent-3),
+ 0 0 24px rgba(168,85,247,.25),
+ 0 8px 24px rgba(0,0,0,.5);
+
+ /* ─────────────── fonts ─────────────── */
+ --fc-font-pixel: 'Press Start 2P', monospace;
+ --fc-font-mono: 'JetBrains Mono', ui-monospace, monospace;
+ --fc-font-body: 'Inter', system-ui, sans-serif;
+}
+
+/* Global reset for the app root */
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--fc-bg);
+ color: var(--fc-text);
+ font-family: var(--fc-font-body);
+}
+* { box-sizing: border-box; }
+
+/* Util classes */
+.fc-pixel { font-family: var(--fc-font-pixel); letter-spacing: .02em; }
+.fc-mono { font-family: var(--fc-font-mono); }
+.fc-body { font-family: var(--fc-font-body); }
+
+.fc-text-dim { color: var(--fc-text-dim); }
+.fc-text-accent { color: var(--fc-accent); }
+.fc-text-accent2 { color: var(--fc-accent-2); }
+.fc-text-accent3 { color: var(--fc-accent-3); }
+.fc-text-gold { color: var(--fc-gold); }
+.fc-text-green { color: var(--fc-green); }
+.fc-text-red { color: var(--fc-red); }
+
+.fc-glow-cyan { text-shadow: 0 0 8px rgba(0,240,255,.6); }
+.fc-glow-magenta { text-shadow: 0 0 8px rgba(255,45,149,.6); }
+.fc-glow-purple { text-shadow: 0 0 8px rgba(168,85,247,.6); }
+.fc-glow-gold { text-shadow: 0 0 8px rgba(255,216,77,.6); }
+.fc-glow-green { text-shadow: 0 0 8px rgba(57,255,122,.6); }
+.fc-glow-red { text-shadow: 0 0 8px rgba(255,59,107,.6); }
+
+/* App shell — wrap your in */
+.fc-app {
+ background:
+ radial-gradient(ellipse 80% 50% at 50% 0%, rgba(168,85,247,.18), transparent 70%),
+ radial-gradient(ellipse 60% 40% at 80% 100%, rgba(255,45,149,.12), transparent 60%),
+ var(--fc-bg);
+ position: relative;
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+.fc-app::before {
+ content: '';
+ position: absolute; inset: 0;
+ background-image: repeating-linear-gradient(0deg, rgba(255,255,255,.025) 0 1px, transparent 1px 3px);
+ pointer-events: none;
+ z-index: 2;
+}
+.fc-app::after {
+ content: '';
+ position: absolute; inset: 0;
+ background-image:
+ linear-gradient(var(--fc-line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--fc-line) 1px, transparent 1px);
+ background-size: 32px 32px;
+ opacity: .12;
+ pointer-events: none;
+}
+.fc-app > * { position: relative; z-index: 1; }
+
+/* Blinking utility for critical alerts (recurrence missing, etc.) */
+@keyframes fc-blink { 0%,49% { opacity: 1 } 50%,100% { opacity: 0.2 } }
+.fc-blink { animation: fc-blink 1.2s steps(1) infinite; }
+
+/* ── Form elements ───────────────────────────────────────────── */
+.fc-input,
+.fc-select {
+ font-family: var(--fc-font-mono);
+ font-size: 12px;
+ padding: 8px 10px;
+ background: var(--fc-bg);
+ border: 1px solid var(--fc-panel-edge);
+ border-radius: var(--fc-radius);
+ color: var(--fc-text);
+ outline: none;
+ transition: border-color .15s, box-shadow .15s;
+ width: 100%;
+}
+.fc-input::placeholder { color: var(--fc-text-dim); }
+.fc-input:focus,
+.fc-select:focus {
+ border-color: var(--fc-accent-3);
+ box-shadow: 0 0 8px rgba(168,85,247,.3);
+}
+.fc-select {
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%238b75b8'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 10px center;
+ padding-right: 28px;
+}
+.fc-select option { background: var(--fc-bg-panel); color: var(--fc-text); }
+
+.fc-label {
+ font-family: var(--fc-font-mono);
+ font-size: 10px;
+ color: var(--fc-text-dim);
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+/* ── Buttons ─────────────────────────────────────────────────── */
+.fc-btn {
+ font-family: var(--fc-font-pixel);
+ font-size: 8px;
+ letter-spacing: .04em;
+ padding: 8px 14px;
+ border: 2px solid var(--fc-panel-edge);
+ border-radius: var(--fc-radius);
+ background: var(--fc-bg-raised);
+ color: var(--fc-text);
+ cursor: pointer;
+ transition: border-color .12s, box-shadow .12s, background .12s;
+ white-space: nowrap;
+}
+.fc-btn:disabled { opacity: .45; cursor: not-allowed; }
+
+.fc-btn--primary {
+ border-color: var(--fc-accent-3);
+ color: var(--fc-accent-3);
+}
+.fc-btn--primary:not(:disabled):hover {
+ background: var(--fc-accent-3);
+ color: var(--fc-bg);
+ box-shadow: 0 0 14px rgba(168,85,247,.5);
+}
+
+.fc-btn--pink {
+ border-color: var(--fc-accent);
+ color: var(--fc-accent);
+}
+.fc-btn--pink:not(:disabled):hover {
+ background: var(--fc-accent);
+ color: #fff;
+ box-shadow: 0 0 14px rgba(255,45,149,.5);
+}
+
+.fc-btn--ghost {
+ border-color: transparent;
+ color: var(--fc-text-dim);
+}
+.fc-btn--ghost:hover { color: var(--fc-text); border-color: var(--fc-panel-edge); }
+
+.fc-btn--danger {
+ border-color: var(--fc-red);
+ color: var(--fc-red);
+}
+.fc-btn--danger:not(:disabled):hover {
+ background: var(--fc-red);
+ color: #fff;
+ box-shadow: 0 0 14px rgba(255,59,107,.5);
+}
+
+.fc-btn--sm {
+ font-size: 7px;
+ padding: 5px 10px;
+}
diff --git a/apps/web/src/views/AccountsView.vue b/apps/web/src/views/AccountsView.vue
index b4d564e..0efff24 100644
--- a/apps/web/src/views/AccountsView.vue
+++ b/apps/web/src/views/AccountsView.vue
@@ -1,9 +1,9 @@
-