diff --git a/DESIGN.md b/DESIGN.md
deleted file mode 100644
index a89db51..0000000
--- a/DESIGN.md
+++ /dev/null
@@ -1,183 +0,0 @@
-# Design
-
-## Overview
-
-DataBreeze is a product UI, not a brand landing page. The design system should support a logged-in dashboard app with upload workflows, mapping tables, validation states, KPI summaries, cost management, and plan usage.
-
-Design mood: a morning seller workspace, clean white desk, crisp cobalt brand mark, quiet operational clarity.
-
-Color strategy: Restrained. The interface uses neutral work surfaces with one primary cobalt action accent from the DataBreeze logo. Semantic colors are reserved for status only.
-
-Theme strategy: light-first with system dark-mode support later. The first implementation should not create section-level theme flips.
-
-## Color Tokens
-
-Use OKLCH custom properties. Do not introduce hex color literals in source styles.
-
-```css
-:root {
- --color-bg: oklch(1.000 0.000 0);
- --color-surface: oklch(0.976 0.004 255);
- --color-surface-strong: oklch(0.948 0.007 255);
- --color-ink: oklch(0.205 0.018 255);
- --color-muted: oklch(0.470 0.020 255);
- --color-border: oklch(0.900 0.010 255);
-
- --color-primary: oklch(0.600 0.210 262);
- --color-primary-hover: oklch(0.540 0.220 262);
- --color-primary-soft: oklch(0.955 0.038 262);
-
- --color-accent: oklch(0.620 0.115 178);
- --color-accent-soft: oklch(0.950 0.035 178);
-
- --color-success: oklch(0.560 0.120 145);
- --color-warning: oklch(0.720 0.135 75);
- --color-danger: oklch(0.560 0.145 25);
- --color-info: oklch(0.570 0.115 245);
-}
-```
-
-Primary cobalt is used for the main action and selected app state. Teal is a secondary accent for freshness, successful data quality, or non-primary emphasis. Red is reserved for danger and destructive states. Do not use teal as a competing CTA color.
-
-## Typography
-
-Use one product sans family:
-
-```css
-font-family: "Be Vietnam Pro", "Noto Sans", "Segoe UI", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
-```
-
-Be Vietnam Pro is the preferred product font because the primary UI language is Vietnamese and diacritics must feel native, not bolted on. Avoid display fonts, serif emphasis, decorative letter spacing, and viewport-scaled type.
-
-Type scale:
-
-- Page title: 28px, 700, 1.15 line height.
-- Section title: 18px, 650, 1.25 line height.
-- Body: 14px or 15px, 400, 1.55 line height.
-- UI label: 12px or 13px, 600, 1.25 line height.
-- Table cell: 13px or 14px, 400, 1.4 line height.
-- Numeric KPI: 26px to 34px, 700, tabular numbers.
-
-Use `font-variant-numeric: tabular-nums` for KPI values, currency, percentages, dates, and table numbers.
-
-## Shape And Elevation
-
-Radius system:
-
-- Small controls: 6px.
-- Buttons, inputs, badges: 8px.
-- Panels and cards: 8px maximum.
-- Full pill only for compact badges or segmented controls where the pill shape carries state.
-
-Elevation should be rare. Prefer borders, dividers, spacing, and background layers. Do not combine a 1px border with a wide decorative shadow.
-
-## Layout System
-
-Primary app structure:
-
-- Top bar: 64px max height.
-- Sidebar: compact, 232px desktop width.
-- Main content: fluid, max useful reading width per surface, with dashboard grids expanding when data needs it.
-- Page gutter: 24px desktop, 16px tablet, 12px mobile.
-- Section spacing: 20px to 32px inside product surfaces.
-
-Use CSS grid for dashboards and complex two-dimensional layouts. Use flex for toolbar rows and inline control groups.
-
-Avoid app-wide hero sections. The first screen after login should be the working surface: dashboard, empty upload prompt, setup flow, or upload state.
-
-## Component Vocabulary
-
-Core components:
-
-- App shell.
-- Top bar.
-- Sidebar navigation.
-- Workspace switcher.
-- Store selector.
-- Date range control.
-- Primary, secondary, ghost, and danger buttons.
-- Text input, select, file drop zone, checkbox, segmented control.
-- Status badge.
-- KPI tile.
-- Data table.
-- Empty state panel.
-- Error and warning callout.
-- Step indicator.
-- Mapping row.
-- Validation summary.
-- Insight item.
-- Usage meter.
-- Side panel or modal for create/edit flows.
-
-Every interactive component needs default, hover, focus-visible, active, disabled, loading, and error states where applicable.
-
-## Data Visualization
-
-Charts should be quiet and readable:
-
-- Use red only for primary selected series, negative movement, or critical risk.
-- Use teal or green only for positive or healthy states.
-- Use neutral gridlines and muted axes.
-- Always pair color with label, value, icon, or text cue.
-- Avoid fake precision. Seed data in demos must be clearly sample data in code comments or constants.
-
-Recommended chart library later: Recharts or another React chart library that keeps chart labels code-native and accessible.
-
-## Motion
-
-Motion intensity: low. Use 150ms to 220ms transitions for state changes, selected rows, drawer entry, hover feedback, and upload step progression.
-
-Rules:
-
-- No decorative page-load choreography.
-- No scroll hijacking.
-- No layout-property animation.
-- Respect `prefers-reduced-motion`.
-- Motion should communicate state, feedback, or progress.
-
-## Copy Rules
-
-Visible product copy should be short, operational, and Vietnamese-first where the docs specify Vietnamese labels.
-
-Use:
-
-- "Tải file mới"
-- "Tiếp tục map cột"
-- "Kiểm tra dữ liệu"
-- "Cập nhật dashboard"
-- "Thêm shop"
-- "Giá vốn bị thiếu"
-
-Avoid:
-
-- "Seamless insights"
-- "AI-powered growth engine"
-- "Unlock next-gen profit intelligence"
-- Technical terms like ETL, schema, row normalization, or pipeline in primary labels.
-
-## Responsive Rules
-
-Desktop:
-
-- Sidebar remains visible.
-- Tables use dense rows and sticky header where useful.
-- Mapping can use main plus helper rail.
-
-Tablet:
-
-- Sidebar can collapse to icons or drawer.
-- Filters wrap into two rows.
-- Tables may reduce optional columns.
-
-Mobile:
-
-- Sidebar becomes drawer or bottom navigation.
-- Tables become stacked records.
-- Primary action can be sticky at the bottom for upload and mapping flows.
-- Keep buttons single-line when possible; shorten labels before shrinking text.
-
-## Implementation Notes
-
-Start with the app shell, dashboard empty/data states, upload history, upload flow, mapping screen, validation results, costs, expenses, insights, and plan usage.
-
-Do not add a marketing landing page inside this product frontend unless explicitly requested later.
diff --git a/PRODUCT.md b/PRODUCT.md
deleted file mode 100644
index 894b3a8..0000000
--- a/PRODUCT.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Product
-
-## Register
-
-product
-
-## Users
-
-DataBreeze is designed first for Vietnamese e-commerce sellers who manage one to three shops, then for multi-shop sellers, SMEs, and small teams. These users work inside recurring operational routines: exporting marketplace files, checking revenue, adding missing costs, reviewing warnings, and deciding what to fix next.
-
-They are usually not data engineers. They need a product that turns messy CSV and Excel files into trusted profit views without asking them to understand ETL, SQL, schemas, or backend jobs.
-
-## Product Purpose
-
-DataBreeze is a Vietnamese-first profit workspace where sellers upload Shopee, TikTok Shop, ads, and expense files, map messy columns, validate import results, and see real profit by SKU, shop, campaign, and time period.
-
-Success means a seller can answer practical business questions quickly:
-
-- Which products actually make money?
-- Which SKUs are missing cost data?
-- Which shop, campaign, or date range is underperforming?
-- Which uploaded files imported cleanly, partially, or failed?
-- What action should happen next?
-
-The frontend should hide avoidable technical complexity while preserving enough visibility for trust, especially around uploads, mapping, validation, and data quality.
-
-## Brand Personality
-
-Calm, operational, trustworthy.
-
-DataBreeze should feel like a clear workspace for business decisions, not a marketing site, analytics toy, or generic AI dashboard. The voice should be direct, seller-friendly, and Vietnamese-first. It should feel helpful without being childish, and precise without sounding like enterprise software.
-
-## Anti-references
-
-Avoid these directions:
-
-- A marketing landing page as the first product screen.
-- Decorative SaaS gradients, generic purple-blue glow, or glass-heavy UI.
-- Dense enterprise dashboards before the user has enough data.
-- Fake AI chat as the core interaction.
-- Hiding mapping and validation details so deeply that users cannot trust the import.
-- Technical ETL language as primary UI copy.
-- Overbuilt team/admin settings before upload, mapping, dashboard, costs, and expenses work.
-- Repeated equal card grids where lists, tables, steps, or focused panels would be clearer.
-
-## Design Principles
-
-1. Start from the user's next decision.
- Every screen should make the next useful action obvious: upload, map, fix costs, review warnings, or inspect profit.
-
-2. Trust comes from visibility.
- Uploads, mappings, validation errors, skipped rows, and dashboard freshness should be visible and explainable.
-
-3. Vietnamese first, technical second.
- Use seller-friendly Vietnamese labels first. Technical names can appear as secondary helper text when needed.
-
-4. Solo-simple, team-ready.
- The product should feel easy for a solo seller while keeping room for workspaces, stores, roles, billing, and auditability later.
-
-5. Restraint earns confidence.
- Use familiar app patterns, compact navigation, calm surfaces, clear states, and limited accent color. Save visual emphasis for action, risk, and status.
-
-## Accessibility & Inclusion
-
-Target WCAG AA for the product UI. Body text, controls, placeholders, focus states, charts, status badges, and table text must remain readable in light and dark preferences.
-
-Required considerations:
-
-- Keyboard reachable navigation and forms.
-- Visible focus states on all controls.
-- Reduced motion support for transitions.
-- No color-only status communication.
-- Touch targets that work on mobile.
-- Responsive table alternatives for small screens.
-- Vietnamese text length support without clipped labels or overflowing buttons.
diff --git a/docs/AGENT_FRONTEND_GUIDE.md b/docs/AGENT_FRONTEND_GUIDE.md
deleted file mode 100644
index 09042c1..0000000
--- a/docs/AGENT_FRONTEND_GUIDE.md
+++ /dev/null
@@ -1,309 +0,0 @@
-# DataBreeze Frontend Agent Guide
-
-This guide is for agents working inside `FE_Databreeze`.
-
-## Current State
-
-`FE_Databreeze` is currently an empty Git repository with documentation only. There is no React app, package manifest, router, components, API client, or build setup yet.
-
-Agents should treat this folder as the future home of the DataBreeze product frontend.
-
-## Recommended Frontend Stack
-
-Long-term frontend stack:
-
-```text
-React
-TypeScript
-Vite
-React Router
-TanStack Query
-Tailwind CSS
-shadcn/ui or Radix-based primitives
-Recharts or a comparable dashboard chart library
-Zod for client-side schema validation where useful
-Vitest + Testing Library
-Playwright for end-to-end flow verification
-```
-
-Why this stack:
-
-- DataBreeze is a logged-in dashboard app, not an SEO-first website.
-- React + Vite keeps the product app simpler than Next.js.
-- The backend is already Spring Boot, so backend business logic should stay there.
-- TanStack Query is a good fit for server-state-heavy screens such as uploads, jobs, dashboards, costs, stores, and usage.
-
-Use Next.js only for a separate marketing/docs site if needed later.
-
-## Product Role Of The Frontend
-
-The frontend should make DataBreeze feel like:
-
-> A simple Vietnamese-first profit workspace where sellers upload messy files and quickly understand real revenue, costs, and profit.
-
-The UI must hide unnecessary ETL complexity while still making mapping, validation, and errors understandable.
-
-## Core UX Flow
-
-```text
-Auth
- -> first workspace/store setup
- -> dashboard empty state
- -> upload file
- -> map columns
- -> validate data
- -> run import/update dashboard
- -> view dashboard/insights
- -> fill missing costs
- -> recalculate profit
-```
-
-## Expected App Shell
-
-### Top Bar
-
-- Logo and product name.
-- Workspace switcher when the user belongs to more than one workspace.
-- Current user menu.
-- Optional usage/plan indicator for low-tier plans.
-
-### Sidebar
-
-Recommended initial navigation:
-
-- Dashboard
-- Uploads
-- Stores
-- Product Costs
-- Expenses
-- Insights
-- Plan & Usage
-- Settings later
-
-Keep the navigation compact and operational. This is a SaaS tool, not a marketing landing page.
-
-## Main Screens
-
-### Auth
-
-Screens:
-
-- Login.
-- Sign up.
-- Verify OTP.
-- Forgot password.
-- Reset password.
-
-Notes:
-
-- Keep forms simple.
-- Show inline validation.
-- Preserve Vietnamese-friendly copy.
-
-### First-Time Setup
-
-Screens:
-
-- Workspace setup.
-- Add first store.
-- Dashboard empty state.
-
-Goal:
-
-Get the user to the first upload as quickly as possible.
-
-### Dashboard
-
-States:
-
-- Empty dashboard with a clear upload CTA.
-- Data dashboard with filters, KPI cards, daily chart, top SKUs, data quality, and insights.
-
-Filters:
-
-- Workspace.
-- Store.
-- Date range.
-- Source/platform.
-
-KPI cards:
-
-- Revenue.
-- Gross profit.
-- Net profit.
-- Orders.
-- Margin.
-- Refunds or fees where useful.
-
-### Uploads
-
-Screens:
-
-- Upload history.
-- Upload detail.
-- New upload.
-- Mapping.
-- Validation/import result.
-- Error report download.
-
-The mapping screen is the most important frontend workflow. It must be understandable to non-technical sellers.
-
-### Stores
-
-Screens:
-
-- Store list.
-- Add/edit store.
-
-Store cards/list rows should show:
-
-- Store name.
-- Platform.
-- Status.
-- Last upload/import.
-- Basic performance summary when available.
-
-### Product Costs
-
-Screens:
-
-- Product cost list.
-- Add/edit cost.
-- Missing SKU costs.
-- Apply costs/recalculate dashboard.
-
-This area is critical because profit is unreliable without COGS.
-
-### Expenses
-
-Screens:
-
-- Operating expense list.
-- Add/edit expense.
-
-Expenses feed net profit and should be date/store-aware.
-
-### Insights
-
-Screens:
-
-- Insight list.
-- Generate insight.
-- Insight status update.
-
-Insights should be presented as practical business warnings, not vague AI advice.
-
-### Plan & Usage
-
-Screens:
-
-- Current plan.
-- Upload/row/store/member/token usage.
-- Upgrade prompts.
-
-The UI should explain limits before they surprise users.
-
-## Backend API Expectations
-
-The frontend should authenticate with the backend and send bearer tokens to protected endpoints.
-
-Core API groups:
-
-- `/api/v1/auth/**`
-- `/api/v1/me/workspaces`
-- `/api/v1/workspaces/**`
-- `/api/v1/workspaces/{workspaceId}/stores`
-- `/api/v1/workspaces/{workspaceId}/etl/**`
-- `/api/v1/workspaces/{workspaceId}/dashboard/**`
-- `/api/v1/workspaces/{workspaceId}/processed-data/**`
-- `/api/v1/workspaces/{workspaceId}/product-costs/**`
-- `/api/v1/workspaces/{workspaceId}/operating-expenses/**`
-- `/api/v1/workspaces/{workspaceId}/insights/**`
-- `/api/v1/workspaces/{workspaceId}/subscription`
-- `/api/v1/workspaces/{workspaceId}/usage`
-- `/api/v1/plans`
-
-Do not reintroduce `actorUserId` request params. Current backend controllers use JWT principal.
-
-## State Model
-
-Recommended state split:
-
-- Authentication/session state: small local app store or context.
-- Current workspace/store/date filters: URL params where shareable, local state where temporary.
-- Server state: TanStack Query.
-- Form state: local form libraries or controlled forms, depending on complexity.
-
-Do not duplicate server data into global client state unless there is a strong reason.
-
-## UI Principles
-
-### Vietnamese-First
-
-Use seller-friendly language. Avoid technical ETL language in primary UI.
-
-### Guided Steps
-
-The upload workflow should always show progress:
-
-```text
-Upload file -> Map columns -> Check data -> Update dashboard
-```
-
-### Clear Empty States
-
-Every no-data screen should have one clear primary action.
-
-### Gentle Errors
-
-Data errors should explain what to fix and how to continue.
-
-### Workspace Clarity
-
-The user should always know which workspace and store they are viewing.
-
-### Solo-First, Team-Ready
-
-The first UI should feel simple for one seller. Do not design in a way that blocks later organization workspaces, roles, and billing.
-
-## Implementation Guidance For Future Agents
-
-When scaffolding the app:
-
-1. Use Vite React TypeScript.
-2. Add routing early.
-3. Add a typed API client.
-4. Add auth token handling.
-5. Build the app shell before deep screens.
-6. Build upload/mapping/dashboard flows before secondary settings pages.
-7. Add Playwright smoke tests for login mock, upload flow mock, and dashboard rendering once possible.
-
-Suggested folders:
-
-```text
-src/
-|-- app/ # providers, router, app shell
-|-- api/ # API client and generated/manual types
-|-- components/ # shared UI components
-|-- features/
-| |-- auth/
-| |-- dashboard/
-| |-- uploads/
-| |-- mapping/
-| |-- stores/
-| |-- costs/
-| |-- expenses/
-| |-- insights/
-| `-- billing/
-|-- hooks/
-|-- lib/
-`-- styles/
-```
-
-## What Not To Build First
-
-- Do not start with a marketing landing page.
-- Do not overbuild admin/team settings before upload and dashboard work.
-- Do not build fake AI chat as the core experience.
-- Do not hide mapping details completely; users need trust.
-- Do not make the UI depend on unimplemented backend platforms such as TikTok or Google Ads without feature flags or placeholder states.
diff --git a/docs/UI_UX_BRIEF.md b/docs/UI_UX_BRIEF.md
deleted file mode 100644
index f85be7d..0000000
--- a/docs/UI_UX_BRIEF.md
+++ /dev/null
@@ -1,534 +0,0 @@
-# DataBreeze UI/UX Brief
-
-This brief is for designers and frontend agents. It describes the intended product experience, not an implemented frontend.
-
-## One-Line Product Definition
-
-DataBreeze is a Vietnamese-first profit workspace where sellers upload Shopee, TikTok Shop, ads, and expense files, then see real profit by SKU, shop, and campaign.
-
-## Design Audience
-
-Design primarily for:
-
-- Solo sellers with one to three shops.
-- Multi-shop sellers.
-- Later, small teams and SMEs.
-
-The interface should feel simple enough for one person and structured enough to support teams later.
-
-## Overall Layout
-
-### Top Bar
-
-- Logo and product name.
-- Workspace switcher when the user has more than one workspace.
-- User menu with account settings and logout.
-
-### Sidebar
-
-Initial navigation:
-
-- Dashboard
-- Uploads
-- Stores
-- Product Costs
-- Expenses
-- Insights
-- Plan & Usage
-
-### Content Area
-
-Each page should have:
-
-- Page title.
-- Main action button where relevant.
-- Filters where relevant.
-- Main content.
-- Clear empty/error/loading states.
-
-## 1. Auth Screens
-
-### Login
-
-Goal: Let users enter quickly.
-
-Content:
-
-- Title: "Dang nhap vao DataBreeze"
-- Email field.
-- Password field.
-- Primary button: "Dang nhap"
-- Link: "Quen mat khau?"
-- Secondary link: "Chua co tai khoan? Dang ky"
-
-States:
-
-- Inline field errors.
-- Loading button state.
-- Friendly auth error.
-
-### Sign Up
-
-Goal: Create account and prepare the first workspace.
-
-Content:
-
-- Title: "Tao tai khoan moi"
-- Name field.
-- Email field.
-- Password field.
-- Primary button: "Tiep tuc"
-- Link: "Da co tai khoan? Dang nhap"
-
-After success:
-
-- Continue to OTP verification if required.
-- Then continue to first-time workspace setup.
-
-### Forgot/Reset Password
-
-Use the same simple card layout. Keep copy direct and reassuring.
-
-## 2. First-Time Setup
-
-### Workspace Setup
-
-Goal: Create or confirm the personal workspace without overwhelming the user.
-
-Content:
-
-- Title: "Thiet lap khong gian lam viec"
-- Short explanation: this name represents the user's business/shop workspace.
-- Workspace name field, prefilled if possible.
-- Primary button: "Tiep tuc"
-
-### Add First Store
-
-Goal: Introduce the store/shop concept.
-
-Content:
-
-- Title: "Them shop dau tien"
-- Store name field.
-- Platform dropdown: Shopee, TikTok Shop, Other.
-- Primary button: "Them shop"
-- Secondary link: "Bo qua, toi se them sau"
-
-After adding a store:
-
-- Show a small store card with platform badge and store name.
-- Primary button: "Vao bang dieu khien"
-
-## 3. Dashboard
-
-### Empty State
-
-Goal: Push the user to first upload.
-
-Content:
-
-- Title: "Bang dieu khien loi nhuan"
-- Empty message: "Chua co du lieu nao."
-- Subtext: "Tai file tu Shopee, TikTok Shop, Google Ads hoac chi phi de xem loi nhuan thuc te."
-- Primary button: "Tai file dau tien"
-
-### Dashboard With Data
-
-Goal: Show a clear profit picture.
-
-Filters:
-
-- Store selector: all stores or one store.
-- Date range.
-- Source/platform.
-
-KPI cards:
-
-- Doanh thu.
-- Loi nhuan gop.
-- Loi nhuan rong.
-- So don hang.
-- Bien loi nhuan.
-- Refunds/fees where useful.
-
-Main panels:
-
-- Profit over time chart.
-- Top SKUs by profit table.
-- Store comparison.
-- Data quality card.
-- Insight/warning card.
-
-Interactions:
-
-- Click SKU to open SKU detail later.
-- Click store to filter or open store detail.
-- Click insight to view supporting details.
-
-## 4. Stores Page
-
-### Store List
-
-Goal: Let users manage shops in the current workspace.
-
-Header:
-
-- Title: "Shop"
-- Button: "Them shop"
-
-Store card/list item:
-
-- Platform badge.
-- Store name.
-- Status.
-- Last upload/import.
-- Upload count or data status.
-
-Empty state:
-
-- Message: "Chua co shop nao."
-- Button: "Them shop"
-
-### Add/Edit Store
-
-Use a modal or side panel.
-
-Fields:
-
-- Store name.
-- Platform.
-- External store ID optional.
-- Country/currency advanced or hidden by default.
-
-## 5. Uploads Page
-
-### Upload History
-
-Goal: Show file processing history.
-
-Header:
-
-- Title: "Tai len va xu ly"
-- Button: "Tai file moi"
-
-Filters:
-
-- Store.
-- Data source.
-- Status.
-- Date range.
-
-Table columns:
-
-- File name.
-- Store.
-- Source type.
-- Upload time.
-- Status badge.
-- Row count.
-- Action: details.
-
-Statuses:
-
-- Waiting for mapping.
-- Validating.
-- Running.
-- Completed.
-- Failed.
-- Warning.
-
-### Upload Detail
-
-Show:
-
-- File metadata.
-- Import jobs.
-- Mapping status.
-- Row counts.
-- Error report download if available.
-- Dashboard link if completed.
-
-## 6. Upload Flow
-
-### Upload New File
-
-Goal: Make upload safe and guided.
-
-Fields:
-
-- Store dropdown.
-- Data source type dropdown.
-- File drop zone.
-
-Drop zone text:
-
-- "Keo tha file Excel/CSV vao day hoac chon tu may."
-
-Accepted:
-
-- `.xlsx`
-- `.xls`
-- `.csv`
-
-After file select:
-
-- Show file name.
-- Show source/store.
-- Primary button: "Tiep tuc map cot"
-
-### Mapping Screen
-
-This is the critical UX screen.
-
-Goal: Make column mapping understandable to non-technical sellers.
-
-Top context:
-
-- File name.
-- Store.
-- Source type.
-- Step indicator:
-
-```text
-1. Upload -> 2. Map cot -> 3. Kiem tra -> 4. Hoan tat
-```
-
-Main layout:
-
-- Left/main area: detected source columns.
-- Right/help area: standard DataBreeze fields grouped by category.
-
-Each mapping row:
-
-- Source column name.
-- Example value.
-- Suggested target field dropdown.
-- Confidence/status pill.
-- Required-field warning if relevant.
-
-Target field groups:
-
-- Order basics.
-- Revenue and fees.
-- Product and SKU.
-- Costs.
-- Ads later.
-
-Bottom actions:
-
-- Checkbox: "Luu mapping nay cho file cung loai sau nay."
-- Secondary button: "Quay lai"
-- Primary button: "Tiep tuc kiem tra du lieu"
-
-UX notes:
-
-- Required missing fields should be obvious.
-- Do not block users with vague errors.
-- Explain field names in Vietnamese first, technical field name second.
-
-## 7. Validation Results
-
-Goal: Give confidence and show data problems clearly.
-
-Summary cards:
-
-- Total rows.
-- Valid rows.
-- Warning rows.
-- Failed rows.
-
-Warning/error list examples:
-
-- Missing SKU cost.
-- Missing required mapping.
-- Invalid dates.
-- Duplicate order IDs.
-- Empty SKU.
-
-Actions:
-
-- Primary: "Dong y va cap nhat dashboard"
-- Secondary: "Tai file loi"
-- Secondary: "Quay lai map cot"
-
-Design tone:
-
-- Warnings should be calm and actionable.
-- The user should understand what will still import and what will not.
-
-## 8. Product Costs
-
-Goal: Help users make profit accurate.
-
-Screens:
-
-- Cost list.
-- Missing cost SKUs.
-- Add/edit product cost.
-- Apply costs.
-
-Cost row:
-
-- SKU.
-- Product name if known.
-- Unit cost.
-- Effective date.
-- Currency.
-- Status.
-
-Missing cost view:
-
-- SKU.
-- Product name.
-- Order item count.
-- Quantity sold.
-- Revenue affected.
-- CTA to add cost.
-
-## 9. Expenses
-
-Goal: Capture non-COGS costs affecting net profit.
-
-Fields:
-
-- Store optional.
-- Expense date.
-- Category.
-- Description.
-- Amount.
-- Currency.
-
-UX note:
-
-- Expenses should feel like accounting-lite, not a complex ledger.
-
-## 10. Insights
-
-Goal: Explain business risks and opportunities.
-
-Insight card:
-
-- Severity.
-- Title.
-- Short summary.
-- Metric.
-- Recommended action.
-- Status: open, resolved, ignored.
-
-Examples:
-
-- "SKU co doanh thu nhung dang lo."
-- "Mot so dong ban hang thieu gia von."
-- "Ty le hoan tien cao."
-- "Bien loi nhuan thap."
-
-Insights should link back to dashboard/product/cost context when possible.
-
-## 11. Plan & Usage
-
-Goal: Make limits understandable.
-
-Show:
-
-- Current plan.
-- Uploads used.
-- Rows processed.
-- Stores used.
-- AI token usage if visible.
-- Insight generation count.
-
-Upgrade prompts:
-
-- Friendly, clear, tied to the action the user tried to perform.
-
-Example:
-
-```text
-Ban da dat gioi han shop cua goi hien tai.
-Nang cap de them nhieu shop hon va xu ly nhieu dong hon moi thang.
-```
-
-## 12. Detail Dashboards Later
-
-### SKU Detail
-
-Show:
-
-- Revenue.
-- Profit.
-- Quantity sold.
-- Orders.
-- Margin.
-- Profit over time.
-- Orders containing this SKU.
-
-### Store Detail
-
-Show:
-
-- Revenue.
-- Profit.
-- Upload history.
-- Top SKUs.
-- Data quality.
-
-### Campaign Detail
-
-Later when ads imports exist:
-
-- Spend.
-- Revenue.
-- ROAS.
-- Profit.
-- Conversions.
-- Daily trend.
-
-## Mobile Behavior
-
-On mobile:
-
-- Collapse sidebar into bottom nav or drawer.
-- Turn tables into cards.
-- Use filter sheets/drawers.
-- Keep primary action sticky when useful.
-- Prioritize upload status, KPI cards, and warnings.
-
-## Component Inventory
-
-Core components:
-
-- App shell.
-- Workspace switcher.
-- Store selector.
-- Date range picker.
-- KPI card.
-- Status badge.
-- Upload drop zone.
-- Step indicator.
-- Mapping row.
-- Target field dropdown.
-- Validation summary card.
-- Data quality card.
-- Insight card.
-- Empty state.
-- Error report download button.
-- Usage meter.
-- Confirm modal.
-
-## Visual Tone
-
-DataBreeze should feel:
-
-- Trustworthy.
-- Calm.
-- Clear.
-- Operational.
-- Vietnamese-first.
-- Helpful without feeling childish.
-
-Avoid:
-
-- Overly decorative landing-page patterns inside the app.
-- Dense enterprise dashboards too early.
-- Purple/blue generic SaaS sameness if a more grounded palette is chosen.
-- Technical jargon as primary labels.
diff --git a/index.html b/index.html
index 6161fe0..a0c5067 100644
--- a/index.html
+++ b/index.html
@@ -1,20 +1,9 @@
-
+
-
-
-
-
-
- DataBreeze
+ Workspace
diff --git a/package-lock.json b/package-lock.json
index 3620afe..294083c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,19 +8,14 @@
"name": "fe-databreeze",
"version": "0.1.0",
"dependencies": {
- "@phosphor-icons/react": "^2.1.10",
- "@tanstack/react-query": "^5.101.0",
"react": "^19.2.7",
- "react-dom": "^19.2.7",
- "react-router-dom": "^7.18.0"
+ "react-dom": "^19.2.7"
},
"devDependencies": {
- "@tailwindcss/vite": "^4.3.1",
"@types/node": "^26.0.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
- "tailwindcss": "^4.3.1",
"typescript": "^6.0.3",
"vite": "^8.0.16"
}
@@ -59,56 +54,6 @@
"tslib": "^2.4.0"
}
},
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
@@ -138,19 +83,6 @@
"url": "https://github.com/sponsors/Boshen"
}
},
- "node_modules/@phosphor-icons/react": {
- "version": "2.1.10",
- "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
- "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "react": ">= 16.8",
- "react-dom": ">= 16.8"
- }
- },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
@@ -244,9 +176,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -264,9 +193,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -284,9 +210,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -304,9 +227,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -324,9 +244,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -344,9 +261,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -433,316 +347,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@tailwindcss/node": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
- "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/remapping": "^2.3.5",
- "enhanced-resolve": "5.21.6",
- "jiti": "^2.7.0",
- "lightningcss": "1.32.0",
- "magic-string": "^0.30.21",
- "source-map-js": "^1.2.1",
- "tailwindcss": "4.3.1"
- }
- },
- "node_modules/@tailwindcss/oxide": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
- "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 20"
- },
- "optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.3.1",
- "@tailwindcss/oxide-darwin-arm64": "4.3.1",
- "@tailwindcss/oxide-darwin-x64": "4.3.1",
- "@tailwindcss/oxide-freebsd-x64": "4.3.1",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
- "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
- "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
- "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
- "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
- "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
- }
- },
- "node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
- "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
- "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
- "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
- "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
- "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
- "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
- "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
- "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
- "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
- "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
- "bundleDependencies": [
- "@napi-rs/wasm-runtime",
- "@emnapi/core",
- "@emnapi/runtime",
- "@tybys/wasm-util",
- "@emnapi/wasi-threads",
- "tslib"
- ],
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.10.0",
- "@emnapi/runtime": "^1.10.0",
- "@emnapi/wasi-threads": "^1.2.1",
- "@napi-rs/wasm-runtime": "^1.1.4",
- "@tybys/wasm-util": "^0.10.2",
- "tslib": "^2.8.1"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
- "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
- "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 20"
- }
- },
- "node_modules/@tailwindcss/vite": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
- "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@tailwindcss/node": "4.3.1",
- "@tailwindcss/oxide": "4.3.1",
- "tailwindcss": "4.3.1"
- },
- "peerDependencies": {
- "vite": "^5.2.0 || ^6 || ^7 || ^8"
- }
- },
- "node_modules/@tanstack/query-core": {
- "version": "5.101.0",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
- "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/react-query": {
- "version": "5.101.0",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
- "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==",
- "license": "MIT",
- "dependencies": {
- "@tanstack/query-core": "5.101.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- },
- "peerDependencies": {
- "react": "^18 || ^19"
- }
- },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -810,19 +414,6 @@
}
}
},
- "node_modules/cookie": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
- "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -840,20 +431,6 @@
"node": ">=8"
}
},
- "node_modules/enhanced-resolve": {
- "version": "5.21.6",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
- "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "graceful-fs": "^4.2.4",
- "tapable": "^2.3.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -887,19 +464,14 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -1047,9 +619,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1071,9 +640,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1095,9 +661,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1119,9 +682,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1177,16 +737,6 @@
"url": "https://opencollective.com/parcel"
}
},
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
"node_modules/nanoid": {
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
@@ -1276,44 +826,6 @@
"react": "^19.2.7"
}
},
- "node_modules/react-router": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz",
- "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==",
- "license": "MIT",
- "dependencies": {
- "cookie": "^1.0.1",
- "set-cookie-parser": "^2.6.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/react-router-dom": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz",
- "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==",
- "license": "MIT",
- "dependencies": {
- "react-router": "7.18.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "react": ">=18",
- "react-dom": ">=18"
- }
- },
"node_modules/rolldown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
@@ -1354,12 +866,6 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
- "node_modules/set-cookie-parser": {
- "version": "2.7.2",
- "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
- "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
- "license": "MIT"
- },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1370,27 +876,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/tailwindcss": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
- "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tapable": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
- "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
diff --git a/package.json b/package.json
index b5de976..4c1699e 100644
--- a/package.json
+++ b/package.json
@@ -10,19 +10,14 @@
"typecheck": "tsc -b"
},
"dependencies": {
- "@phosphor-icons/react": "^2.1.10",
- "@tanstack/react-query": "^5.101.0",
"react": "^19.2.7",
- "react-dom": "^19.2.7",
- "react-router-dom": "^7.18.0"
+ "react-dom": "^19.2.7"
},
"devDependencies": {
- "@tailwindcss/vite": "^4.3.1",
"@types/node": "^26.0.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
- "tailwindcss": "^4.3.1",
"typescript": "^6.0.3",
"vite": "^8.0.16"
}
diff --git a/public/brand/asset-10.png b/public/brand/asset-10.png
deleted file mode 100644
index 8ed71a0..0000000
Binary files a/public/brand/asset-10.png and /dev/null differ
diff --git a/public/brand/asset-12.png b/public/brand/asset-12.png
deleted file mode 100644
index abb97e5..0000000
Binary files a/public/brand/asset-12.png and /dev/null differ
diff --git a/public/brand/asset-14.png b/public/brand/asset-14.png
deleted file mode 100644
index e3e6e55..0000000
Binary files a/public/brand/asset-14.png and /dev/null differ
diff --git a/public/brand/asset-2.png b/public/brand/asset-2.png
deleted file mode 100644
index d9138c1..0000000
Binary files a/public/brand/asset-2.png and /dev/null differ
diff --git a/public/brand/asset-3.png b/public/brand/asset-3.png
deleted file mode 100644
index aa1b762..0000000
Binary files a/public/brand/asset-3.png and /dev/null differ
diff --git a/public/brand/asset-4.png b/public/brand/asset-4.png
deleted file mode 100644
index 60ecac7..0000000
Binary files a/public/brand/asset-4.png and /dev/null differ
diff --git a/public/brand/asset-5.png b/public/brand/asset-5.png
deleted file mode 100644
index 2d316b0..0000000
Binary files a/public/brand/asset-5.png and /dev/null differ
diff --git a/public/brand/asset-8.png b/public/brand/asset-8.png
deleted file mode 100644
index 408004c..0000000
Binary files a/public/brand/asset-8.png and /dev/null differ
diff --git a/public/brand/asset-9.png b/public/brand/asset-9.png
deleted file mode 100644
index 66e5dd7..0000000
Binary files a/public/brand/asset-9.png and /dev/null differ
diff --git a/public/brand/databreeze-mark-dark.png b/public/brand/databreeze-mark-dark.png
deleted file mode 100644
index e3e6e55..0000000
Binary files a/public/brand/databreeze-mark-dark.png and /dev/null differ
diff --git a/public/brand/databreeze-wordmark-black.png b/public/brand/databreeze-wordmark-black.png
deleted file mode 100644
index aa1b762..0000000
Binary files a/public/brand/databreeze-wordmark-black.png and /dev/null differ
diff --git a/public/brand/databreeze-wordmark-blue.png b/public/brand/databreeze-wordmark-blue.png
deleted file mode 100644
index 60ecac7..0000000
Binary files a/public/brand/databreeze-wordmark-blue.png and /dev/null differ
diff --git a/src/app/App.tsx b/src/app/App.tsx
deleted file mode 100644
index cdab070..0000000
--- a/src/app/App.tsx
+++ /dev/null
@@ -1,973 +0,0 @@
-import type { ChangeEvent, FormEvent, ReactNode } from 'react'
-import {
- Bell,
- CaretRight,
- CheckCircle,
- CreditCard,
- DownloadSimple,
- House,
- Lightbulb,
- List,
- MagnifyingGlass,
- Receipt,
- ShoppingBag,
- Storefront,
- Tag,
- UploadSimple,
- WarningCircle,
- X,
-} from '@phosphor-icons/react'
-import { useMemo, useRef, useState } from 'react'
-import { NavLink, Navigate, Route, Routes, useNavigate } from 'react-router-dom'
-import {
- dateRangeOptions,
- initialExpenses,
- initialMappingRows,
- initialMissingCosts,
- initialUploads,
- insights,
- kpis,
- mappingTargets,
- profitSeries,
- sourceOptions,
- storeOptions,
- stores,
- topSkus,
- type CostItem,
- type ExpenseItem,
- type MappingRow,
- type Tone,
- type UploadRecord,
- type UploadStatus,
-} from '../data/sampleData'
-import { Button, EmptyState, MetricCard, Panel, SelectField, StatusBadge, StepIndicator } from '../components/ui'
-import { LandingPage } from './LandingPage'
-
-const navItems = [
- { path: '/dashboard', label: 'Tổng quan', icon: House },
- { path: '/uploads', label: 'Tệp tải lên', icon: UploadSimple },
- { path: '/stores', label: 'Shop', icon: Storefront },
- { path: '/costs', label: 'Giá vốn', icon: Tag },
- { path: '/expenses', label: 'Chi phí', icon: Receipt },
- { path: '/insights', label: 'Gợi ý', icon: Lightbulb },
- { path: '/usage', label: 'Gói & sử dụng', icon: CreditCard },
-]
-
-export function App() {
- return (
-
- } />
- } />
-
- )
-}
-
-function ProductApp() {
- const navigate = useNavigate()
- const [mobileNavOpen, setMobileNavOpen] = useState(false)
- const [selectedStore, setSelectedStore] = useState(storeOptions[0])
- const [dateRange, setDateRange] = useState(dateRangeOptions[1])
- const [source, setSource] = useState(sourceOptions[0])
- const [query, setQuery] = useState('')
- const [notice, setNotice] = useState('Dữ liệu mẫu đã sẵn sàng để thao tác.')
- const [uploads, setUploads] = useState(initialUploads)
- const [mappingRows, setMappingRows] = useState(initialMappingRows)
- const [costItems, setCostItems] = useState(initialMissingCosts)
- const [expenses, setExpenses] = useState(initialExpenses)
- const [validationComplete, setValidationComplete] = useState(false)
-
- const updateLatestUploadStatus = (status: UploadStatus) => {
- setUploads((current) => current.map((upload, index) => (index === 0 ? { ...upload, status, time: 'Vừa xong' } : upload)))
- }
-
- return (
-
-
setMobileNavOpen(false)} />
-
- setMobileNavOpen(true)}
- onUpload={() => navigate('/uploads')}
- />
-
-
- } />
- navigate('/costs')}
- />
- }
- />
- setUploads((current) => [upload, ...current])}
- onNotice={setNotice}
- />
- }
- />
- {
- updateLatestUploadStatus('validating')
- setNotice('Đã lưu mapping. DataBreeze đang kiểm tra dữ liệu trước khi import.')
- navigate('/uploads/validation')
- }}
- onReset={() => setMappingRows(initialMappingRows)}
- />
- }
- />
- setNotice('Đã tạo file lỗi mẫu cho các dòng cần sửa.')}
- onImport={() => {
- setValidationComplete(true)
- updateLatestUploadStatus('completed')
- setNotice('Dashboard đã được cập nhật từ file Shopee mới nhất.')
- }}
- />
- }
- />
- } />
-
- setCostItems((current) => current.map((item) => (item.sku === sku ? { ...item, unitCost: value } : item)))
- }
- onSave={(sku) => {
- const item = costItems.find((cost) => cost.sku === sku)
- if (!item?.unitCost.trim()) return
- setCostItems((current) => current.filter((cost) => cost.sku !== sku))
- setNotice(`Đã lưu giá vốn cho ${sku}.`)
- }}
- />
- }
- />
- {
- setExpenses((current) => [expense, ...current])
- setNotice('Đã thêm chi phí mới vào workspace.')
- }}
- />
- }
- />
- navigate('/costs')} />} />
- } />
- } />
-
-
-
-
- )
-}
-
-function Sidebar({ mobileOpen, onClose }: { mobileOpen: boolean; onClose: () => void }) {
- return (
- <>
-
- {mobileOpen ? : null}
- >
- )
-}
-
-function Topbar({
- query,
- onQueryChange,
- onMenu,
- onUpload,
-}: {
- query: string
- onQueryChange: (value: string) => void
- onMenu: () => void
- onUpload: () => void
-}) {
- return (
-
- )
-}
-
-function PageHeader({
- title,
- body,
- action,
-}: {
- title: string
- body: string
- action?: ReactNode
-}) {
- return (
-
- )
-}
-
-function DashboardPage({
- selectedStore,
- dateRange,
- source,
- query,
- notice,
- onStoreChange,
- onDateRangeChange,
- onSourceChange,
- onResolveCosts,
-}: {
- selectedStore: string
- dateRange: string
- source: string
- query: string
- notice: string
- onStoreChange: (value: string) => void
- onDateRangeChange: (value: string) => void
- onSourceChange: (value: string) => void
- onResolveCosts: () => void
-}) {
- const visibleSkus = useMemo(() => {
- const normalized = query.trim().toLowerCase()
- if (!normalized) return topSkus
- return topSkus.filter((sku) => `${sku.sku} ${sku.name} ${sku.status}`.toLowerCase().includes(normalized))
- }, [query])
-
- return (
-
-
Xử lý giá vốn}
- />
-
-
- {notice}
-
-
-
-
-
-
-
- {kpis.map((kpi) => (
-
- ))}
-
-
-
-
-
-
3 việc}>
-
-
-
-
-
-
-
- 86%
- Dữ liệu đủ tốt để xem dashboard. Thêm giá vốn cho 3 SKU để lợi nhuận chính xác hơn.
-
-
-
-
- )
-}
-
-function ActionList({ onResolveCosts }: { onResolveCosts: () => void }) {
- return (
-
- {insights.slice(0, 3).map((insight, index) => (
-
-
-
- {insight.title}
- {insight.summary}
-
-
-
- ))}
-
- )
-}
-
-function ProfitChart() {
- const max = Math.max(...profitSeries)
- const points = profitSeries
- .map((value, index) => {
- const x = (index / (profitSeries.length - 1)) * 100
- const y = 100 - (value / max) * 86
- return `${x},${y}`
- })
- .join(' ')
-
- return (
-
-
-
-
-
-
-
-
- 7 thg 5
- Hôm nay
-
-
- )
-}
-
-function SkuTable({ skus }: { skus: typeof topSkus }) {
- if (!skus.length) {
- return
- }
-
- return (
-
-
-
-
- SKU
- Sản phẩm
- Doanh thu
- Lợi nhuận
- Biên
- Trạng thái
-
-
-
- {skus.map((sku) => (
-
- {sku.sku}
- {sku.name}
- {sku.revenue}
- {sku.profit}
- {sku.margin}
-
-
- {sku.status}
-
-
-
- ))}
-
-
-
- )
-}
-
-function UploadsPage({
- uploads,
- selectedStore,
- source,
- query,
- onAddUpload,
- onNotice,
-}: {
- uploads: UploadRecord[]
- selectedStore: string
- source: string
- query: string
- onAddUpload: (upload: UploadRecord) => void
- onNotice: (value: string) => void
-}) {
- const navigate = useNavigate()
- const inputRef = useRef(null)
- const filteredUploads = uploads.filter((upload) => {
- const normalized = query.trim().toLowerCase()
- return !normalized || `${upload.file} ${upload.store} ${upload.source}`.toLowerCase().includes(normalized)
- })
-
- const handleFiles = (event: ChangeEvent) => {
- const file = event.target.files?.[0]
- if (!file) return
- const nextUpload: UploadRecord = {
- file: file.name,
- store: selectedStore === 'Tất cả shop' ? 'Maison Sài Gòn' : selectedStore,
- source: inferSource(file.name, source),
- status: 'mapping',
- rows: 'Đang đọc',
- time: 'Vừa xong',
- }
- onAddUpload(nextUpload)
- onNotice(`Đã nhận ${file.name}. Hãy kiểm tra mapping trước khi import.`)
- event.target.value = ''
- navigate('/uploads/mapping')
- }
-
- return (
-
-
inputRef.current?.click()}>Tải file mới}
- />
-
-
-
-
-
-
- inputRef.current?.click()}>
-
- Kéo thả hoặc chọn file
- Hỗ trợ .xlsx, .xls và .csv. Sau khi chọn file, DataBreeze sẽ đưa bạn sang bước map cột.
- Chọn file từ máy
-
-
-
- navigate('/uploads/mapping')}>Mở mapping mẫu}
- />
-
- )
-}
-
-function UploadTable({ uploads }: { uploads: UploadRecord[] }) {
- const navigate = useNavigate()
-
- if (!uploads.length) {
- return
- }
-
- return (
-
-
-
-
- File
- Shop
- Nguồn
- Trạng thái
- Dòng
- Thời gian
- Hành động
-
-
-
- {uploads.map((upload) => (
-
- {upload.file}
- {upload.store}
- {upload.source}
-
- {statusLabel(upload.status)}
-
- {upload.rows}
- {upload.time}
-
- navigate(upload.status === 'completed' ? '/dashboard' : '/uploads/mapping')}
- >
- {upload.status === 'completed' ? 'Xem dashboard' : 'Tiếp tục'}
-
-
-
- ))}
-
-
-
- )
-}
-
-function MappingPage({
- rows,
- onRowsChange,
- onContinue,
- onReset,
-}: {
- rows: MappingRow[]
- onRowsChange: (rows: MappingRow[]) => void
- onContinue: () => void
- onReset: () => void
-}) {
- const navigate = useNavigate()
- const [saveTemplate, setSaveTemplate] = useState(true)
- const requiredMissing = rows.filter((row) => row.required && row.target === 'Không map').length
- const mappedRequired = rows.filter((row) => row.required && row.target !== 'Không map').length
- const requiredTotal = rows.filter((row) => row.required).length
-
- const updateTarget = (source: string, target: string) => {
- onRowsChange(
- rows.map((row) =>
- row.source === source
- ? {
- ...row,
- target,
- confidence: target === 'Không map' ? 'Thiếu' : row.confidence === 'Thiếu' ? 'Trung bình' : row.confidence,
- }
- : row,
- ),
- )
- }
-
- return (
-
-
0} onClick={onContinue}>Tiếp tục kiểm tra dữ liệu}
- />
-
-
-
-
- {rows.map((row) => (
-
-
- {row.source}
- Ví dụ: {row.example || 'Không có giá trị mẫu'}
-
-
- {row.group}
- updateTarget(row.source, event.target.value)} aria-label={`Trường đích cho ${row.source}`}>
- {mappingTargets.map((target) => (
- {target}
- ))}
-
-
-
- {row.confidence}
-
- {row.required ?
Bắt buộc :
Tùy chọn }
-
- ))}
-
-
-
- setSaveTemplate(event.target.checked)} />
- Lưu mapping này cho file cùng loại sau này
-
-
- navigate('/uploads')}>Quay lại
- Map lại mẫu
- 0} onClick={onContinue}>Kiểm tra dữ liệu
-
-
-
-
-
- {[
- ['Thông tin đơn', 'Mã đơn, ngày tạo, trạng thái và shop.'],
- ['Doanh thu và phí', 'Tính doanh thu gộp, phí sàn và khuyến mãi.'],
- ['Sản phẩm và SKU', 'Gắn lợi nhuận về đúng SKU và biến thể.'],
- ['Giá vốn', 'Có thể bổ sung sau nếu file không có.'],
- ].map(([group, text]) => (
-
- {group}
- {text}
-
- ))}
-
- {requiredMissing > 0 ? (
-
-
- Còn {requiredMissing} cột bắt buộc chưa được map.
-
- ) : (
-
-
- Các cột bắt buộc đã sẵn sàng để kiểm tra.
-
- )}
-
-
-
- )
-}
-
-function ValidationPage({
- complete,
- onImport,
- onDownloadErrors,
-}: {
- complete: boolean
- onImport: () => void
- onDownloadErrors: () => void
-}) {
- return (
-
-
{complete ? 'Đã cập nhật dashboard' : 'Đồng ý và cập nhật dashboard'}}
- />
-
-
-
-
-
-
-
- {complete ? (
-
-
- Dashboard đã cập nhật. Các dòng lỗi vẫn được giữ lại để bạn tải xuống và sửa sau.
-
- ) : null}
-
-
- {['369 dòng bán hàng thiếu giá vốn', '64 dòng thiếu SKU', '18 đơn có ngày không hợp lệ'].map((warning) => (
-
-
- {warning}
- Xem dòng
-
- ))}
-
-
- Tải file lỗi
- {complete ? 'Đã import' : 'Import dòng hợp lệ'}
-
-
-
- )
-}
-
-function StoresPage() {
- return (
-
-
Thêm shop} />
-
- {stores.map((store) => (
-
-
-
-
-
-
- {store.name}
- {store.platform}
-
-
{store.status}
-
-
-
-
Lần import gần nhất
- {store.lastImport}
-
-
-
Doanh thu
- {store.revenue}
-
-
-
- ))}
-
-
- )
-}
-
-function CostsPage({
- items,
- onCostChange,
- onSave,
-}: {
- items: CostItem[]
- onCostChange: (sku: string, value: string) => void
- onSave: (sku: string) => void
-}) {
- return (
-
-
Nhập giá vốn hàng loạt}
- />
-
- {items.length ? (
-
- ) : (
-
- )}
-
-
- )
-}
-
-function ExpensesPage({ expenses, onAddExpense }: { expenses: ExpenseItem[]; onAddExpense: (expense: ExpenseItem) => void }) {
- const [draft, setDraft] = useState({ category: 'Quảng cáo', description: '', store: 'Tất cả shop', amount: '' })
-
- const submitExpense = (event: FormEvent) => {
- event.preventDefault()
- if (!draft.description.trim() || !draft.amount.trim()) return
- onAddExpense({ ...draft, date: 'Hôm nay' })
- setDraft({ category: 'Quảng cáo', description: '', store: 'Tất cả shop', amount: '' })
- }
-
- return (
-
-
Thêm chi phí}
- />
-
-
-
-
-
-
-
-
-
- Nhóm
- Mô tả
- Shop
- Số tiền
- Ngày
-
-
-
- {expenses.map((expense, index) => (
-
- {expense.category}
- {expense.description}
- {expense.store}
- {expense.amount}
- {expense.date}
-
- ))}
-
-
-
-
-
-
- )
-}
-
-function InsightsPage({ onResolveCosts }: { onResolveCosts: () => void }) {
- return (
-
-
Làm mới gợi ý}
- />
-
- {insights.map((insight) => (
-
-
-
- {insight.severity}
-
-
- {insight.title}
- {insight.summary}
-
-
- {insight.action}
-
-
-
- ))}
-
-
- )
-}
-
-function UsagePage() {
- return (
-
-
Nâng cấp} />
-
- {[
- ['Tệp tải lên', '42 / 80', '52%'],
- ['Dòng đã xử lý', '318k / 500k', '64%'],
- ['Shop', '3 / 5', '60%'],
- ['Lượt tạo gợi ý', '18 / 40', '45%'],
- ].map(([label, value, percent]) => (
-
-
-
{label}
-
{value}
-
-
-
-
Đã dùng {percent}
-
-
- ))}
-
-
- )
-}
-
-function statusTone(status: UploadStatus): Tone {
- if (status === 'completed') return 'good'
- if (status === 'warning' || status === 'mapping' || status === 'validating') return 'warn'
- if (status === 'failed') return 'danger'
- return 'info'
-}
-
-function statusLabel(status: UploadStatus) {
- const labels: Record = {
- mapping: 'Đang map cột',
- validating: 'Đang kiểm tra',
- running: 'Đang xử lý',
- completed: 'Hoàn tất',
- warning: 'Cần xem lại',
- failed: 'Thất bại',
- }
- return labels[status]
-}
-
-function inferSource(fileName: string, selectedSource: string) {
- const lower = fileName.toLowerCase()
- if (selectedSource !== 'Tất cả nguồn') return selectedSource
- if (lower.includes('tiktok')) return 'TikTok Shop'
- if (lower.includes('ads') || lower.includes('google')) return 'Google Ads'
- if (lower.includes('expense') || lower.includes('chi_phi')) return 'Chi phí'
- return 'Shopee'
-}
diff --git a/src/app/LandingPage.tsx b/src/app/LandingPage.tsx
deleted file mode 100644
index 3f38312..0000000
--- a/src/app/LandingPage.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import { CaretRight, CheckCircle, UploadSimple, WarningCircle } from '@phosphor-icons/react'
-import { Link } from 'react-router-dom'
-
-const incomingFiles = [
- ['Shopee', 'orders_thang_06.xlsx', '4.218 dòng'],
- ['TikTok Shop', 'tiktok_doanh_thu.csv', '1.906 dòng'],
- ['Ads', 'chi_phi_quang_cao.xlsx', '12 chiến dịch'],
-]
-
-const processSteps = ['Map cột', 'Kiểm tra lỗi', 'Bổ sung giá vốn']
-
-const workflowSteps = [
- {
- title: 'Tải file thật',
- body: 'Nhận file Shopee, TikTok Shop, quảng cáo và chi phí mà không bắt seller sửa mẫu trước.',
- icon: UploadSimple,
- },
- {
- title: 'Thấy dữ liệu đang thiếu gì',
- body: 'Cột bắt buộc, giá trị mẫu, lỗi ngày tháng và SKU thiếu cost đều được đưa ra trước khi import.',
- icon: WarningCircle,
- },
- {
- title: 'Chốt dashboard đủ tin',
- body: 'Doanh thu, phí, giá vốn và lợi nhuận được cập nhật khi dữ liệu đã qua mapping và validation.',
- icon: CheckCircle,
- },
-]
-
-const trustLanes = [
- ['3.842', 'dòng hợp lệ', 'Sẵn sàng cập nhật dashboard'],
- ['312', 'cảnh báo', 'Cho phép import nhưng vẫn cần xem'],
- ['64', 'dòng lỗi', 'Giữ lại để tải xuống và sửa'],
- ['369', 'thiếu giá vốn', 'Tạo danh sách hành động theo SKU'],
-]
-
-export function LandingPage() {
- return (
-
-
-
-
-
-
-
-
-
-
-
Shopee CSV
-
Thiếu giá vốn
-
Lợi nhuận sẵn sàng
-
-
-
-
-
Lợi nhuận rõ ràng từ những file bán hàng lộn xộn.
-
- DataBreeze giúp seller Việt đưa file marketplace vào một quy trình có kiểm tra, có cảnh báo và có dashboard lợi nhuận đủ tin để ra quyết định.
-
-
-
-
-
-
- {incomingFiles.map(([source, name, rows]) => (
-
- {source}
- {name}
- {rows}
-
- ))}
-
-
-
-
-
-
- DataBreeze import
- Đang làm sạch dữ liệu
-
-
-
-
-
-
- {processSteps.map((step) => (
-
-
- {step}
-
- ))}
-
-
-
-
-
- Lợi nhuận ròng
- 37,8M
-
-
-
-
-
-
-
-
-
-
-
- 369 SKU cần bổ sung giá vốn
-
-
-
-
-
-
- Không chỉ gom file vào một chỗ. DataBreeze cho seller biết số nào đáng tin, số nào cần sửa và việc nào nên làm tiếp theo.
-
- 86%
- độ sẵn sàng dữ liệu
-
-
- 4 nguồn
- bán hàng, ads, chi phí, giá vốn
-
-
-
-
-
-
Quy trình vận hành
-
Từ file thô đến quyết định có căn cứ.
-
Mỗi bước giữ lại dấu vết dữ liệu: file nào đã vào, lỗi nào bị chặn và dashboard nào đã được cập nhật.
-
-
- {workflowSteps.map((step) => {
- const Icon = step.icon
- return (
-
-
-
-
{step.title}
-
{step.body}
-
-
- )
- })}
-
-
-
-
-
-
Dữ liệu trước, quyết định sau
-
Không giấu lỗi import dưới một con số đẹp.
-
- Seller không cần biết ETL hay SQL. Họ cần biết file nào đã vào, dòng nào bị chặn, SKU nào thiếu cost và dashboard nào đủ tin để hành động.
-
-
-
- {trustLanes.map(([value, label, body]) => (
-
-
{value}
-
{label}
-
{body}
-
- ))}
-
-
-
-
-
-
-
Mở thử workspace và xem luồng dữ liệu chạy thật.
-
-
- Mở sản phẩm
-
-
-
-
-
- )
-}
diff --git a/src/components/ui.tsx b/src/components/ui.tsx
deleted file mode 100644
index 01d71e7..0000000
--- a/src/components/ui.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import type { ReactNode } from 'react'
-import type { Tone } from '../data/sampleData'
-
-const toneClass: Record = {
- neutral: 'tone-neutral',
- good: 'tone-good',
- warn: 'tone-warn',
- danger: 'tone-danger',
- info: 'tone-info',
-}
-
-export function Button({
- children,
- variant = 'primary',
- type = 'button',
- disabled,
- loading,
- onClick,
- className = '',
- form,
-}: {
- children: ReactNode
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
- type?: 'button' | 'submit'
- disabled?: boolean
- loading?: boolean
- onClick?: () => void
- className?: string
- form?: string
-}) {
- return (
-
- {loading ? : null}
- {children}
-
- )
-}
-
-export function StatusBadge({ children, tone = 'neutral' }: { children: ReactNode; tone?: Tone }) {
- return {children}
-}
-
-export function Panel({
- children,
- className = '',
- title,
- description,
- action,
-}: {
- children: ReactNode
- className?: string
- title?: string
- description?: string
- action?: ReactNode
-}) {
- return (
-
- {(title || description || action) && (
-
-
- {title ?
{title} : null}
- {description ?
{description}
: null}
-
- {action}
-
- )}
- {children}
-
- )
-}
-
-export function MetricCard({
- label,
- value,
- delta,
- tone,
-}: {
- label: string
- value: string
- delta: string
- tone: Tone
-}) {
- return (
-
- {label}
- {value}
- {delta}
-
- )
-}
-
-export function EmptyState({
- title,
- body,
- action,
-}: {
- title: string
- body: string
- action?: ReactNode
-}) {
- return (
-
-
-
-
-
- {action ?
{action}
: null}
-
- )
-}
-
-export function StepIndicator({ active }: { active: number }) {
- const steps = ['Tải file', 'Map cột', 'Kiểm tra', 'Hoàn tất']
-
- return (
-
- {steps.map((step, index) => {
- const state = index < active ? 'done' : index === active ? 'active' : 'idle'
- return (
-
- {index + 1}
- {step}
-
- )
- })}
-
- )
-}
-
-export function Field({
- label,
- value,
- helper,
-}: {
- label: string
- value: string
- helper?: string
-}) {
- return (
-
- {label}
-
- {helper ? {helper} : null}
-
- )
-}
-
-export function SelectField({
- label,
- value,
- options,
- onChange,
- helper,
-}: {
- label: string
- value: string
- options: string[]
- onChange: (value: string) => void
- helper?: string
-}) {
- return (
-
- {label}
- onChange(event.target.value)} aria-describedby={helper ? `${label}-helper` : undefined}>
- {options.map((option) => (
- {option}
- ))}
-
- {helper ? {helper} : null}
-
- )
-}
diff --git a/src/data/sampleData.ts b/src/data/sampleData.ts
deleted file mode 100644
index 2714e2c..0000000
--- a/src/data/sampleData.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-export type Tone = 'neutral' | 'good' | 'warn' | 'danger' | 'info'
-
-export type UploadStatus = 'mapping' | 'validating' | 'running' | 'completed' | 'warning' | 'failed'
-
-export type UploadRecord = {
- file: string
- store: string
- source: string
- status: UploadStatus
- rows: string
- time: string
-}
-
-export type MappingRow = {
- source: string
- example: string
- target: string
- group: string
- confidence: 'Cao' | 'Trung bình' | 'Thiếu'
- required: boolean
-}
-
-export type CostItem = {
- sku: string
- product: string
- orders: number
- affected: string
- unitCost: string
-}
-
-export type ExpenseItem = {
- category: string
- description: string
- store: string
- amount: string
- date: string
-}
-
-export const storeOptions = ['Tất cả shop', 'Maison Sài Gòn', 'Breeze Beauty', 'Home Kit VN']
-export const dateRangeOptions = ['7 ngày qua', '30 ngày qua', 'Tháng này', 'Quý này']
-export const sourceOptions = ['Tất cả nguồn', 'Shopee', 'TikTok Shop', 'Google Ads', 'Chi phí']
-
-export const kpis: Array<{ label: string; value: string; delta: string; tone: Tone }> = [
- { label: 'Doanh thu', value: '186,4M', delta: '+12,8%', tone: 'good' },
- { label: 'Lợi nhuận gộp', value: '54,2M', delta: '+8,1%', tone: 'good' },
- { label: 'Lợi nhuận ròng', value: '37,8M', delta: '+4,4%', tone: 'good' },
- { label: 'Biên lợi nhuận', value: '20,3%', delta: '-1,6%', tone: 'warn' },
- { label: 'Đơn hàng', value: '2.418', delta: '+194', tone: 'neutral' },
-]
-
-export const profitSeries = [18, 24, 21, 32, 28, 37, 42, 39, 48, 44, 52, 58]
-
-export const topSkus = [
- { sku: 'DB-TEE-001', name: 'Áo thun basic', revenue: '31,2M', profit: '9,8M', margin: '31,4%', status: 'Khỏe' },
- { sku: 'SKN-BOX-09', name: 'Combo chăm sóc da', revenue: '26,7M', profit: '7,4M', margin: '27,7%', status: 'Thiếu giá vốn' },
- { sku: 'BAG-MINI-18', name: 'Túi đeo chéo mini', revenue: '19,1M', profit: '3,2M', margin: '16,8%', status: 'Biên thấp' },
- { sku: 'KIT-HOME-22', name: 'Bộ dụng cụ nhà bếp', revenue: '15,6M', profit: '5,1M', margin: '32,7%', status: 'Khỏe' },
-]
-
-export const initialUploads: UploadRecord[] = [
- {
- file: 'shopee_orders_may.xlsx',
- store: 'Maison Sài Gòn',
- source: 'Shopee',
- status: 'mapping',
- rows: '4.218',
- time: '12 phút trước',
- },
- {
- file: 'tiktok_shop_week_21.csv',
- store: 'Breeze Beauty',
- source: 'TikTok Shop',
- status: 'warning',
- rows: '1.840',
- time: 'Hôm qua',
- },
- {
- file: 'ads_google_q2.csv',
- store: 'Tất cả shop',
- source: 'Google Ads',
- status: 'completed',
- rows: '940',
- time: '29 thg 5',
- },
- {
- file: 'expense_may.xlsx',
- store: 'Maison Sài Gòn',
- source: 'Chi phí',
- status: 'completed',
- rows: '64',
- time: '28 thg 5',
- },
-]
-
-export const mappingTargets = [
- 'Không map',
- 'Mã đơn hàng',
- 'SKU',
- 'Doanh thu gộp',
- 'Phí sàn',
- 'Ngày tạo đơn',
- 'Giá vốn',
- 'Tên sản phẩm',
- 'Số lượng',
-]
-
-export const initialMappingRows: MappingRow[] = [
- {
- source: 'Mã đơn hàng',
- example: '250522QF14MA9',
- target: 'Mã đơn hàng',
- group: 'Thông tin đơn',
- confidence: 'Cao',
- required: true,
- },
- {
- source: 'SKU phân loại hàng',
- example: 'DB-TEE-001-BLACK-M',
- target: 'SKU',
- group: 'Sản phẩm và SKU',
- confidence: 'Cao',
- required: true,
- },
- {
- source: 'Tổng tiền hàng',
- example: '349000',
- target: 'Doanh thu gộp',
- group: 'Doanh thu và phí',
- confidence: 'Trung bình',
- required: true,
- },
- {
- source: 'Phí dịch vụ',
- example: '18400',
- target: 'Phí sàn',
- group: 'Doanh thu và phí',
- confidence: 'Trung bình',
- required: false,
- },
- {
- source: 'Ngày tạo đơn',
- example: '2026-05-29 14:32',
- target: 'Ngày tạo đơn',
- group: 'Thông tin đơn',
- confidence: 'Cao',
- required: true,
- },
- {
- source: 'Giá vốn',
- example: '',
- target: 'Không map',
- group: 'Giá vốn',
- confidence: 'Thiếu',
- required: false,
- },
-]
-
-export const stores = [
- { name: 'Maison Sài Gòn', platform: 'Shopee', status: 'Đang hoạt động', lastImport: '12 phút trước', revenue: '94,1M' },
- { name: 'Breeze Beauty', platform: 'TikTok Shop', status: 'Cần giá vốn', lastImport: 'Hôm qua', revenue: '61,8M' },
- { name: 'Home Kit VN', platform: 'Shopee', status: 'Đang hoạt động', lastImport: '29 thg 5', revenue: '30,5M' },
-]
-
-export const initialMissingCosts: CostItem[] = [
- { sku: 'SKN-BOX-09', product: 'Combo chăm sóc da', orders: 214, affected: '26,7M', unitCost: '' },
- { sku: 'BAG-MINI-18', product: 'Túi đeo chéo mini', orders: 91, affected: '19,1M', unitCost: '' },
- { sku: 'ACC-CABLE-04', product: 'Cáp sạc nhanh', orders: 64, affected: '8,4M', unitCost: '' },
-]
-
-export const initialExpenses: ExpenseItem[] = [
- { category: 'Quảng cáo', description: 'Google Ads tháng 5', store: 'Tất cả shop', amount: '8,2M', date: '31 thg 5' },
- { category: 'Vận hành', description: 'Kho và đóng gói', store: 'Maison Sài Gòn', amount: '4,6M', date: '29 thg 5' },
- { category: 'Công cụ', description: 'Công cụ vận hành', store: 'Tất cả shop', amount: '1,2M', date: '25 thg 5' },
-]
-
-export const insights = [
- {
- severity: 'Cao',
- title: 'SKU có doanh thu nhưng đang lỗ',
- summary: 'BAG-MINI-18 có biên lợi nhuận 16,8% sau phí sàn và khuyến mãi.',
- action: 'Kiểm tra giá vốn',
- },
- {
- severity: 'Vừa',
- title: 'Một số dòng bán hàng thiếu giá vốn',
- summary: '369 đơn hàng bị ảnh hưởng bởi SKU chưa có giá vốn.',
- action: 'Thêm giá vốn',
- },
- {
- severity: 'Thấp',
- title: 'Doanh thu Shopee tăng trong 7 ngày',
- summary: 'Maison Sài Gòn tăng 12,8% so với kỳ trước.',
- action: 'Xem chi tiết',
- },
-]
diff --git a/src/main.tsx b/src/main.tsx
index 4a5a680..283c999 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,26 +1,8 @@
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
-import { BrowserRouter } from 'react-router-dom'
-import { App } from './app/App'
-import './styles/index.css'
-import './styles/landing.css'
-
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 60_000,
- refetchOnWindowFocus: false,
- },
- },
-})
createRoot(document.getElementById('root')!).render(
-
-
-
-
-
+
,
)
diff --git a/src/styles/index.css b/src/styles/index.css
deleted file mode 100644
index dac6c86..0000000
--- a/src/styles/index.css
+++ /dev/null
@@ -1,1892 +0,0 @@
-@import "tailwindcss";
-
-:root {
- --font-ui: "Be Vietnam Pro", "Noto Sans", "Segoe UI", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
- --color-bg: oklch(0.992 0.003 255);
- --color-surface: oklch(1.000 0.000 0);
- --color-surface-muted: oklch(0.972 0.006 255);
- --color-surface-strong: oklch(0.936 0.010 255);
- --color-ink: oklch(0.188 0.020 260);
- --color-muted: oklch(0.438 0.026 260);
- --color-subtle: oklch(0.565 0.024 260);
- --color-border: oklch(0.885 0.012 255);
- --color-border-strong: oklch(0.805 0.020 255);
- --color-primary: oklch(0.585 0.215 262);
- --color-primary-hover: oklch(0.520 0.222 262);
- --color-primary-soft: oklch(0.946 0.045 262);
- --color-primary-soft-strong: oklch(0.905 0.072 262);
- --color-accent: oklch(0.590 0.120 178);
- --color-accent-soft: oklch(0.944 0.038 178);
- --color-success: oklch(0.520 0.126 145);
- --color-success-soft: oklch(0.944 0.042 145);
- --color-warning: oklch(0.660 0.132 75);
- --color-warning-soft: oklch(0.950 0.052 75);
- --color-danger: oklch(0.550 0.154 28);
- --color-danger-soft: oklch(0.952 0.044 28);
- --color-info: oklch(0.555 0.122 245);
- --color-info-soft: oklch(0.948 0.040 245);
- --sidebar: oklch(0.132 0.038 260);
- --sidebar-hover: oklch(0.190 0.046 260);
- --sidebar-active: oklch(0.245 0.068 260);
- --sidebar-ink: oklch(0.980 0.004 255);
- --sidebar-muted: oklch(0.748 0.034 260);
- --focus: oklch(0.620 0.180 262);
- color: var(--color-ink);
- background: var(--color-bg);
- font-family: var(--font-ui);
- font-synthesis: none;
- font-variant-numeric: tabular-nums;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
-}
-
-* {
- box-sizing: border-box;
-}
-
-html {
- background: var(--color-bg);
-}
-
-body {
- margin: 0;
- min-width: 320px;
- min-height: 100dvh;
- background: var(--color-bg);
-}
-
-button,
-input,
-select {
- font: inherit;
-}
-
-button {
- cursor: pointer;
-}
-
-button:disabled {
- cursor: not-allowed;
-}
-
-:focus-visible {
- outline: 2px solid var(--focus);
- outline-offset: 2px;
-}
-
-.visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-
-.app-shell {
- min-height: 100dvh;
- display: grid;
- grid-template-columns: 244px minmax(0, 1fr);
- background: var(--color-bg);
-}
-
-.sidebar {
- position: sticky;
- top: 0;
- height: 100dvh;
- display: flex;
- flex-direction: column;
- gap: 24px;
- padding: 18px 12px;
- background: var(--sidebar);
- color: var(--sidebar-ink);
-}
-
-.brand-lockup {
- display: grid;
- grid-template-columns: 42px minmax(0, 1fr) auto;
- align-items: center;
- gap: 11px;
- min-height: 48px;
- padding: 0 4px;
-}
-
-.brand-lockup img {
- width: 38px;
- height: 38px;
- border-radius: 8px;
- object-fit: cover;
-}
-
-.brand-lockup strong {
- display: block;
- font-size: 15px;
- font-weight: 800;
- line-height: 1.2;
-}
-
-.brand-lockup span {
- display: block;
- margin-top: 3px;
- color: var(--sidebar-muted);
- font-size: 12px;
- line-height: 1.35;
-}
-
-.sidebar-close,
-.topbar-menu,
-.icon-button.sidebar-close,
-.icon-button.topbar-menu {
- display: none;
-}
-
-.sidebar-nav {
- display: grid;
- gap: 4px;
-}
-
-.nav-link {
- min-height: 42px;
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 0 11px;
- border-radius: 8px;
- color: var(--sidebar-muted);
- text-decoration: none;
- font-size: 14px;
- font-weight: 650;
- transition: background-color 170ms ease, color 170ms ease, transform 170ms ease;
-}
-
-.nav-link:hover {
- background: var(--sidebar-hover);
- color: var(--sidebar-ink);
-}
-
-.nav-link:active {
- transform: translateY(1px);
-}
-
-.nav-link.active {
- background: var(--sidebar-active);
- color: var(--sidebar-ink);
-}
-
-.sidebar-footer {
- margin-top: auto;
- padding: 13px;
- border: 1px solid oklch(0.255 0.042 260);
- border-radius: 8px;
- display: grid;
- gap: 8px;
-}
-
-.sidebar-footer span,
-.sidebar-footer small {
- color: var(--sidebar-muted);
- font-size: 12px;
- line-height: 1.4;
-}
-
-.sidebar-footer strong {
- font-size: 14px;
-}
-
-.usage-bar {
- width: 100%;
- height: 8px;
- border-radius: 999px;
- background: var(--color-surface-strong);
- overflow: hidden;
-}
-
-.sidebar-footer .usage-bar {
- background: oklch(0.260 0.040 260);
-}
-
-.usage-bar span {
- display: block;
- height: 100%;
- border-radius: inherit;
- background: var(--color-primary);
- transition: width 180ms ease;
-}
-
-.app-main {
- min-width: 0;
- display: grid;
- grid-template-rows: 64px minmax(0, 1fr);
-}
-
-.topbar {
- position: sticky;
- top: 0;
- z-index: 20;
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 0 24px;
- border-bottom: 1px solid var(--color-border);
- background: color-mix(in oklch, var(--color-surface) 94%, transparent);
- backdrop-filter: blur(10px);
-}
-
-.workspace-switcher {
- min-width: 210px;
- display: grid;
- gap: 2px;
-}
-
-.workspace-switcher span {
- color: var(--color-subtle);
- font-size: 12px;
- font-weight: 650;
-}
-
-.workspace-switcher strong {
- display: block;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: 14px;
- font-weight: 750;
- line-height: 1.25;
- white-space: nowrap;
-}
-
-.command-box {
- width: min(460px, 38vw);
- min-height: 38px;
- display: flex;
- align-items: center;
- gap: 9px;
- padding: 0 12px;
- border: 1px solid var(--color-border);
- border-radius: 8px;
- background: var(--color-surface-muted);
- color: var(--color-subtle);
-}
-
-.command-box input {
- width: 100%;
- min-width: 0;
- border: 0;
- outline: 0;
- background: transparent;
- color: var(--color-ink);
- font-size: 13px;
-}
-
-.command-box input::placeholder {
- color: var(--color-muted);
- opacity: 1;
-}
-
-.topbar-actions {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.icon-button {
- width: 38px;
- height: 38px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border: 1px solid var(--color-border);
- border-radius: 8px;
- background: var(--color-surface);
- color: var(--color-ink);
- transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
-}
-
-.icon-button:hover {
- border-color: var(--color-border-strong);
- background: var(--color-surface-muted);
-}
-
-.icon-button:active {
- transform: translateY(1px);
-}
-
-.user-chip {
- width: 38px;
- height: 38px;
- border-radius: 8px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- background: var(--color-surface-strong);
- color: var(--color-ink);
- font-size: 13px;
- font-weight: 800;
-}
-
-.button {
- min-height: 38px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 0 14px;
- border-radius: 8px;
- border: 1px solid transparent;
- font-size: 13px;
- font-weight: 750;
- line-height: 1;
- white-space: nowrap;
- transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms ease;
-}
-
-.button:active {
- transform: translateY(1px);
-}
-
-.button:disabled {
- opacity: 0.58;
-}
-
-.button-primary {
- background: var(--color-primary);
- color: oklch(0.990 0.000 0);
-}
-
-.button-primary:hover:not(:disabled) {
- background: var(--color-primary-hover);
-}
-
-.button-secondary {
- border-color: var(--color-border);
- background: var(--color-surface);
- color: var(--color-ink);
-}
-
-.button-secondary:hover:not(:disabled),
-.button-ghost:hover:not(:disabled) {
- background: var(--color-surface-muted);
-}
-
-.button-ghost {
- background: transparent;
- color: var(--color-ink);
-}
-
-.button-danger {
- background: var(--color-danger);
- color: oklch(0.990 0.000 0);
-}
-
-.button-spinner {
- width: 14px;
- height: 14px;
- border: 2px solid currentColor;
- border-right-color: transparent;
- border-radius: 999px;
- animation: spin 720ms linear infinite;
-}
-
-.page-frame {
- min-width: 0;
- padding: 24px;
- background:
- linear-gradient(180deg, var(--color-surface-muted) 0, var(--color-bg) 250px),
- var(--color-bg);
-}
-
-.page-stack {
- display: grid;
- gap: 20px;
- max-width: 1460px;
- margin: 0 auto;
-}
-
-.page-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 18px;
-}
-
-.page-header h1 {
- margin: 0;
- color: var(--color-ink);
- font-size: 29px;
- font-weight: 760;
- line-height: 1.18;
- letter-spacing: 0;
- text-wrap: balance;
-}
-
-.page-header p {
- margin: 8px 0 0;
- max-width: 74ch;
- color: var(--color-muted);
- font-size: 14px;
- line-height: 1.62;
- text-wrap: pretty;
-}
-
-.notice-strip,
-.inline-warning,
-.inline-success {
- min-height: 42px;
- display: flex;
- align-items: center;
- gap: 10px;
- border: 1px solid var(--color-border);
- border-radius: 8px;
- padding: 10px 12px;
- background: var(--color-surface);
- color: var(--color-ink);
- font-size: 13px;
- line-height: 1.45;
-}
-
-.notice-strip svg,
-.inline-success svg {
- flex: 0 0 auto;
- color: var(--color-success);
-}
-
-.notice-strip.success {
- border-color: oklch(0.830 0.055 145);
- background: var(--color-success-soft);
-}
-
-.inline-warning {
- margin-top: 14px;
- border-color: oklch(0.865 0.070 75);
- background: var(--color-warning-soft);
-}
-
-.inline-warning svg {
- flex: 0 0 auto;
- color: var(--color-warning);
-}
-
-.inline-success {
- margin-top: 14px;
- border-color: oklch(0.830 0.055 145);
- background: var(--color-success-soft);
-}
-
-.filter-row {
- display: grid;
- grid-template-columns: repeat(3, minmax(190px, 1fr));
- gap: 12px;
-}
-
-.field,
-.expense-form label {
- display: grid;
- gap: 7px;
-}
-
-.field span,
-.expense-form span {
- color: var(--color-muted);
- font-size: 12px;
- font-weight: 750;
-}
-
-.field input,
-.field select,
-.mapping-row select,
-.expense-form input,
-.expense-form select,
-.table-input {
- width: 100%;
- min-height: 38px;
- border: 1px solid var(--color-border);
- border-radius: 8px;
- background: var(--color-surface);
- color: var(--color-ink);
- padding: 0 11px;
- font-size: 13px;
- transition: border-color 160ms ease, background-color 160ms ease;
-}
-
-.field input:hover,
-.field select:hover,
-.mapping-row select:hover,
-.expense-form input:hover,
-.expense-form select:hover,
-.table-input:hover {
- border-color: var(--color-border-strong);
-}
-
-.field small {
- color: var(--color-muted);
-}
-
-.metrics-grid {
- display: grid;
- grid-template-columns: repeat(5, minmax(0, 1fr));
- gap: 12px;
-}
-
-.metrics-grid.four {
- grid-template-columns: repeat(4, minmax(0, 1fr));
-}
-
-.metric-card,
-.panel {
- border: 1px solid var(--color-border);
- border-radius: 8px;
- background: var(--color-surface);
-}
-
-.metric-card {
- min-height: 124px;
- display: grid;
- align-content: space-between;
- gap: 12px;
- padding: 16px;
-}
-
-.metric-card > span {
- color: var(--color-muted);
- font-size: 12px;
- font-weight: 750;
-}
-
-.metric-card strong {
- display: block;
- font-size: 30px;
- font-weight: 780;
- line-height: 1;
- letter-spacing: 0;
-}
-
-.status-badge {
- width: fit-content;
- min-height: 24px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0 8px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 760;
- line-height: 1;
-}
-
-.tone-neutral {
- background: var(--color-surface-strong);
- color: var(--color-ink);
-}
-
-.tone-good {
- background: var(--color-success-soft);
- color: var(--color-success);
-}
-
-.tone-warn {
- background: var(--color-warning-soft);
- color: oklch(0.410 0.105 70);
-}
-
-.tone-danger {
- background: var(--color-danger-soft);
- color: var(--color-danger);
-}
-
-.tone-info {
- background: var(--color-info-soft);
- color: var(--color-info);
-}
-
-.panel {
- min-width: 0;
- padding: 16px;
-}
-
-.panel-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 14px;
-}
-
-.panel-header h2 {
- margin: 0;
- font-size: 17px;
- font-weight: 720;
- line-height: 1.3;
-}
-
-.panel-header p {
- margin: 4px 0 0;
- color: var(--color-muted);
- font-size: 12px;
- line-height: 1.45;
-}
-
-.dashboard-grid,
-.upload-layout,
-.mapping-layout {
- display: grid;
- grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr);
- gap: 12px;
-}
-
-.expenses-layout {
- display: grid;
- grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.4fr);
- gap: 12px;
-}
-
-.span-2 {
- grid-column: span 1;
-}
-
-.chart-wrap {
- display: grid;
- gap: 10px;
-}
-
-.chart-wrap svg {
- width: 100%;
- height: 260px;
-}
-
-.chart-grid {
- fill: none;
- stroke: var(--color-border);
- stroke-width: 0.6;
-}
-
-.chart-line {
- fill: none;
- stroke: var(--color-primary);
- stroke-linecap: round;
- stroke-linejoin: round;
- stroke-width: 2.6;
-}
-
-.chart-caption {
- display: flex;
- justify-content: space-between;
- color: var(--color-muted);
- font-size: 12px;
-}
-
-.table-wrap {
- width: 100%;
- overflow-x: auto;
-}
-
-table {
- width: 100%;
- border-collapse: collapse;
- min-width: 760px;
-}
-
-th,
-td {
- padding: 12px 10px;
- border-bottom: 1px solid var(--color-border);
- text-align: left;
- font-size: 13px;
- line-height: 1.42;
- white-space: nowrap;
-}
-
-th {
- color: var(--color-muted);
- font-weight: 780;
-}
-
-td {
- color: var(--color-ink);
-}
-
-tr:last-child td {
- border-bottom: 0;
-}
-
-.table-input {
- max-width: 150px;
-}
-
-.action-list,
-.field-groups,
-.insight-list {
- display: grid;
- gap: 10px;
-}
-
-.action-item,
-.field-groups > div,
-.warning-list > div,
-.store-row,
-.insight-row {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr) auto;
- gap: 10px;
- align-items: center;
-}
-
-.action-item {
- width: 100%;
- align-items: flex-start;
- padding: 11px;
- border: 1px solid transparent;
- border-radius: 8px;
- background: var(--color-surface-muted);
- color: var(--color-ink);
- text-align: left;
- transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease;
-}
-
-.action-item:hover {
- border-color: var(--color-border);
- background: var(--color-primary-soft);
-}
-
-.action-item:active {
- transform: translateY(1px);
-}
-
-.action-item svg {
- color: var(--color-warning);
-}
-
-.action-item strong,
-.field-groups strong,
-.insight-row strong {
- display: block;
- font-size: 13px;
- font-weight: 760;
-}
-
-.action-item span,
-.field-groups span,
-.insight-row span {
- display: block;
- margin-top: 4px;
- color: var(--color-muted);
- font-size: 12px;
- line-height: 1.5;
-}
-
-.quality-score {
- display: grid;
- gap: 10px;
-}
-
-.quality-score strong {
- font-size: 42px;
- font-weight: 800;
- line-height: 1;
- color: var(--color-accent);
-}
-
-.quality-score span {
- color: var(--color-muted);
- font-size: 13px;
- line-height: 1.62;
-}
-
-.drop-zone {
- width: 100%;
- min-height: 266px;
- display: grid;
- place-items: center;
- gap: 10px;
- text-align: center;
- padding: 24px;
- border: 1px dashed var(--color-border-strong);
- border-radius: 8px;
- background: var(--color-surface-muted);
- color: var(--color-ink);
- transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease;
-}
-
-.drop-zone:hover {
- border-color: var(--color-primary);
- background: var(--color-primary-soft);
-}
-
-.drop-zone:active {
- transform: translateY(1px);
-}
-
-.drop-zone svg {
- color: var(--color-primary);
-}
-
-.drop-zone span {
- max-width: 34ch;
- color: var(--color-muted);
- font-size: 13px;
- line-height: 1.55;
-}
-
-.drop-zone-action {
- min-height: 32px;
- display: inline-flex !important;
- align-items: center;
- justify-content: center;
- padding: 0 11px;
- border-radius: 8px;
- background: var(--color-surface);
- color: var(--color-primary) !important;
- font-weight: 750;
-}
-
-.empty-state {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 18px;
- border: 1px solid var(--color-border);
- border-radius: 8px;
- background: var(--color-surface);
-}
-
-.empty-state-mark {
- width: 48px;
- height: 48px;
- flex: 0 0 auto;
- border-radius: 8px;
- overflow: hidden;
-}
-
-.empty-state-mark img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.empty-state h2 {
- margin: 0;
- font-size: 16px;
- font-weight: 760;
-}
-
-.empty-state p {
- margin: 5px 0 0;
- color: var(--color-muted);
- font-size: 13px;
- line-height: 1.55;
-}
-
-.empty-state-action {
- margin-left: auto;
-}
-
-.step-indicator {
- display: flex;
- gap: 8px;
- margin: 0;
- padding: 0;
- list-style: none;
- scrollbar-width: none;
-}
-
-.step-indicator::-webkit-scrollbar {
- display: none;
-}
-
-.step {
- flex: 0 0 auto;
- min-height: 36px;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- padding: 0 12px;
- border: 1px solid var(--color-border);
- border-radius: 999px;
- background: var(--color-surface);
- color: var(--color-muted);
- font-size: 13px;
- font-weight: 760;
- white-space: nowrap;
-}
-
-.step span {
- width: 20px;
- height: 20px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border-radius: 999px;
- background: var(--color-surface-strong);
- font-size: 11px;
-}
-
-.step-active {
- border-color: var(--color-primary);
- color: var(--color-primary);
- background: var(--color-primary-soft);
-}
-
-.step-active span,
-.step-done span {
- background: var(--color-primary);
- color: oklch(0.990 0.000 0);
-}
-
-.mapping-table {
- display: grid;
-}
-
-.mapping-row {
- display: grid;
- grid-template-columns: minmax(180px, 1.2fr) minmax(190px, 1fr) auto auto;
- align-items: center;
- gap: 12px;
- padding: 13px 0;
- border-bottom: 1px solid var(--color-border);
-}
-
-.mapping-row:last-child {
- border-bottom: 0;
-}
-
-.mapping-row strong,
-.mapping-row span {
- display: block;
-}
-
-.mapping-row strong {
- font-size: 13px;
- font-weight: 760;
-}
-
-.mapping-row span {
- margin-top: 4px;
- color: var(--color-muted);
- font-size: 12px;
-}
-
-.required-mark,
-.optional-mark {
- color: var(--color-danger) !important;
- font-size: 12px;
- font-weight: 760;
-}
-
-.optional-mark {
- color: var(--color-muted) !important;
-}
-
-.mapping-footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- padding-top: 14px;
-}
-
-.checkbox-row {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--color-ink);
- font-size: 13px;
- font-weight: 650;
-}
-
-.checkbox-row input {
- width: 18px;
- height: 18px;
- accent-color: var(--color-primary);
-}
-
-.mapping-actions,
-.panel-actions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- justify-content: flex-end;
-}
-
-.panel-actions {
- margin-top: 14px;
-}
-
-.warning-list {
- display: grid;
- gap: 4px;
-}
-
-.warning-list > div {
- min-height: 48px;
- padding: 9px 0;
- border-bottom: 1px solid var(--color-border);
-}
-
-.warning-list > div:last-child {
- border-bottom: 0;
-}
-
-.warning-list svg {
- color: var(--color-warning);
-}
-
-.list-grid,
-.usage-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
- gap: 12px;
-}
-
-.store-row {
- grid-template-columns: auto minmax(0, 1fr) auto;
-}
-
-.store-icon {
- width: 40px;
- height: 40px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border-radius: 8px;
- background: var(--color-primary-soft);
- color: var(--color-primary);
-}
-
-.store-row strong,
-.store-row span {
- display: block;
-}
-
-.store-row span {
- margin-top: 2px;
- color: var(--color-muted);
- font-size: 12px;
-}
-
-.compact-facts {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px;
- margin: 16px 0 0;
-}
-
-.compact-facts div,
-.usage-card {
- display: grid;
- gap: 5px;
-}
-
-.compact-facts dt,
-.usage-card span,
-.usage-card small {
- color: var(--color-muted);
- font-size: 12px;
-}
-
-.compact-facts dd,
-.usage-card strong {
- margin: 0;
- font-size: 16px;
- font-weight: 760;
-}
-
-.expense-form {
- display: grid;
- gap: 12px;
-}
-
-.insight-row {
- grid-template-columns: auto minmax(0, 1fr) auto;
-}
-
-.mobile-scrim {
- display: none;
-}
-
-.landing-page {
- min-height: 100dvh;
- background: var(--color-bg);
- color: var(--color-ink);
-}
-
-.landing-nav {
- height: 72px;
- display: flex;
- align-items: center;
- gap: 24px;
- padding: 0 32px;
- border-bottom: 1px solid var(--color-border);
- background: color-mix(in oklch, var(--color-surface) 94%, transparent);
- backdrop-filter: blur(10px);
-}
-
-.landing-brand,
-.landing-nav-cta,
-.landing-button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- text-decoration: none;
-}
-
-.landing-brand {
- gap: 10px;
- color: var(--color-ink);
- font-size: 15px;
- font-weight: 800;
-}
-
-.landing-brand img {
- width: 34px;
- height: 34px;
- border-radius: 8px;
-}
-
-.landing-nav nav {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 22px;
-}
-
-.landing-nav nav a {
- color: var(--color-muted);
- font-size: 13px;
- font-weight: 650;
- text-decoration: none;
- transition: color 160ms ease;
-}
-
-.landing-nav nav a:hover {
- color: var(--color-ink);
-}
-
-.landing-nav-cta {
- min-height: 38px;
- padding: 0 14px;
- border-radius: 8px;
- background: var(--color-primary);
- color: oklch(0.990 0.000 0);
- font-size: 13px;
- font-weight: 760;
- transition: background-color 160ms ease, transform 160ms ease;
-}
-
-.landing-nav-cta:hover {
- background: var(--color-primary-hover);
-}
-
-.landing-nav-cta:active,
-.landing-button:active {
- transform: translateY(1px);
-}
-
-.landing-hero {
- position: relative;
- min-height: calc(100svh - 132px);
- overflow: hidden;
- display: flex;
- align-items: center;
- padding: 42px 32px;
- background: var(--color-surface-muted);
- isolation: isolate;
-}
-
-.landing-hero-copy {
- position: relative;
- z-index: 2;
- width: min(610px, 56vw);
- animation: landing-copy-in 520ms ease-out both;
-}
-
-.landing-wordmark {
- width: 188px;
- height: auto;
- margin-bottom: 24px;
-}
-
-.landing-hero h1 {
- margin: 0;
- max-width: 12ch;
- color: var(--color-ink);
- font-size: 72px;
- font-weight: 800;
- line-height: 0.98;
- letter-spacing: 0;
- text-wrap: balance;
-}
-
-.landing-hero p {
- margin: 24px 0 0;
- max-width: 62ch;
- color: var(--color-muted);
- font-size: 17px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.landing-actions {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 30px;
-}
-
-.landing-button {
- min-height: 44px;
- padding: 0 18px;
- border-radius: 8px;
- border: 1px solid transparent;
- font-size: 14px;
- font-weight: 780;
- transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms ease;
-}
-
-.landing-button-primary {
- background: var(--color-primary);
- color: oklch(0.990 0.000 0);
-}
-
-.landing-button-primary:hover {
- background: var(--color-primary-hover);
-}
-
-.landing-button-secondary {
- border-color: var(--color-border-strong);
- background: var(--color-surface);
- color: var(--color-ink);
-}
-
-.landing-button-secondary:hover {
- background: var(--color-primary-soft);
- border-color: var(--color-primary-soft-strong);
- color: var(--color-primary-hover);
-}
-
-.landing-hero-scene {
- position: absolute;
- inset: 0;
- z-index: 1;
- pointer-events: none;
-}
-
-.landing-preview-window {
- position: absolute;
- right: max(32px, 5vw);
- bottom: max(-42px, -4vw);
- width: min(620px, 48vw);
- padding: 16px;
- border: 1px solid oklch(0.790 0.038 260);
- border-radius: 14px;
- background: var(--color-surface);
- transform: rotate(-2deg);
- animation: landing-preview-in 680ms ease-out 120ms both, landing-preview-float 7600ms ease-in-out 900ms infinite;
-}
-
-.landing-preview-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- min-height: 42px;
- border-bottom: 1px solid var(--color-border);
- padding-bottom: 12px;
-}
-
-.landing-preview-bar img {
- width: 132px;
- height: auto;
-}
-
-.landing-preview-bar span {
- color: var(--color-muted);
- font-size: 12px;
- font-weight: 760;
-}
-
-.landing-preview-grid {
- display: grid;
- grid-template-columns: minmax(0, 1.5fr) minmax(190px, 0.8fr);
- gap: 12px;
- padding-top: 14px;
-}
-
-.landing-preview-main,
-.landing-preview-side,
-.landing-signal-row {
- border: 1px solid var(--color-border);
- border-radius: 8px;
- background: var(--color-surface);
-}
-
-.landing-preview-main,
-.landing-preview-side {
- padding: 16px;
-}
-
-.landing-preview-main span,
-.landing-preview-side span {
- display: block;
- color: var(--color-muted);
- font-size: 12px;
- font-weight: 760;
-}
-
-.landing-preview-main strong {
- display: block;
- margin-top: 10px;
- font-size: 44px;
- font-weight: 800;
- line-height: 1;
-}
-
-.landing-preview-side strong {
- display: block;
- margin-top: 10px;
- font-size: 28px;
- font-weight: 800;
-}
-
-.landing-preview-side p {
- margin: 8px 0 0;
- color: var(--color-muted);
- font-size: 12px;
- line-height: 1.5;
-}
-
-.landing-chart {
- height: 170px;
- display: flex;
- align-items: end;
- gap: 9px;
- margin-top: 20px;
- padding-top: 22px;
- border-top: 1px solid var(--color-border);
-}
-
-.landing-chart i {
- flex: 1;
- border-radius: 8px 8px 0 0;
- background: var(--color-primary);
-}
-
-.landing-signal-list {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px;
- margin-top: 12px;
-}
-
-.landing-signal-row {
- min-height: 48px;
- display: flex;
- align-items: center;
- gap: 9px;
- padding: 0 12px;
- color: var(--color-ink);
- font-size: 12px;
- font-weight: 700;
- animation: landing-signal-in 480ms ease-out 380ms both;
-}
-
-.landing-signal-row svg:first-child {
- color: var(--color-success);
-}
-
-.landing-signal-row:nth-child(2) svg {
- color: var(--color-warning);
-}
-
-.landing-proof {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 1px;
- border-top: 1px solid var(--color-border);
- border-bottom: 1px solid var(--color-border);
- background: var(--color-border);
-}
-
-.landing-proof div {
- min-height: 128px;
- display: grid;
- align-content: center;
- gap: 9px;
- padding: 24px 32px;
- background: var(--color-surface);
-}
-
-.landing-proof strong {
- font-size: 34px;
- font-weight: 800;
- line-height: 1;
-}
-
-.landing-proof span {
- max-width: 32ch;
- color: var(--color-muted);
- font-size: 13px;
- line-height: 1.55;
-}
-
-.landing-section,
-.landing-detail,
-.landing-final {
- padding: 88px 32px;
-}
-
-.landing-section {
- display: grid;
- grid-template-columns: minmax(280px, 0.82fr) minmax(0, 1.18fr);
- gap: clamp(32px, 6vw, 92px);
- align-items: start;
-}
-
-.landing-section-copy span,
-.landing-detail span {
- color: var(--color-primary);
- font-size: 13px;
- font-weight: 800;
-}
-
-.landing-section h2,
-.landing-detail h2,
-.landing-final h2 {
- margin: 12px 0 0;
- max-width: 13ch;
- font-size: 44px;
- font-weight: 800;
- line-height: 1.05;
- letter-spacing: 0;
- text-wrap: balance;
-}
-
-.landing-section-copy p {
- margin: 18px 0 0;
- max-width: 48ch;
- color: var(--color-muted);
- font-size: 15px;
- line-height: 1.7;
-}
-
-.landing-workflow {
- display: grid;
- gap: 0;
- border-top: 1px solid var(--color-border);
-}
-
-.landing-workflow-item {
- display: grid;
- grid-template-columns: 40px minmax(0, 1fr);
- gap: 16px;
- padding: 22px 0;
- border-bottom: 1px solid var(--color-border);
-}
-
-.landing-workflow-item svg {
- margin-top: 1px;
- color: var(--color-primary);
-}
-
-.landing-workflow-item strong {
- display: block;
- font-size: 16px;
- font-weight: 800;
-}
-
-.landing-workflow-item p {
- margin: 6px 0 0;
- max-width: 72ch;
- color: var(--color-muted);
- font-size: 14px;
- line-height: 1.65;
-}
-
-.landing-detail {
- display: grid;
- grid-template-columns: minmax(280px, 0.86fr) minmax(0, 1.14fr);
- gap: clamp(32px, 6vw, 92px);
- border-top: 1px solid var(--color-border);
- background: var(--color-surface);
-}
-
-.landing-detail-list {
- display: grid;
- gap: 22px;
- align-content: start;
-}
-
-.landing-detail-list p {
- margin: 0;
- max-width: 68ch;
- color: var(--color-muted);
- font-size: 17px;
- line-height: 1.72;
-}
-
-.landing-final {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 32px;
- border-top: 1px solid var(--color-border);
- background: var(--color-surface-muted);
-}
-
-.landing-final h2 {
- max-width: 16ch;
-}
-
-.landing-final p {
- margin: 12px 0 0;
- color: var(--color-muted);
- font-size: 15px;
- line-height: 1.65;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-@keyframes landing-copy-in {
- from {
- opacity: 0;
- transform: translateY(14px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes landing-preview-in {
- from {
- opacity: 0;
- transform: translateX(22px) rotate(-2deg);
- }
-
- to {
- opacity: 1;
- transform: translateX(0) rotate(-2deg);
- }
-}
-
-@keyframes landing-preview-float {
- 0%,
- 100% {
- transform: translateY(0) rotate(-2deg);
- }
-
- 50% {
- transform: translateY(-10px) rotate(-2deg);
- }
-}
-
-@keyframes landing-signal-in {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@media (max-width: 1180px) {
- .command-box {
- width: min(360px, 32vw);
- }
-
- .landing-hero h1 {
- font-size: 58px;
- }
-
- .landing-preview-window {
- width: min(540px, 46vw);
- }
-
- .metrics-grid {
- grid-template-columns: repeat(3, minmax(0, 1fr));
- }
-
- .dashboard-grid,
- .upload-layout,
- .mapping-layout,
- .expenses-layout {
- grid-template-columns: 1fr;
- }
-
- .chart-wrap svg {
- height: 224px;
- }
-}
-
-@media (max-width: 860px) {
- .landing-nav {
- height: 64px;
- padding: 0 16px;
- }
-
- .landing-nav nav {
- display: none;
- }
-
- .landing-brand span {
- display: none;
- }
-
- .landing-nav-cta {
- margin-left: auto;
- }
-
- .landing-hero {
- min-height: auto;
- display: grid;
- gap: 28px;
- padding: 34px 16px 28px;
- }
-
- .landing-hero-copy {
- width: 100%;
- order: 1;
- }
-
- .landing-wordmark {
- width: 154px;
- margin-bottom: 20px;
- }
-
- .landing-hero h1 {
- max-width: 11ch;
- font-size: 43px;
- }
-
- .landing-hero p {
- font-size: 15px;
- }
-
- .landing-actions {
- flex-direction: column;
- align-items: stretch;
- }
-
- .landing-hero-scene {
- position: relative;
- inset: auto;
- order: 2;
- }
-
- .landing-preview-window {
- position: relative;
- right: auto;
- bottom: auto;
- width: 100%;
- max-height: 208px;
- overflow: hidden;
- transform: none;
- animation: landing-copy-in 520ms ease-out 120ms both;
- }
-
- .landing-preview-grid,
- .landing-signal-list,
- .landing-proof,
- .landing-section,
- .landing-detail {
- grid-template-columns: 1fr;
- }
-
- .landing-preview-grid {
- gap: 0;
- }
-
- .landing-preview-side,
- .landing-signal-list {
- display: none;
- }
-
- .landing-preview-main strong {
- font-size: 34px;
- }
-
- .landing-chart {
- height: 104px;
- }
-
- .landing-proof div {
- min-height: 110px;
- padding: 22px 16px;
- }
-
- .landing-section,
- .landing-detail,
- .landing-final {
- padding: 64px 16px;
- }
-
- .landing-section h2,
- .landing-detail h2,
- .landing-final h2 {
- font-size: 34px;
- }
-
- .landing-final {
- align-items: stretch;
- flex-direction: column;
- }
-
- .app-shell {
- grid-template-columns: 1fr;
- }
-
- .sidebar {
- position: fixed;
- inset: 0 auto 0 0;
- z-index: 50;
- width: min(84vw, 304px);
- transform: translateX(-100%);
- transition: transform 180ms ease;
- }
-
- .sidebar-open {
- transform: translateX(0);
- }
-
- .icon-button.sidebar-close,
- .icon-button.topbar-menu,
- .mobile-scrim {
- display: inline-flex;
- }
-
- .mobile-scrim {
- position: fixed;
- inset: 0;
- z-index: 40;
- border: 0;
- background: oklch(0.170 0.020 260 / 0.42);
- }
-
- .topbar {
- justify-content: flex-start;
- gap: 12px;
- padding: 0 16px;
- }
-
- .topbar-actions,
- .workspace-switcher span,
- .command-box {
- display: none;
- }
-
- .workspace-switcher {
- flex: 1;
- min-width: 0;
- }
-
- .page-frame {
- padding: 16px;
- }
-
- .page-header {
- flex-direction: column;
- align-items: stretch;
- }
-
- .page-header h1 {
- font-size: 25px;
- }
-
- .filter-row,
- .metrics-grid,
- .metrics-grid.four {
- grid-template-columns: 1fr;
- }
-
- .mapping-row {
- grid-template-columns: 1fr;
- align-items: stretch;
- }
-
- .mapping-footer,
- .empty-state,
- .insight-row {
- align-items: stretch;
- flex-direction: column;
- display: grid;
- }
-
- .empty-state-action {
- margin-left: 0;
- }
-
- .mapping-actions,
- .panel-actions {
- flex-direction: column-reverse;
- }
-
- .button {
- width: 100%;
- min-height: 42px;
- }
-
- .step-indicator {
- overflow-x: auto;
- padding-bottom: 2px;
- }
-
- table {
- min-width: 690px;
- }
-}
-
-@media (max-width: 520px) {
- .landing-preview-bar {
- align-items: flex-start;
- flex-direction: column;
- }
-
- .landing-button {
- width: 100%;
- }
-
- .page-frame {
- padding: 14px;
- }
-
- .metric-card {
- min-height: 112px;
- }
-
- .metric-card strong {
- font-size: 28px;
- }
-
- .panel,
- .metric-card,
- .empty-state {
- padding: 14px;
- }
-
- .drop-zone {
- min-height: 224px;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- *,
- *::before,
- *::after {
- animation-duration: 1ms !important;
- scroll-behavior: auto !important;
- transition-duration: 1ms !important;
- }
-
- .landing-preview-window {
- transform: none !important;
- }
-}
diff --git a/src/styles/landing.css b/src/styles/landing.css
deleted file mode 100644
index b0e173d..0000000
--- a/src/styles/landing.css
+++ /dev/null
@@ -1,1236 +0,0 @@
-@import url("https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&display=swap");
-/* DataBreeze landing redesign */
-.landing-page {
- --landing-deep: oklch(0.172 0.070 263);
- --landing-deep-2: oklch(0.215 0.086 263);
- --landing-deep-3: oklch(0.126 0.052 263);
- --landing-ink: oklch(0.182 0.022 260);
- --landing-muted: oklch(0.458 0.026 260);
- --landing-line: oklch(0.872 0.013 258);
- --landing-paper: oklch(0.988 0.005 258);
- --landing-white: oklch(1.000 0.000 0);
- min-height: 100dvh;
- overflow-x: hidden;
- background: var(--landing-paper);
- color: var(--landing-ink);
-}
-
-.landing-nav {
- position: sticky;
- top: 0;
- z-index: 30;
- height: 68px;
- display: flex;
- align-items: center;
- gap: 24px;
- padding: 0 clamp(18px, 4vw, 54px);
- border-bottom: 1px solid color-mix(in oklch, var(--landing-line) 70%, transparent);
- background: color-mix(in oklch, var(--landing-white) 92%, transparent);
- backdrop-filter: blur(14px);
-}
-
-.landing-brand,
-.landing-nav-cta,
-.landing-button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- text-decoration: none;
-}
-
-.landing-brand {
- width: clamp(188px, 14vw, 220px);
- min-width: 188px;
- color: var(--landing-ink);
- font-size: 15px;
- font-weight: 800;
-}
-
-.landing-brand img {
- width: 100%;
- height: auto;
- border-radius: 0;
- object-fit: contain;
-}
-
-.landing-nav nav {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 24px;
-}
-
-.landing-nav nav a {
- color: var(--landing-muted);
- font-size: 13px;
- font-weight: 700;
- text-decoration: none;
- transition: color 180ms cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.landing-nav nav a:hover {
- color: var(--landing-ink);
-}
-
-.landing-nav-cta {
- min-height: 38px;
- padding: 0 14px;
- border-radius: 8px;
- background: var(--color-primary);
- color: var(--landing-white);
- font-size: 13px;
- font-weight: 800;
- transition:
- background-color 180ms cubic-bezier(0.22, 1, 0.36, 1),
- transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.landing-nav-cta:hover {
- background: var(--color-primary-hover);
- transform: translateY(-1px);
-}
-
-.landing-hero {
- position: relative;
- min-height: calc(100svh - 116px);
- display: grid;
- grid-template-columns: minmax(320px, 0.82fr) minmax(540px, 1.18fr);
- align-items: center;
- gap: clamp(28px, 5vw, 86px);
- padding: clamp(34px, 5vw, 70px) clamp(18px, 5vw, 72px) clamp(34px, 5vw, 66px);
- overflow: hidden;
- background: var(--landing-deep-3);
- color: var(--landing-white);
- isolation: isolate;
-}
-
-.landing-hero::before {
- content: "";
- position: absolute;
- inset: 0;
- z-index: 0;
- background: color-mix(in oklch, var(--color-primary) 36%, var(--landing-deep));
- clip-path: polygon(42% 0, 100% 0, 100% 100%, 58% 100%);
- opacity: 0.74;
-}
-
-.landing-hero-backdrop {
- position: absolute;
- inset: 0;
- z-index: 1;
- overflow: hidden;
- pointer-events: none;
-}
-
-.landing-bg-mark {
- position: absolute;
- top: 50%;
- right: max(-180px, -10vw);
- width: min(760px, 54vw);
- height: auto;
- opacity: 0.11;
- filter: brightness(0) invert(1);
- transform: translateY(-50%) rotate(-11deg);
-}
-
-.landing-bg-lane {
- position: absolute;
- left: clamp(360px, 38vw, 620px);
- right: -7vw;
- height: 1px;
- background: color-mix(in oklch, var(--landing-white) 28%, transparent);
- transform-origin: left center;
-}
-
-.landing-bg-lane::after {
- content: "";
- position: absolute;
- top: -2px;
- left: 0;
- width: 64px;
- height: 5px;
- border-radius: 999px;
- background: color-mix(in oklch, var(--landing-white) 68%, transparent);
- animation: landing-packet-move 5200ms cubic-bezier(0.22, 1, 0.36, 1) infinite;
-}
-
-.landing-bg-lane-one {
- top: 23%;
- transform: rotate(9deg);
-}
-
-.landing-bg-lane-two {
- top: 50%;
- transform: rotate(-2deg);
-}
-
-.landing-bg-lane-two::after {
- animation-delay: 900ms;
-}
-
-.landing-bg-lane-three {
- top: 72%;
- transform: rotate(-10deg);
-}
-
-.landing-bg-lane-three::after {
- animation-delay: 1650ms;
-}
-
-.landing-bg-token {
- position: absolute;
- min-height: 30px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0 11px;
- border: 1px solid color-mix(in oklch, var(--landing-white) 26%, transparent);
- border-radius: 999px;
- background: color-mix(in oklch, var(--landing-white) 13%, transparent);
- color: oklch(0.924 0.016 255);
- font-size: 11px;
- font-weight: 800;
- white-space: nowrap;
- animation: landing-token-float 6400ms ease-in-out infinite;
-}
-
-.landing-bg-token-one {
- top: 17%;
- left: 52%;
-}
-
-.landing-bg-token-two {
- top: 70%;
- left: 45%;
- animation-delay: 700ms;
-}
-
-.landing-bg-token-three {
- right: 5%;
- bottom: 12%;
- animation-delay: 1400ms;
-}
-
-.landing-hero-copy {
- position: relative;
- z-index: 3;
- width: auto;
- max-width: 650px;
- animation: landing-copy-rise 620ms cubic-bezier(0.16, 1, 0.3, 1) both;
-}
-
-.landing-wordmark {
- width: clamp(160px, 15vw, 214px);
- height: auto;
- margin-bottom: clamp(16px, 2.5vw, 28px);
-}
-
-.landing-hero h1 {
- margin: 0;
- max-width: 12.2ch;
- color: var(--landing-white);
- font-size: clamp(44px, 6.4vw, 86px);
- font-weight: 800;
- line-height: 0.98;
- letter-spacing: 0;
- text-wrap: balance;
-}
-
-.landing-hero p {
- margin: clamp(16px, 2vw, 24px) 0 0;
- max-width: 62ch;
- color: oklch(0.910 0.014 255);
- font-size: clamp(15px, 1.35vw, 18px);
- line-height: 1.68;
- text-wrap: pretty;
-}
-
-.landing-actions {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
- margin-top: clamp(22px, 3vw, 34px);
-}
-
-.landing-button {
- min-height: 44px;
- gap: 7px;
- padding: 0 18px;
- border: 1px solid transparent;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 800;
- line-height: 1;
- white-space: nowrap;
- transition:
- background-color 180ms cubic-bezier(0.22, 1, 0.36, 1),
- border-color 180ms cubic-bezier(0.22, 1, 0.36, 1),
- color 180ms cubic-bezier(0.22, 1, 0.36, 1),
- transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.landing-button:hover {
- transform: translateY(-1px);
-}
-
-.landing-button:active,
-.landing-nav-cta:active {
- transform: translateY(1px);
-}
-
-.landing-button-primary {
- background: var(--color-primary);
- color: var(--landing-white);
-}
-
-.landing-button-primary:hover {
- background: var(--color-primary-hover);
-}
-
-.landing-hero .landing-button-primary {
- background: var(--landing-white);
- color: var(--color-primary-hover);
-}
-
-.landing-hero .landing-button-primary:hover {
- background: oklch(0.958 0.018 255);
-}
-
-.landing-button-secondary {
- border-color: color-mix(in oklch, var(--landing-white) 46%, transparent);
- background: transparent;
- color: var(--landing-white);
-}
-
-.landing-button-secondary:hover {
- border-color: var(--landing-white);
- background: color-mix(in oklch, var(--landing-white) 10%, transparent);
-}
-
-.landing-flow-stage {
- position: relative;
- min-height: clamp(420px, 47vw, 566px);
- display: grid;
- grid-template-columns: minmax(150px, 0.72fr) minmax(210px, 0.88fr) minmax(250px, 1fr);
- align-items: center;
- gap: clamp(14px, 2vw, 24px);
- z-index: 3;
- animation: landing-stage-in 760ms cubic-bezier(0.16, 1, 0.3, 1) 80ms both;
-}
-
-.landing-flow-stage::before {
- content: "";
- position: absolute;
- left: 7%;
- right: 7%;
- top: 50%;
- height: 2px;
- background: color-mix(in oklch, var(--landing-white) 38%, transparent);
- transform: scaleX(0);
- transform-origin: left;
- animation: landing-line-draw 920ms cubic-bezier(0.16, 1, 0.3, 1) 360ms forwards;
-}
-
-.landing-file-stack,
-.landing-flow-core,
-.landing-profit-board {
- position: relative;
- z-index: 1;
-}
-
-.landing-file-stack {
- display: grid;
- gap: 12px;
-}
-
-.landing-file-chip {
- min-width: 0;
- display: grid;
- gap: 5px;
- padding: 14px;
- border-radius: 12px;
- background: var(--landing-white);
- color: var(--landing-ink);
- animation: landing-file-enter 720ms cubic-bezier(0.16, 1, 0.3, 1) both, landing-file-breathe 5200ms ease-in-out infinite;
-}
-
-.landing-file-chip:nth-child(1) {
- animation-delay: 120ms, 1200ms;
-}
-
-.landing-file-chip:nth-child(2) {
- transform: translateX(18px);
- animation-delay: 220ms, 1600ms;
-}
-
-.landing-file-chip:nth-child(3) {
- transform: translateX(6px);
- animation-delay: 320ms, 2000ms;
-}
-
-.landing-file-chip span,
-.landing-file-chip small,
-.landing-profit-topline span,
-.landing-core-head span {
- color: var(--landing-muted);
- font-size: 11px;
- font-weight: 800;
- line-height: 1.25;
-}
-
-.landing-file-chip strong {
- min-width: 0;
- overflow: hidden;
- color: var(--landing-ink);
- font-size: 13px;
- font-weight: 800;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.landing-flow-core {
- display: grid;
- gap: 18px;
- padding: clamp(16px, 2.3vw, 24px);
- border-radius: 16px;
- background: var(--color-primary);
- color: var(--landing-white);
- contain: paint;
- animation: landing-core-pulse 4600ms ease-in-out 1000ms infinite;
-}
-
-.landing-core-head {
- display: grid;
- grid-template-columns: 42px minmax(0, 1fr);
- align-items: center;
- gap: 12px;
-}
-
-.landing-core-head img {
- width: 42px;
- height: 42px;
- border-radius: 10px;
- object-fit: cover;
-}
-
-.landing-core-head span {
- color: oklch(0.872 0.042 255);
-}
-
-.landing-core-head strong {
- display: block;
- margin-top: 3px;
- color: var(--landing-white);
- font-size: clamp(16px, 1.4vw, 19px);
- font-weight: 800;
- line-height: 1.2;
-}
-
-.landing-core-line {
- height: 7px;
- overflow: hidden;
- border-radius: 999px;
- background: color-mix(in oklch, var(--landing-white) 24%, transparent);
-}
-
-.landing-core-line span {
- display: block;
- width: 48%;
- height: 100%;
- border-radius: inherit;
- background: var(--landing-white);
- animation: landing-progress-scan 2600ms cubic-bezier(0.22, 1, 0.36, 1) infinite;
-}
-
-.landing-core-steps {
- display: grid;
- gap: 9px;
-}
-
-.landing-core-steps div {
- min-width: 0;
- display: flex;
- align-items: center;
- gap: 8px;
- color: oklch(0.960 0.010 255);
- font-size: 13px;
- font-weight: 750;
-}
-
-.landing-core-steps svg {
- flex: 0 0 auto;
-}
-
-.landing-profit-board {
- display: grid;
- gap: 18px;
- padding: clamp(18px, 2.6vw, 28px);
- border-radius: 16px;
- background: var(--landing-white);
- color: var(--landing-ink);
-}
-
-.landing-profit-topline {
- display: grid;
- gap: 9px;
-}
-
-.landing-profit-topline strong {
- display: block;
- color: var(--landing-ink);
- font-size: clamp(38px, 4.8vw, 62px);
- font-weight: 800;
- line-height: 0.9;
- letter-spacing: 0;
-}
-
-.landing-profit-chart {
- height: clamp(132px, 15vw, 192px);
- display: flex;
- align-items: end;
- gap: 9px;
- padding-top: 14px;
- border-top: 1px solid var(--landing-line);
-}
-
-.landing-profit-chart i {
- flex: 1;
- min-width: 12px;
- border-radius: 7px 7px 0 0;
- background: var(--color-primary);
- transform-origin: bottom;
- animation: landing-bar-rise 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
-}
-
-.landing-profit-chart i:nth-child(1) {
- height: 42%;
- animation-delay: 360ms;
-}
-
-.landing-profit-chart i:nth-child(2) {
- height: 58%;
- animation-delay: 420ms;
-}
-
-.landing-profit-chart i:nth-child(3) {
- height: 48%;
- animation-delay: 480ms;
-}
-
-.landing-profit-chart i:nth-child(4) {
- height: 72%;
- animation-delay: 540ms;
-}
-
-.landing-profit-chart i:nth-child(5) {
- height: 66%;
- animation-delay: 600ms;
-}
-
-.landing-profit-chart i:nth-child(6) {
- height: 88%;
- animation-delay: 660ms;
-}
-
-.landing-profit-alert {
- min-height: 42px;
- display: flex;
- align-items: center;
- gap: 9px;
- padding: 0 12px;
- border-radius: 8px;
- background: var(--color-warning-soft);
- color: oklch(0.398 0.100 72);
- font-size: 12px;
- font-weight: 800;
-}
-
-.landing-proof {
- position: relative;
- overflow: hidden;
- display: grid;
- grid-template-columns: minmax(280px, 1fr) minmax(150px, 0.28fr) minmax(190px, 0.32fr);
- align-items: stretch;
- border-bottom: 1px solid var(--landing-line);
- background: var(--landing-white);
-}
-
-.landing-proof::before {
- content: "";
- position: absolute;
- inset: 0;
- border-top: 1px solid color-mix(in oklch, var(--color-primary) 20%, transparent);
- pointer-events: none;
-}
-
-.landing-proof p,
-.landing-proof div {
- margin: 0;
- min-height: 118px;
- display: grid;
- align-content: center;
- gap: 8px;
- padding: clamp(22px, 3vw, 34px) clamp(18px, 4vw, 54px);
-}
-
-.landing-proof p {
- max-width: 78ch;
- color: var(--landing-muted);
- font-size: 15px;
- line-height: 1.72;
- text-wrap: pretty;
-}
-
-.landing-proof div {
- border-left: 1px solid var(--landing-line);
-}
-
-.landing-proof strong {
- color: var(--landing-ink);
- font-size: clamp(26px, 2.8vw, 36px);
- font-weight: 800;
- line-height: 1;
-}
-
-.landing-proof span {
- color: var(--landing-muted);
- font-size: 12px;
- font-weight: 750;
- line-height: 1.45;
-}
-
-.landing-section,
-.landing-trust,
-.landing-final {
- padding: clamp(68px, 8vw, 118px) clamp(18px, 5vw, 72px);
-}
-
-.landing-section {
- position: relative;
- overflow: hidden;
- display: grid;
- grid-template-columns: minmax(280px, 0.78fr) minmax(0, 1.22fr);
- align-items: start;
- gap: clamp(36px, 7vw, 104px);
- background: var(--landing-paper);
-}
-
-.landing-section::before {
- content: "";
- position: absolute;
- right: clamp(18px, 5vw, 72px);
- top: 44px;
- bottom: 44px;
- width: min(32vw, 420px);
- border-top: 1px solid var(--landing-line);
- border-bottom: 1px solid var(--landing-line);
- opacity: 0.58;
- pointer-events: none;
-}
-
-.landing-section-copy,
-.landing-workflow {
- position: relative;
- z-index: 1;
-}
-
-.landing-section-copy span,
-.landing-trust-copy span {
- color: var(--color-primary);
- font-size: 13px;
- font-weight: 800;
-}
-
-.landing-section h2,
-.landing-trust h2,
-.landing-final h2 {
- margin: 12px 0 0;
- max-width: 15ch;
- color: var(--landing-ink);
- font-size: clamp(34px, 4.3vw, 58px);
- font-weight: 800;
- line-height: 1.04;
- letter-spacing: 0;
- text-wrap: balance;
-}
-
-.landing-section-copy p,
-.landing-trust-copy p {
- margin: 18px 0 0;
- max-width: 58ch;
- color: var(--landing-muted);
- font-size: 15px;
- line-height: 1.72;
- text-wrap: pretty;
-}
-
-.landing-workflow {
- display: grid;
- border-top: 1px solid var(--landing-line);
-}
-
-.landing-workflow-item {
- display: grid;
- grid-template-columns: 42px minmax(0, 1fr);
- gap: 18px;
- padding: 24px 0;
- border-bottom: 1px solid var(--landing-line);
- transition:
- color 180ms cubic-bezier(0.22, 1, 0.36, 1),
- transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.landing-workflow-item:hover {
- transform: translateX(4px);
-}
-
-.landing-workflow-item svg {
- color: var(--color-primary);
-}
-
-.landing-workflow-item strong {
- display: block;
- color: var(--landing-ink);
- font-size: 17px;
- font-weight: 800;
-}
-
-.landing-workflow-item p {
- margin: 7px 0 0;
- max-width: 76ch;
- color: var(--landing-muted);
- font-size: 14px;
- line-height: 1.68;
-}
-
-.landing-trust {
- position: relative;
- overflow: hidden;
- display: grid;
- grid-template-columns: minmax(280px, 0.86fr) minmax(0, 1.14fr);
- gap: clamp(34px, 7vw, 104px);
- background: var(--landing-deep-3);
- border-top: 1px solid color-mix(in oklch, var(--landing-white) 12%, transparent);
- border-bottom: 1px solid color-mix(in oklch, var(--landing-white) 12%, transparent);
- color: var(--landing-white);
-}
-
-.landing-trust::before {
- content: "";
- position: absolute;
- right: -160px;
- bottom: -230px;
- width: min(620px, 60vw);
- aspect-ratio: 1;
- background: url("/brand/databreeze-mark-dark.png") center / contain no-repeat;
- opacity: 0.07;
- filter: brightness(0) invert(1);
- transform: rotate(14deg);
-}
-
-.landing-trust-copy,
-.landing-trust-lanes {
- position: relative;
- z-index: 1;
-}
-
-.landing-trust h2,
-.landing-trust-row span {
- color: var(--landing-white);
-}
-
-.landing-trust-copy p,
-.landing-trust-row p {
- color: oklch(0.862 0.018 255);
-}
-
-.landing-trust-row p {
- margin: 0;
- font-size: 14px;
- line-height: 1.62;
-}
-
-.landing-trust-lanes {
- display: grid;
- align-content: start;
- border-top: 1px solid color-mix(in oklch, var(--landing-white) 18%, transparent);
-}
-
-.landing-trust-row {
- display: grid;
- grid-template-columns: minmax(86px, 0.24fr) minmax(120px, 0.3fr) minmax(0, 1fr);
- align-items: baseline;
- gap: 18px;
- padding: 20px 0;
- border-bottom: 1px solid color-mix(in oklch, var(--landing-white) 18%, transparent);
-}
-
-.landing-trust-row strong {
- color: oklch(0.800 0.090 205);
- font-size: clamp(26px, 3vw, 38px);
- font-weight: 800;
- line-height: 1;
-}
-
-.landing-trust-row span {
- color: var(--landing-ink);
- font-size: 14px;
- font-weight: 800;
-}
-
-.landing-final {
- position: relative;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 32px;
- background: var(--landing-white);
-}
-
-.landing-final::before {
- content: "";
- position: absolute;
- inset: 22px clamp(18px, 5vw, 72px);
- border-top: 1px solid var(--landing-line);
- border-bottom: 1px solid var(--landing-line);
- pointer-events: none;
-}
-
-.landing-final > * {
- position: relative;
- z-index: 1;
-}
-
-.landing-final img {
- width: 178px;
- height: auto;
- margin-bottom: 18px;
-}
-
-.landing-final h2 {
- max-width: 18ch;
-}
-
-@keyframes landing-copy-rise {
- from {
- opacity: 0;
- transform: translateY(18px);
- filter: blur(8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- filter: blur(0);
- }
-}
-
-@keyframes landing-stage-in {
- from {
- opacity: 0;
- transform: translateY(24px) scale(0.985);
- }
-
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-
-@keyframes landing-line-draw {
- to {
- transform: scaleX(1);
- }
-}
-
-@keyframes landing-file-enter {
- from {
- opacity: 0;
- transform: translateX(-34px);
- }
-
- to {
- opacity: 1;
- }
-}
-
-@keyframes landing-file-breathe {
- 0%,
- 100% {
- translate: 0 0;
- }
-
- 50% {
- translate: 0 -7px;
- }
-}
-
-@keyframes landing-core-pulse {
- 0%,
- 100% {
- background: var(--color-primary);
- }
-
- 50% {
- background: var(--color-primary-hover);
- }
-}
-
-@keyframes landing-progress-scan {
- 0% {
- transform: translateX(-110%);
- }
-
- 48%,
- 100% {
- transform: translateX(220%);
- }
-}
-
-@keyframes landing-bar-rise {
- from {
- opacity: 0;
- transform: scaleY(0.12);
- }
-
- to {
- opacity: 1;
- transform: scaleY(1);
- }
-}
-
-@keyframes landing-packet-move {
- 0% {
- transform: translateX(-80px) scaleX(0.7);
- opacity: 0;
- }
-
- 18% {
- opacity: 0.78;
- }
-
- 74% {
- opacity: 0.78;
- }
-
- 100% {
- transform: translateX(80vw) scaleX(1);
- opacity: 0;
- }
-}
-
-@keyframes landing-token-float {
- 0%,
- 100% {
- transform: translateY(0);
- }
-
- 50% {
- transform: translateY(-8px);
- }
-}
-
-@media (max-width: 1180px) {
- .landing-hero {
- grid-template-columns: minmax(300px, 0.9fr) minmax(460px, 1.1fr);
- }
-
- .landing-flow-stage {
- grid-template-columns: minmax(130px, 0.66fr) minmax(185px, 0.9fr) minmax(220px, 1fr);
- }
-
- .landing-file-chip strong {
- font-size: 12px;
- }
-}
-
-@media (max-width: 900px) {
- .landing-nav {
- height: 60px;
- padding: 0 16px;
- }
-
- .landing-brand {
- width: 176px;
- min-width: 176px;
- }
-
- .landing-nav nav {
- display: none;
- }
-
- .landing-nav-cta {
- margin-left: auto;
- }
-
- .landing-hero {
- min-height: auto;
- grid-template-columns: 1fr;
- gap: 18px;
- padding: 26px 16px 22px;
- }
-
- .landing-hero::before {
- background: color-mix(in oklch, var(--color-primary) 30%, var(--landing-deep));
- clip-path: polygon(0 54%, 100% 38%, 100% 100%, 0 100%);
- }
-
- .landing-bg-mark {
- top: 76%;
- right: -112px;
- width: 380px;
- opacity: 0.09;
- }
-
- .landing-bg-lane,
- .landing-bg-token {
- display: none;
- }
-
- .landing-hero-copy {
- max-width: 100%;
- }
-
- .landing-wordmark {
- width: 138px;
- margin-bottom: 14px;
- }
-
- .landing-hero h1 {
- max-width: 13ch;
- font-size: clamp(36px, 11vw, 48px);
- line-height: 1;
- }
-
- .landing-hero p {
- margin-top: 14px;
- max-width: 56ch;
- font-size: 14px;
- line-height: 1.58;
- }
-
- .landing-actions {
- gap: 10px;
- margin-top: 18px;
- }
-
- .landing-actions .landing-button {
- flex: 1 1 150px;
- }
-
- .landing-flow-stage {
- min-height: 196px;
- grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
- gap: 10px;
- }
-
- .landing-flow-stage::before {
- left: 10%;
- right: 10%;
- }
-
- .landing-file-stack {
- display: none;
- }
-
- .landing-flow-core,
- .landing-profit-board {
- height: 188px;
- align-content: space-between;
- gap: 10px;
- padding: 12px;
- border-radius: 12px;
- }
-
- .landing-core-head {
- grid-template-columns: 32px minmax(0, 1fr);
- gap: 8px;
- }
-
- .landing-core-head img {
- width: 32px;
- height: 32px;
- border-radius: 8px;
- }
-
- .landing-core-head strong {
- font-size: 12px;
- }
-
- .landing-core-head span,
- .landing-file-chip span,
- .landing-file-chip small,
- .landing-profit-topline span {
- font-size: 10px;
- }
-
- .landing-core-line {
- height: 5px;
- }
-
- .landing-core-steps {
- gap: 6px;
- }
-
- .landing-core-steps div {
- gap: 6px;
- font-size: 11px;
- }
-
- .landing-profit-topline {
- gap: 5px;
- }
-
- .landing-profit-topline strong {
- font-size: 31px;
- }
-
- .landing-profit-chart {
- height: 72px;
- gap: 6px;
- padding-top: 10px;
- }
-
- .landing-profit-chart i {
- min-width: 6px;
- border-radius: 5px 5px 0 0;
- }
-
- .landing-profit-alert {
- min-height: 30px;
- gap: 6px;
- padding: 0 8px;
- font-size: 10px;
- }
-
- .landing-proof,
- .landing-section,
- .landing-trust {
- grid-template-columns: 1fr;
- }
-
- .landing-section::before,
- .landing-final::before {
- display: none;
- }
-
- .landing-proof p,
- .landing-proof div {
- min-height: 86px;
- padding: 18px 16px;
- }
-
- .landing-proof div {
- border-left: 0;
- border-top: 1px solid var(--landing-line);
- }
-
- .landing-section,
- .landing-trust,
- .landing-final {
- padding: 58px 16px;
- }
-
- .landing-section h2,
- .landing-trust h2,
- .landing-final h2 {
- font-size: clamp(32px, 9vw, 42px);
- }
-
- .landing-workflow-item {
- grid-template-columns: 34px minmax(0, 1fr);
- gap: 14px;
- padding: 20px 0;
- }
-
- .landing-trust-row {
- grid-template-columns: minmax(72px, 0.32fr) minmax(0, 1fr);
- gap: 8px 14px;
- }
-
- .landing-trust-row p {
- grid-column: 2;
- }
-
- .landing-final {
- align-items: stretch;
- flex-direction: column;
- }
-}
-
-@media (max-width: 520px) {
- .landing-nav {
- gap: 12px;
- }
-
- .landing-brand {
- width: 164px;
- min-width: 164px;
- }
-
- .landing-nav-cta {
- min-height: 36px;
- padding: 0 11px;
- }
-
- .landing-button {
- min-height: 42px;
- width: auto;
- padding: 0 13px;
- font-size: 13px;
- }
-
- .landing-hero {
- padding-top: 22px;
- }
-
- .landing-flow-stage {
- min-height: 182px;
- }
-
- .landing-flow-core,
- .landing-profit-board {
- height: 178px;
- }
-
- .landing-profit-alert span {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .landing-trust-row {
- grid-template-columns: 1fr;
- }
-
- .landing-trust-row p {
- grid-column: auto;
- }
-}
-
-@media (max-width: 380px) {
- .landing-brand {
- width: 152px;
- min-width: 152px;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .landing-flow-stage::before,
- .landing-bg-lane::after,
- .landing-bg-token,
- .landing-core-line span,
- .landing-file-chip,
- .landing-flow-core,
- .landing-profit-chart i,
- .landing-hero-copy,
- .landing-flow-stage {
- animation: none !important;
- }
-
- .landing-flow-stage::before {
- transform: scaleX(1);
- }
-}
diff --git a/vite.config.ts b/vite.config.ts
index 888ede1..ae74518 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,7 +1,6 @@
-import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
- plugins: [react(), tailwindcss()],
+ plugins: [react()],
})