feat(components): ActionButton stopAlert + typography type fixes

This commit is contained in:
fightrate-bot committed 2026-06-16 01:50:26 -03:00
1 parent 2c51fd2623
commit bbe2919c2c
89 files changed
+75993 -9961

No files matched your search

+276
View File
@@ -0,0 +1,276 @@
# FightRate Development Workflow
## Objetivo
Este documento define o padrão de desenvolvimento do FightRate com apoio de IA. O objetivo é evoluir o app com qualidade, reduzir retrabalho, evitar código morto e reaproveitar o que já existe antes de criar novas estruturas.
## Princípio central
> Antes de criar algo novo, procure e avalie o que já existe.
A IA deve atuar como engenheira disciplinada: entender o problema, auditar o código existente, propor uma solução pequena, implementar em fatias, verificar e revisar.
## Níveis de mudança
### Nível 1 — Ajuste pequeno
Use para copy, espaçamento, correção simples ou bug isolado.
Fluxo:
1. Mini-spec de 2 a 5 linhas.
2. Editar o menor trecho possível.
3. Compilar ou rodar teste relacionado.
4. Responder com evidência.
### Nível 2 — Feature média
Use para melhorias em uma tela, novo comportamento de domínio, persistência pequena ou ajustes de BLE sem mudança arquitetural.
Fluxo:
1. Spec curta.
2. Auditoria do existente.
3. Plano em fatias.
4. Teste de domínio quando houver regra de negócio.
5. Implementação incremental.
6. Build/test final.
7. Revisão de código e UI.
### Nível 3 — Feature grande
Use para novos fluxos de treino, histórico avançado, dashboards, mudanças de banco importantes, BLE/background tracking ou nova tela grande.
Fluxo:
1. Spec completa.
2. Aprovação humana.
3. Plano técnico.
4. Tarefas pequenas com acceptance/verify.
5. Implementação uma tarefa por vez.
6. Revisão final e atualização do roadmap.
## Intake obrigatório
Antes de implementar, responder:
```text
O que vamos melhorar?
Qual problema real isso resolve?
Em qual tela/fluxo?
Como saberemos que ficou bom?
```
Quando o pedido for vago, converter para critérios testáveis.
Exemplo:
```text
Pedido: melhorar alertas.
Critérios:
- usuário configura limite cardíaco;
- alerta não dispara sem controle;
- vibração/som são opcionais;
- UI em PT-BR;
- build final passa.
```
## Auditoria do existente
Antes de criar arquivo, classe, componente ou dependência, verificar:
```text
Já existe domínio parecido?
Já existe state no ViewModel?
Já existe componente visual reutilizável?
Já existe persistência parecida?
Já existe teste relacionado?
```
Pontos comuns de reaproveitamento:
```text
HeartRateSampleStore
LiveHeartRateViewModel
LiveHeartRateUiState
HeartRateZones
HeartRateForegroundService
HomeScreen
FightCard
MetricTile
LiveMetricPill
```
Regra:
> Se a IA não consegue explicar por que um arquivo novo precisa existir, ela não deve criar esse arquivo.
## Arquitetura esperada
### Domínio
Use `domain/` para regra pura e testável sem Android.
Exemplos:
```text
RoundTimer.kt
HeartRateZones.kt
WorkoutSession.kt
HeartRateAlertSettings.kt
```
### Dados
Use `data/` para persistência, exportação e acesso local.
```text
HeartRateSampleStore.kt
HeartRateExporter.kt
```
SQLite local-first continua sendo a fonte de verdade.
### BLE / Android
Use `ble/`, `service/` e `system/` para integração Android.
Regra importante:
> A conexão BLE pertence ao `HeartRateForegroundService`, não ao ViewModel.
### UI
Use `ui/screens/`, `ui/live/` e `ui/theme/`.
A UI deve consumir estado pronto e não concentrar regra de negócio pesada.
## Padrão de fatias
Implementar em fatias pequenas:
```text
Fatia 1 — domínio puro
Fatia 2 — persistência
Fatia 3 — ViewModel/state
Fatia 4 — UI
Fatia 5 — integração Android/BLE
Fatia 6 — revisão
```
Cada fatia deve declarar:
```text
Acceptance: o que precisa funcionar.
Verify: comando ou checagem que comprova.
```
## TDD obrigatório quando houver regra
Escrever teste antes ou junto para:
```text
parser BLE
zonas cardíacas
timer de rounds
resumo de treino
alertas
janelas temporais
estatísticas
migração/persistência crítica
```
UI simples não exige TDD pesado, mas exige build e revisão visual/UX.
## Quality gates
Antes de concluir qualquer mudança média/grande:
```bash
./gradlew :app:testDebugUnitTest :app:assembleDebug
```
Para domínio específico:
```bash
./gradlew :app:testDebugUnitTest --tests "*NomeDoTeste"
```
Também verificar:
```text
- UI visível em PT-BR;
- identificadores de código em inglês;
- sem código morto óbvio;
- sem componente duplicado desnecessário;
- sem Canvas custom frágil para gráfico/gauge;
- sem dependência nova sem justificativa;
- sem quebrar background BLE;
- sem alterar minSdk 26 sem aprovação.
```
## Sempre fazer
- Reaproveitar componentes existentes.
- Preservar SQLite como fonte local.
- Escrever domínio puro para regra de negócio.
- Testar domínio antes da UI quando aplicável.
- Manter textos visíveis em PT-BR.
- Manter código em inglês.
- Rodar build final.
- Atualizar roadmap/Obsidian quando mudar escopo relevante.
## Perguntar antes
- Adicionar dependência nova.
- Fazer mudança destrutiva no schema SQLite.
- Criar tela grande nova.
- Trocar biblioteca de gráfico/UI.
- Alterar BLE service/background tracking.
- Mudar minSdk, targetSdk, AGP, Kotlin ou Gradle.
## Nunca fazer
- Criar gráfico/gauge em Canvas custom sem necessidade clara.
- Remover teste falhando só para passar build.
- Criar componente novo se um existente resolve.
- Deixar código morto “para usar depois”.
- Implementar feature sem critério de sucesso.
- Alterar texto do app para inglês sem pedido.
- Misturar refactor grande com feature.
## Checklist antes de implementar
```text
[ ] Existe spec ou mini-spec?
[ ] Critérios de sucesso são testáveis?
[ ] Arquivos existentes foram auditados?
[ ] Componentes reaproveitados foram listados?
[ ] Novos arquivos têm responsabilidade clara?
[ ] Fatia atual toca no máximo 3–5 arquivos principais?
[ ] Há teste para regra de domínio?
[ ] Há comando de verificação definido?
```
## Checklist antes de responder “concluído”
```text
[ ] Testes/build rodaram.
[ ] Resultado foi registrado.
[ ] APK foi localizado quando aplicável.
[ ] Roadmap/Obsidian foi atualizado quando aplicável.
[ ] Limitações foram declaradas.
```
## Dependências aprovadas (histórico)
Adicionar uma dependência nova continua exigindo aprovação explícita. Esta seção registra as que já passaram pelo gate, com a justificativa de produto:
- `react-native-gesture-handler` (aprovado em 2026-06-14) — necessário para gestos nativos de pinch e pan no `TrainingChart` dinâmico (viewport com zoom, pan e tap). O `PanResponder` builtin não tem pinch de 2 dedos e perde precisão fora do viewBox.
- `react-native-reanimated` (aprovado em 2026-06-14) — worklets para gestures performáticos em tempo real (atualização do viewport durante o pinch sem travar a thread JS).
- `react-native-worklets` (aprovado em 2026-06-14) — peer dep de `react-native-reanimated` 4.x; runtime paralelo a JS.
Todas as três exigem a New Architecture habilitada (`newArchEnabled=true` no `android/gradle.properties`) e setup específico:
- `react-native-gesture-handler` importado no topo de `index.js` antes de qualquer outro React Native.
- App envolto em `GestureHandlerRootView`.
- Babel com `react-native-worklets/plugin`.
+551
View File
@@ -0,0 +1,551 @@
# FightRate — Full Project Context for AI Successor
## Author: Evandro
## Stack: Android Native (Kotlin, Jetpack Compose, Material3)
---
## 1. DEV ENVIRONMENT
| Item | Value |
|------|-------|
| Project root | `D:/Documents/Github/HealthTracking` |
| Package | `com.healthtracking.fightrate` |
| Gradle | Wrapper 8.11.1 |
| AGP | 8.9.1 |
| Kotlin | 2.3.21 |
| Compose BOM | 2024.12.01 |
| compileSdk | 36 |
| targetSdk | 36 |
| minSdk | 26 |
| Android SDK | `C:\Users\Evandro\AppData\Local\Android\Sdk` |
| Java | JDK 17 (jvmToolchain) |
| App name | **FightRate** |
| APK naming | `FightRate-YYYY-MM-DD-HH-mm-ss-debug.apk` |
| No git repo | Project directory is NOT a git repository |
### Key dependencies
- `navigation-compose:2.8.5`
- `compose.material3`
- `icons-lucide-android:2.2.1` (via `com.composables:icons-lucide-android`)
- **IMPORTANT**: Non-transitive R; access Lucide icons via `import com.composables.icons.lucide.R as LucideR`
- `vico:compose:3.2.2` + `vico:compose-m3:3.2.2` (charting)
- `coil-compose:3.0.4`
- `lottie-compose:6.6.2`
- `fastexcel:0.18.4` (XLSX export)
- `junit:4.13.2` (testing)
### Build commands
```bash
# Full build + test
./gradlew :app:testDebugUnitTest :app:assembleDebug
# Quick compile
./gradlew :app:compileDebugKotlin
# Unit test focused
./gradlew :app:testDebugUnitTest --tests "*AthleteInsightsTest*"
# Install on device
./gradlew :app:installDebug
```
---
## 2. PROJECT VISION
**FightRate** is a native Android app for tracking heart rate during combat/fighting sports using a **Magene H003** chest strap (BLE Heart Rate Service 0x180D).
### Core pillars
1. **Real-time BLE heart rate tracking** — even with screen off, even in background
2. **Workout sessions** — start/pause/resume/finish with round timer
3. **Athlete profile & cardiac zones** — personalized by age, sex, resting HR, measured max HR
4. **Data insights** — readiness, effort, hydration, recovery, fight zone time
5. **Rich history & charts** — time-based Vico charts with compaction, export (CSV/JSON/XLSX)
6. **Scientific cardiac analytics** — statistical and clinical-grade metrics from raw HR data
### UI conventions
- All visible text in **PT-BR** (Portuguese Brazilian)
- All code identifiers in **English**
- Dark theme with signature orange accent (`FightOrange = #FF5733`)
- Bottom navigation tabs: Ao vivo | Sensor | Histórico | Insights | Perfil
- Cards use `FightCard` composable (dark panels with rounded corners)
---
## 3. FILE STRUCTURE
```
app/src/main/java/com/healthtracking/fightrate/
├── MainActivity.kt # Entry point, permission requests
├── ble/
│ ├── AndroidHeartRateBleClient.kt # Concrete BLE GATT client implementation
│ ├── AndroidHeartRateBleScanner.kt # Concrete BLE scanner implementation
│ ├── HeartRateBleClient.kt # BLE client interface
│ ├── HeartRateBleConstants.kt # BLE UUIDs (0x180D, 0x2A37, 0x2A38, 0x180F, 0x2A19)
│ ├── HeartRateBleScanner.kt # BLE scanner interface
│ ├── HeartRateBleStatus.kt # Status sealed class (Connecting/Connected/etc.)
│ ├── HeartRateDevice.kt # Device data class
│ ├── HeartRateParser.kt # BLE heart rate measurement parser
│ └── HeartRateSensorMetadata.kt # Battery level + body sensor location
├── data/
│ ├── HeartRateSampleStore.kt # SQLite DB (heart_rate_samples + workout_sessions)
│ └── HeartRateExporter.kt # CSV/JSON/XLSX export via FileProvider
├── domain/
│ ├── AthleteInsights.kt # BMI, Karvonen, readiness, effort, hydration, fight zone, alerts
│ ├── HeartRateAlertSettings.kt # Alert config + evaluator
│ ├── HeartRateStats.kt # Stats (avg/min/max/stdDev)
│ ├── HeartRateWindows.kt # Rolling time window filter
│ ├── HeartRateZones.kt # Zone calculation (6 zones by % max HR)
│ ├── RoundTimer.kt # Round/rest timer logic
│ ├── WorkoutSession.kt # Session + summary models + calculations
│ └── model/
│ ├── AthleteProfile.kt # Age, sex, height, weight, maxHR, restingHR, sport, class, goal
│ ├── FightSport.kt # Boxing, MuayThai, MMA, BJJ, Wrestling, FreeTraining
│ ├── HeartRateMeasurement.kt # Measurement model
│ └── HeartRateSample.kt # Core sample (timestampMillis, bpm, sport, source, deviceName, sessionId)
├── service/
│ └── HeartRateForegroundService.kt # Foreground BLE service with auto-reconnect + backoff
├── system/
│ ├── BackgroundRunPermission.kt # Battery optimization settings opener
│ └── HeartRateAlertNotifier.kt # Vibration + sound alert dispatcher
└── ui/
├── live/
│ ├── BleConnectionStatus.kt # Connection status enum
│ ├── HistoryPeriod.kt # Time window enum (Last10min/30min/1h/3h/6h/Today)
│ ├── LiveHeartRateUiState.kt # Single source of truth state data class
│ └── LiveHeartRateViewModel.kt # Central AndroidViewModel (~729 lines)
├── screens/
│ └── HomeScreen.kt # ALL UI in one file (~1700+ lines of Compose)
└── theme/
├── Color.kt # Dark theme color palette
└── FightRateTheme.kt # Material3 theme configuration
Tests:
app/src/test/java/com/healthtracking/fightrate/
├── ble/HeartRateParserTest.kt
├── domain/
│ ├── AthleteInsightsTest.kt
│ ├── HeartRateAlertEvaluatorTest.kt
│ ├── HeartRateStatsCalculatorTest.kt
│ ├── HeartRateWindowsTest.kt
│ ├── HeartRateZonesTest.kt
│ ├── RoundTimerTest.kt
│ └── WorkoutSessionSummaryTest.kt
```
---
## 4. DATABASE SCHEMA
### Table: heart_rate_samples (version 4)
```sql
id INTEGER PRIMARY KEY AUTOINCREMENT
timestamp_millis INTEGER NOT NULL
day_key TEXT NOT NULL
bpm INTEGER NOT NULL
sport TEXT NOT NULL
source TEXT NOT NULL
device_name TEXT
session_id INTEGER
```
Indexed on `day_key`, `timestamp_millis`, `(session_id, timestamp_millis)`.
### Table: workout_sessions (created at v3, expanded at v4)
```sql
id INTEGER PRIMARY KEY AUTOINCREMENT
started_at_millis INTEGER NOT NULL
ended_at_millis INTEGER
status TEXT NOT NULL DEFAULT 'Active'
sport TEXT NOT NULL DEFAULT 'Treino livre'
round_duration_seconds INTEGER NOT NULL DEFAULT 180
rest_duration_seconds INTEGER NOT NULL DEFAULT 60
rounds INTEGER NOT NULL DEFAULT 3
accumulated_active_seconds INTEGER NOT NULL DEFAULT 0
last_resumed_at_millis INTEGER
-- v4 columns:
pre_workout_energy INTEGER
pre_workout_fatigue INTEGER
muscle_soreness INTEGER
sleep_hours REAL
sleep_quality INTEGER
pre_workout_weight_kg REAL
water_intake_ml INTEGER
symptoms_note TEXT NOT NULL DEFAULT ''
post_workout_rpe INTEGER
post_workout_fatigue INTEGER
post_workout_soreness INTEGER
post_workout_weight_kg REAL
post_workout_note TEXT NOT NULL DEFAULT ''
```
---
## 5. DATA FLOW
### BLE → Service → ViewModel → UI
```
Magene H003 (BLE)
↓ Heart Rate Service 0x180D / Measurement 0x2A37
AndroidHeartRateBleClient (GATT)
↓ Flow<HeartRateMeasurement>
HeartRateForegroundService
↓ BroadcastReceiver (ActionSample, ActionStatus, ActionMetadata)
LiveHeartRateViewModel.handleServiceSample() / handleServiceStatus() / handleSensorMetadata()
↓ StateFlow<LiveHeartRateUiState>
HomeScreen.kt (@Composable)
```
### Session lifecycle
1. User fills **Check-in** (mandatory dialog) → `startWorkoutSessionWithCheckIn()`
2. SQLite `workout_sessions` inserted, sample `session_id` auto-assigned
3. Round timer runs in ViewModel ticker
4. User can Pause / Resume
5. User taps Finish → **Post-workout** dialog → `finishWorkoutSessionWithPostWorkout()`
6. Session status = Finished, `ended_at_millis` set
---
## 6. CARDIAC ZONES SYSTEM
### Zone thresholds based on % of max heart rate
| Zone | % Max HR | Label (PT-BR) | Usage |
|------|----------|----------------|-------|
| Recovery | 1–59% | Recuperação | Recovery/active rest |
| Base | 60–69% | Base | Aerobic base |
| Vigorous | 70–85% | Vigorosa | Moderate-high intensity |
| Fight | 86–89% | Luta intensa | Fight zone specific |
| RedLine | 90–95% | Muito intensa | Very high intensity |
| Limit | 96–200% | Limite | Maximum effort |
### Max HR formula
- Estimated: `208 - (0.7 × age)` (Tanaka formula)
- Overrideable with `measuredMaxHeartRate` (campo: "limite cardíaco real")
### Karvonen zones (reserve-based)
When resting HR is available, zones shift upward using:
`TargetHR = RestingHR + Reserve × %Intensity`
where `Reserve = MaxHR - RestingHR`
---
## 7. CURRENT METRICS / CALCULATIONS
### Already implemented
1. **BMI** — from weight + height, with category (abaixo do peso / peso adequado / sobrepeso / obesidade)
2. **Heart Rate Reserve** (Karvonen) — MaxHR − RestingHR
3. **Zones by Karvonen** — personalized zone ranges
4. **Readiness Score** (0–100) — based on sleep hours, sleep quality, energy, fatigue, soreness
5. **Workout Effort Score** (0–100) — based on avg%BPM, peak%BPM, duration, RPE, with session load (RPE×min)
6. **Hydration Estimate** — pre/post weight difference + water intake, with fluid loss warning
7. **Fight Zone Time** — seconds and % of session spent in Fight/RedLine/Limit zones
8. **Athlete Insights Alerts** — combined warnings from readiness, hydration, effort, symptoms
9. **Heart Rate Stats** — avg, min, max, standard deviation
10. **Zone Breakdown** — % of session in each zone
11. **Rolling 60s average** — main display; instant BPM shown as "Agora"
---
## 8. BLE RESILIENCE SYSTEM
### Reconnection strategy
- Service runs as foreground with `foregroundServiceType="connectedDevice"`
- `START_STICKY` — Android restarts if killed
- On unexpected disconnect (distance/signal loss), auto-reconnect with backoff:
- 1st: 2s → 2nd: 5s → 3rd: 10s → 4th+: 30s
- Manual disconnect sets `manualDisconnect = true` flag in SharedPreferences
- When `manualDisconnect = true`, no auto-reconnect
- Reconnection state persisted: last device ID, device name, manual flag
- On service restart (START_STICKY), reads persisted state and auto-reconnects if not manual
- No aggressive continuous scanning; reconnects by known device ID first
### Battery optimization
- Single foreground service during session (no persistent background service when idle)
- Notification updates throttled: max every 15s OR >3 BPM delta
- No SQLite polling on every beat from service
- ViewModel polls DB at 5s intervals only when connected
- Rolling 60s average computed in-memory in service
- Guide user to battery optimization settings via `BackgroundRunPermission`
---
## 9. CURRENT UI STRUCTURE (HomeScreen.kt)
Single massive file with all composable functions (~1700+ lines):
### Screens (tabs)
- **LiveScreen** — rolling 10-min chart, instant BPM, 60s avg, zone, Muay Thai comparison
- **SensorScreen** — scan button, device list, battery chip, body sensor location, connection status, background toggle
- **HistoryScreen** — period selector, stats cards, zone breakdown, Vico chart (time-based) with empty state
- **InsightsScreen** — BMI, reserve, readiness, effort, hydration, fight zone, alerts
- **ProfileScreen** — expandable sections (básico, cardiovascular, luta), choice chips for sex/sport, save button
### Key composables
- `FightCard` — dark panel wrapper
- `MetricTile` — stat label+value pair
- `LiveMetricPill` — compact live stat pill
- `HeartRateLineChart` — Vico chart with time-based x-axis
- `EmptyTimeWindowChart` — placeholder when no data
- `RoundSessionCard` — session controls + round timer
- `ExpandedSessionCard` — session metrics
- `AlertSettingsCard` — alert configuration
---
## 10. WHAT DOCTORS CONSIDER FROM HEART RATE DATA (Scientific Foundation)
Below is a structured compilation of medical-grade metrics that can be calculated from the data types FightRate already collects (raw HR timestamps, profile data, session data, sleep, subjective scales). These are well-established in sports cardiology, exercise physiology, and clinical practice.
### 10.1 Resting Heart Rate (RHR)
- **Clinical significance**: Lower RHR = better cardiovascular fitness. Resting tachycardia (>100 bpm) flags medical concern.
- **Can trend**: Weekly RHR average from morning readings. Spike of +5-7 bpm signals possible illness, overtraining, or poor recovery.
- **Calculation**: Mean of lowest 5 readings during rest periods (early morning before rising is gold standard).
### 10.2 Heart Rate Recovery (HRR)
- **Clinical gold standard**: Drop in BPM 1-minute and 2-minutes post-exercise cessation.
- **Normal**: >22 bpm drop at 1 min (adults). Drop <12 bpm = elevated cardiovascular mortality risk (Cole et al., NEJM 1999).
- **Athlete context**: Faster recovery = better conditioning. Slowing recovery trend = functional overreaching or detraining.
- **Can calculate from existing data**: After session ends, look at first 60s and 120s of BPM decline.
### 10.3 Heart Rate Variability (HRV)
- **Key clinical parameters**:
- **RMSSD** (Root Mean Square of Successive Differences): Parasympathetic activity marker. Gold standard for daily monitoring.
- **SDNN** (Standard Deviation of NN intervals): Overall HRV. Clinical risk predictor.
- **LF/HF Ratio**: Sympathetic/parasympathetic balance.
- **From our data**: HRV requires beat-to-beat R-R intervals (ms). **BLE HR Service 0x2A37 can provide RR intervals** if the sensor sends them (Magene H003 does). We already have `HeartRateMeasurement` with potential RR data.
- **Clinical use**: Low HRV = stress, illness risk, overtraining. High HRV = recovery, readiness.
- **Training application**: Morning HRV guides daily training intensity (Daneshvar, 2024).
### 10.4 Cardiac Drift (Cardiovascular Drift)
- **Definition**: Progressive rise in HR during steady-state exercise despite constant workload.
- **Causes**: Dehydration, electrolyte loss, thermoregulatory strain, blood volume shift.
- **Clinical marker**: Drift >10% of starting HR = significant heat/dehydration stress.
- **Can calculate**: Linear regression slope of HR over session duration at constant intensity.
### 10.5 Chronotropic Response / Chronotropic Incompetence
- **Definition**: Inability of HR to rise appropriately with exercise intensity.
- **Calculation**: `%HeartRateReserve achieved / %MetabolicReserve achieved`
- **Clinical threshold**: CI = failure to reach 85% of age-predicted max HR at peak exercise.
- **Detection from data**: Slope of HR increase in first 3 minutes of session.
### 10.6 Heart Rate Deflection Point (HRDP / Conconi Test)
- **Definition**: The point at which HR stops increasing linearly with running speed/workload.
- **Use**: Non-invasive anaerobic threshold estimation.
- **Calculation**: Piecewise linear regression of HR × workload curve.
### 10.7 TRIMP (Training Impulse)
- **Banister's TRIMP**: `duration × (ΔHR/ΔHRmax) × 0.64e^(1.92×ΔHR/ΔHRmax)`
- **MICT TRIMP**: `duration × %MaxHR`
- **Edwards TRIMP (our approach)**: Sum of minutes in each zone × zone coefficient.
- **Clinical use**: Quantifies internal training load. Correlates with adaptation and injury risk.
### 10.8 Training Monotony & Strain (Foster, 1998)
- **Monotony**: `WeeklyMeanLoad / WeeklySDLoad`. High monotony (>2.0) + high strain = overtraining risk.
- **Strain**: `WeeklySumLoad × Monotony`.
- **Data requirement**: Daily session load over 7+ days.
### 10.9 Excess Post-Exercise Oxygen Consumption (EPOC) Estimate
- **Correlation**: Higher HR during session → higher EPOC → more post-workout calorie burn.
- **Proxy metric**: Time above 70% max HR correlates with EPOC duration.
### 10.10 Aerobic Efficiency / Cardiac Economy
- **Metric**: HR at a given perceived exertion level over time. If HR at same RPE decreases, athlete is fitter.
- **Calculation**: Match sessions with same RPE → compare average HR.
### 10.11 Relative VO2max Estimation
- **Astrand-Ryhming nomogram**: Based on HR at submaximal workload with age/sex correction.
- **Uth-Sørensen-Overgaard formula**: `VO2max = 15.3 × (MaxHR / RestingHR)` (approximate).
### 10.12 Heart Rate Turbulence (HRT)
- **Clinical**: Post-premature-ventricular-contraction HR response. Not feasible without ECG.
### 10.13 Summary of actionable clinical-grade metrics from our data
| Metric | Data needed | Clinical value | Feasibility |
|--------|-----------|----------------|-------------|
| Resting HR trend | Morning readings | Overtraining, illness | ✅ Already have |
| HR Recovery (1min, 2min) | Post-session BPM | CV risk, conditioning | ✅ Easy add |
| Cardiac Drift | Session BPM trend | Hydration, heat stress | ✅ Easy add |
| Chronotropic Response | Session onset BPM | CV fitness, beta-blocker effect | ✅ Easy add |
| TRIMP (Edwards) | Zone time | Training load | ✅ Already have zone breakdown |
| TRIMP (Banister) | Duration, avgHR, maxHR | Training load | ⚡ Medium effort |
| Weekly Monotony & Strain | 7d load history | Overtraining risk | ⚡ Need 7d history UI |
| VO2max estimation | MaxHR, RestingHR, sex | Aerobic fitness estimate | ✅ Ready (formula exists) |
| HRV RMSSD/SDNN | RR intervals | Recovery, readiness | ⚡ Need RR parser |
| HR Deflection Point | Incremental test BPM | Anaerobic threshold | ❌ Requires protocol |
| Fatigue slope | HR per minute over session | Pacing, fatigue onset | ✅ Easy add |
---
## 11. WHAT USER WANTS NOW (Pre-approved)
### A. Charts improvement
Make charts more informative and interactive:
- Better tooltips/markers
- Zone-colored areas
- Callout for min/max/avg points
- Session overlay with round/rest phases
- Smoother pan/zoom
- Multiple time ranges
### B. Session-based detailed report
A system that:
1. User can start/stop a **recording time window**
2. All HR samples within that window are collected
3. At stop, a **detailed report** is generated with:
- Full session statistics
- Zone breakdown
- HR Recovery
- Cardiac drift analysis
- Chronotropic response
- TRIMP score
- Readiness context (from pre-session check-in)
- Hydration estimate
- Fight zone time
- Trend compared to past sessions
### C. Scientific/clinical analytics
Research-backed metrics that doctors consider from HR data (section 10 above).
This is now **approved** to implement:
- HR Recovery (1min, 2min)
- Cardiac Drift
- Chronotropic Response
- TRIMP (Banister + Edwards)
- VO2max estimation
- Weekly monotony & strain
- Fatigue slope
- HRV when RR data available
---
## 12. APPROVED DECISIONS (Perfil/Insights P0)
| Item | Decision |
|------|----------|
| Check-in | **Mandatory** before starting session |
| Insights location | **New tab** in bottom navigation |
| Biological sex | **Collected** as Masculino/Feminino |
| Scale range | **0–10** for energy, fatigue, soreness, sleep quality |
| Advanced fields | **Collapsible** sections (expand/hide) |
---
## 13. IMPORTANT CODE PATTERNS & PITFALLS
### Don'ts (from previous learnings)
1. DON'T create custom Canvas charts — use Vico (`com.patrykandpatrick.vico`)
2. DON'T move BLE ownership from foreground service to ViewModel
3. DON'T add new dependencies without asking
4. DON'T use `lineSeries` — Vico API: use `lineModel` instead
5. DON'T add fragil custom Canvas gauges
6. DON'T mix business rules in Compose — keep domain pure Kotlin
7. DON'T make destructive SQLite migrations without approval
8. DON'T add unused code or placeholder features
### Do's
1. DO use `LucideR.drawable.lucide_ic_xxx` for Lucide icons
2. DO test domain with JUnit before UI
3. DO implement in slices: domain → persistence → ViewModel/state → UI → BLE
4. DO use `HeartRateForegroundService.ActionConnect` / `ActionDisconnect` for BLE control
5. DO preserve PT-BR in UI, English in code
6. DO run `./gradlew :app:testDebugUnitTest :app:assembleDebug` before claiming completion
7. DO save data locally first (SQLite source of truth)
8. DO write `collect` calls with explicit `{ }` braces around the when-body in service
9. DO use `ChoiceChipButton`, `ExpandableSectionHeader`, `ProfileNumberField` for consistent UI
### Known non-blocking warnings
- `lineSeries` deprecated → use `lineModel` (low priority)
- BLE `writeDescriptor` deprecated in SDK 36 (non-blocking)
- No LSP client for Kotlin in this environment; rely on `compileDebugKotlin` output
---
## 14. IMMEDIATE NEXT TASK
### Priority: Implement session-based detailed report + scientific analytics
1. **Enhance chart**: Session overlay with round phases, zone colors, better tooltips
2. **Session report system**:
- Start/stop recording independent of session (or same)
- Collect all samples in range
- Generate report at stop with medical-grade metrics
3. **Add new domain calculations**:
- HR Recovery (1min, 2min)
- Cardiac Drift
- Chronotropic Response
- TRIMP (Banister + Edwards)
- VO2max estimation
4. **Report UI**: Dedicated report screen or dialog with full metrics
5. **After done**: send APK via Telegram
### Workflow to follow
```
→ Search web if needed
→ Write/update spec
→ Audit existing code
→ Implement domain + tests (new calculations)
→ Implement persistence if needed
→ Implement ViewModel/state
→ Implement UI (chart + report)
→ Build & test
→ Send APK
→ Update Obsidian roadmap
```
---
## 15. WORKFLOW DOCUMENT
Path: `D:/Documents/Github/HealthTracking/docs/FIGHTRATE_DEVELOPMENT_WORKFLOW.md`
### Project skill: `fightrate-quality-loop`
Scope: project:HealthTracking
### Skill path
`C:\Users\Evandro\.pi\agent\projects-memory\HealthTracking\skills\fightrate-quality-loop\SKILL.md`
### Obsidian vault
Path: `D:\Documents\Obsidian Vault`
Roadmap file: `Projetos/FightRate - Auditoria e Roadmap.md`
---
## 16. COMMANDS QUICK REFERENCE
```bash
# Full verification
./gradlew :app:testDebugUnitTest :app:assembleDebug
# Quick compile
./gradlew :app:compileDebugKotlin
# Domain tests
./gradlew :app:testDebugUnitTest --tests "*AthleteInsightsTest*"
./gradlew :app:testDebugUnitTest --tests "*WorkoutSessionSummary*"
./gradlew :app:testDebugUnitTest --tests "*HeartRateZonesTest*"
# Install APK
./gradlew :app:installDebug
# Latest APK path
app/build/outputs/apk/debug/FightRate-*-debug.apk
```
---
## 17. TELEGRAM CONTEXT
- User delivers messages via Telegram bridge
- `[telegram]` prefix → deliver replies via Telegram
- When sending APK: call `telegram_attach(localPath)`
- The user says: "Ao terminar me mande o apk no telegram"
---
## 18. FINAL NOTES
- This document was built by reading all source files, build files, specs, and tests
- Everything described here is **already implemented** unless marked as "new" or "next"
- The user has **pre-approved** the scientific analytics implementation + session report
- The user wants **continuous improvement** — no stop until done
- Focus on **charts**, **session-based reporting**, and **scientific-grade metrics**
+112
View File
@@ -0,0 +1,112 @@
## Resultado consolidado
A literatura **não fornece uma tabela robusta e universal** de “mínimo, média e máximo” de batimentos **por faixas de 5 anos** para Muay Thai e corrida intensa. O que existe com boa qualidade são:
1. **Dados populacionais de repouso**, por faixas etárias amplas.
2. **Estudos pequenos e específicos** medindo Muay Thai/corrida, geralmente com média ± desvio padrão ou erro padrão.
3. **Estimativas por fórmula** para frequência cardíaca máxima e zonas de intensidade, que não substituem teste real.
Abaixo separei o que é **medido** do que é **estimado**.
---
# 1. Batimentos em repouso — dados populacionais
A tabela abaixo usa dados da Statistics Canada, com média, mediana e percentis. Para não inventar “mínimo” e “máximo”, usei **P5** e **P95** como aproximações populacionais: P5 significa que 5% ficam abaixo; P95 significa que 95% ficam abaixo. Não são extremos absolutos. ([Statistics Canada][1])
| Faixa etária | P5 “baixo” | Média | Mediana | P95 “alto” |
| -----------: | ---------: | -----: | ------: | ---------: |
| 12–19 anos | 54 bpm | 74 bpm | 73 bpm | 93 bpm |
| 20–39 anos | 52 bpm | 69 bpm | 68 bpm | 85 bpm |
| 40–59 anos | 53 bpm | 67 bpm | 66 bpm | 85 bpm |
| 60–79 anos | 49 bpm | 66 bpm | 65 bpm | 85 bpm |
**Leitura prática:** em repouso, pessoas jovens/adultas geralmente ficam na faixa de **~50 a ~90 bpm**, mas atletas podem ter repouso bem mais baixo. A American Heart Association cita 60–100 bpm como faixa normal para a maioria dos adultos e observa que pessoas muito ativas podem chegar a cerca de 40 bpm em repouso. ([www.heart.org][2])
---
# 2. Muay Thai — dados medidos em estudos
| Estudo / contexto | Amostra | Repouso / referência | Durante ou após atividade | Maior valor reportado |
| ----------------------------------------------------------- | ---------------------------------: | --------------------------------------------------------------------------------------: | -----------------------------------------------------------------------------------------------------: | --------------------------------------------------------------------------------------: |
| Muay Thai simulado — 3 rounds | 10 homens, 23,7 ± 1,5 anos | FC máx em teste incremental: **182,9 ± 1,6 bpm**; limiar anaeróbio: **137,5 ± 4,5 bpm** | Round 1: **159,7 ± 13,7 bpm**; round 2: **165,2 ± 16,4 bpm**; round 3: **174,0 ± 10,9 bpm** | Média do 3º round: **174,0 bpm**; FCmáx laboratorial: **182,9 bpm** ([ResearchGate][3]) |
| Muay Thai em competição real | 20 lutadores homens experientes | FC máx em teste: **187,2 bpm**; limiar anaeróbio: **168,4 bpm** | Média durante as lutas: **178,9 ± 0,3 bpm** | Pós-4º round: **182,9 ± 0,6 bpm** ([ResearchGate][4]) |
| Sessão de Muay Thai em adolescentes com sobrepeso/obesidade | 27 adolescentes, 12,85 ± 2,08 anos | Basal total: **93 ± 12 bpm** | Imediatamente após: **130 ± 16 bpm**; 10 min: **109 ± 11**; 20 min: **104 ± 12**; 30 min: **102 ± 10** | Pós-sessão: **130 ± 16 bpm** ([ResearchGate][5]) |
## Interpretação do Muay Thai
Para adultos jovens treinados, os estudos apontam Muay Thai como atividade **muito intensa**, frequentemente acima do limiar anaeróbio e próxima da FC máxima individual. No estudo de competição real, a média de **178,9 bpm** ficou próxima da FC máxima medida em laboratório, **187,2 bpm**, o que equivale a aproximadamente **95,6% da FCmáx** naquela amostra. ([ResearchGate][4])
Um ponto importante: em esportes intermitentes como Muay Thai, a frequência cardíaca pode **superestimar ou não representar perfeitamente** o custo metabólico real, porque golpes, pausas, explosões, adrenalina e metabolismo anaeróbio alteram a relação entre esforço e batimento. ([ResearchGate][3])
---
# 3. Corrida intensa — dados medidos em estudos
| Estudo / contexto | Amostra | Valor medido |
| ------------------------------------------------------------------------------- | ----------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| Teste incremental em esteira até exaustão, corredores recreacionais de maratona | 180 corredores, idade média **43,2 ± 8,5 anos** | FCmáx total: **178,2 ± 10,2 bpm**; homens: **178,9 ± 10,4 bpm**; mulheres: **174,8 ± 8,8 bpm** ([Frontiers][6]) |
| Maratona real com medição contínua | 10 homens, **41,7 ± 7,7 anos** | Após o 5º km, a FC ficou estável entre **88% e 91% da FCmáx**; tabela do estudo reporta média geral de maratona em torno de **157 ± 10 bpm** ([ResearchGate][7]) |
## Interpretação da corrida intensa
Corrida em ritmo alto pode ficar facilmente em **80–90%+ da FCmáx**, dependendo de condicionamento, distância, calor, hidratação, inclinação, descanso e estratégia de ritmo. Em maratona real, um estudo mostrou FC sustentada entre **88% e 91% da FCmáx**, mas os autores alertam que usar apenas batimentos para controlar ritmo pode ser problemático, porque a FC pode se manter alta enquanto velocidade e consumo de oxigênio caem. ([ResearchGate][7])
---
# 4. Tabela estimada por idade para atividade intensa
Esta tabela **não é dado medido diretamente por faixa etária**. É uma consolidação matemática usando:
* Fórmula de Tanaka: **FCmáx = 208 − 0,7 × idade**, derivada de meta-análise com 351 estudos e 18.712 pessoas. ([ScienceDirect][8])
* Zona vigorosa geral: **70–85% da FCmáx**, conforme referência da American Heart Association. ([www.heart.org][2])
* Zona “muito intensa/competitiva”: **90–95% da FCmáx**, coerente com os estudos de Muay Thai e corrida em que atletas ficaram perto de 89–96% da FCmáx. ([ResearchGate][4])
| Idade | FCmáx estimada | Atividade vigorosa 70–85% | Muito intenso 90–95% |
| -----: | -------------: | ------------------------: | -------------------: |
| 15–19* | 196 bpm | 137–167 bpm | 176–186 bpm |
| 20–24 | 193 bpm | 135–164 bpm | 173–183 bpm |
| 25–29 | 189 bpm | 132–161 bpm | 170–180 bpm |
| 30–34 | 186 bpm | 130–158 bpm | 167–176 bpm |
| 35–39 | 182 bpm | 127–155 bpm | 164–173 bpm |
| 40–44 | 179 bpm | 125–152 bpm | 161–170 bpm |
| 45–49 | 175 bpm | 123–149 bpm | 158–166 bpm |
| 50–54 | 172 bpm | 120–146 bpm | 154–163 bpm |
| 55–59 | 168 bpm | 118–143 bpm | 151–160 bpm |
| 60–64 | 165 bpm | 115–140 bpm | 148–156 bpm |
| 65–69 | 161 bpm | 113–137 bpm | 145–153 bpm |
| 70–74 | 158 bpm | 110–134 bpm | 142–150 bpm |
* Para adolescentes, essa fórmula deve ser usada com muito cuidado. Uma revisão sistemática sobre crianças e adolescentes concluiu que fórmulas adultas de FCmáx têm erro relevante nessa população e não devem ser tratadas como precisas para menores de 18 anos.
---
# 5. Consolidação prática
| Situação | Valores mais defensáveis pela literatura |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------: |
| Repouso, população geral 12–79 anos | Média aproximada: **66–74 bpm**, dependendo da idade; P5–P95: cerca de **49–93 bpm** ([Statistics Canada][1]) |
| Repouso em adultos, referência clínica geral | **60–100 bpm** para maioria; atletas podem ficar perto de **40 bpm** ([www.heart.org][2]) |
| Muay Thai adulto treinado, simulado | Aproximadamente **160–174 bpm** por round, subindo conforme o round ([ResearchGate][3]) |
| Muay Thai adulto treinado, competição real | Média próxima de **179 bpm**, com pico pós-round perto de **183 bpm** ([ResearchGate][4]) |
| Muay Thai adolescente, sessão recreativa/terapêutica | Basal **93 ± 12 bpm**; pós-sessão **130 ± 16 bpm** ([ResearchGate][5]) |
| Corrida máxima em teste incremental, corredores recreacionais | FCmáx medida em torno de **178 ± 10 bpm** em média de idade ~43 anos ([Frontiers][6]) |
| Maratona real / ritmo sustentado alto | Aproximadamente **88–91% da FCmáx** após o 5º km no estudo citado ([ResearchGate][7]) |
---
## Conclusão direta
Para uma pessoa jovem adulta, atividades como **Muay Thai intenso** ou **corrida forte** frequentemente colocam a FC em algo como **160–185 bpm**, dependendo da FCmáx individual. Em atletas de Muay Thai treinados, os estudos mostram médias próximas de **174–179 bpm** e picos perto de **183 bpm**. Para corrida forte ou competição, estudos em corredores recreacionais mostram manutenção de **~88–91% da FCmáx**, o que para muitos adultos fica na faixa de **150–180 bpm**, dependendo da idade e da FCmáx real.
Não existe base científica suficiente para afirmar, com precisão, “mínimo, média e máximo real” por faixas como 15–20, 20–25, 25–30 para Muay Thai/corrida intensa. A forma correta é usar **dados medidos dos estudos específicos** + **estimativas por FCmáx** + **medição individual com cinta cardíaca ou teste cardiopulmonar**.
[1]: https://www150.statcan.gc.ca/n1/pub/82-626-x/2012001/t004-eng.htm "Canadian Health Measures Survey: Cycle 2 Data Tables: Table 3 — Average resting heart rate, by age and sex, household
population, Canada, 2009 to 2011"
[2]: https://www.heart.org/en/healthy-living/exercise-and-physical-activity/fitness-basics/target-heart-rates "Target Heart Rates Chart | American Heart Association"
[3]: https://www.researchgate.net/publication/24281067_Physiological_responses_and_energy_cost_during_a_simulation_of_a_Muay_Thai_boxing_match "(PDF) Physiological responses and energy cost during a simulation of a Muay Thai boxing match"
[4]: https://www.researchgate.net/publication/263213413_Physiological_responses_and_match_analysis_of_Muay_Thai_fighting "(PDF) Physiological responses and match analysis of Muay Thai fighting"
[5]: https://www.researchgate.net/publication/358798865_Acute_Effects_of_Muay_Thai_on_Blood_Pressure_and_Heart_Rate_in_Adolescents_with_OverweightObesity "(PDF) Acute Effects of Muay Thai on Blood Pressure and Heart Rate in Adolescents with Overweight/Obesity"
[6]: https://www.frontiersin.org/journals/physiology/articles/10.3389/fphys.2018.00226/full "Frontiers | Age-Predicted Maximal Heart Rate in Recreational Marathon Runners: A Cross-Sectional Study on Fox's and Tanaka's Equations"
[7]: https://www.researchgate.net/publication/364033021_Heart_Rate_Does_Not_Reflect_the_VO2max_in_Recreational_Runners_during_the_Marathon "(PDF) Heart Rate Does Not Reflect the %VO2max in Recreational Runners during the Marathon"
[8]: https://www.sciencedirect.com/science/article/pii/S0735109700010548 "Age-predicted maximal heart rate revisited - ScienceDirect"
@@ -0,0 +1,513 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate — React Native Redesign Directions</title>
<style>
:root {
--ink: #11110f;
--paper: #f5f1e8;
--muted: #706c63;
--line: #d9d0bf;
--red: #d9352a;
--amber: #f0a01f;
--blue: #1d5cff;
--green: #15a06e;
--black: #050505;
--radius-xl: 32px;
--radius-lg: 22px;
--radius-md: 14px;
--shadow: 0 24px 80px rgba(17, 17, 15, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family:
ui-sans-serif,
system-ui,
-apple-system,
Segoe UI,
sans-serif;
line-height: 1.45;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 10% 0%,
rgba(217, 53, 42, 0.12),
transparent 34%
),
radial-gradient(
circle at 90% 20%,
rgba(29, 92, 255, 0.1),
transparent 36%
),
linear-gradient(rgba(17, 17, 15, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(17, 17, 15, 0.035) 1px, transparent 1px);
background-size:
auto,
auto,
42px 42px,
42px 42px;
}
.wrap {
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0 80px;
position: relative;
}
.eyebrow {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
font-weight: 800;
color: var(--red);
}
h1 {
font-family: Georgia, serif;
font-size: clamp(46px, 9vw, 104px);
line-height: 0.88;
letter-spacing: -0.07em;
margin: 18px 0 18px;
max-width: 980px;
}
h2 {
font-size: clamp(28px, 5vw, 56px);
letter-spacing: -0.055em;
line-height: 0.95;
margin: 0 0 14px;
}
p {
color: var(--muted);
font-size: 17px;
max-width: 760px;
}
.hero {
padding: 34px;
border: 1px solid var(--line);
border-radius: var(--radius-xl);
background: rgba(255, 255, 255, 0.46);
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 28px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 10px;
border-radius: 999px;
padding: 13px 18px;
text-decoration: none;
font-weight: 900;
border: 1px solid var(--ink);
color: var(--ink);
background: #fff;
}
.btn.dark {
background: var(--ink);
color: var(--paper);
}
.grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 14px;
margin-top: 28px;
}
.card {
position: relative;
min-height: 420px;
border-radius: 28px;
border: 1px solid var(--line);
overflow: hidden;
background: #fff;
box-shadow: 0 18px 45px rgba(17, 17, 15, 0.08);
}
.card .top {
padding: 18px;
}
.card h3 {
font-size: 25px;
letter-spacing: -0.04em;
line-height: 1;
margin: 6px 0;
}
.tag {
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 900;
color: var(--muted);
}
.phone {
margin: 10px auto 0;
width: 150px;
height: 282px;
border-radius: 30px;
background: #0d0d0d;
padding: 8px;
box-shadow: 0 22px 44px rgba(0, 0, 0, 0.22);
}
.screen {
height: 100%;
border-radius: 24px;
padding: 12px;
overflow: hidden;
}
.metric {
font-size: 44px;
font-weight: 950;
letter-spacing: -0.08em;
}
.small {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.14em;
font-weight: 900;
}
.line {
height: 56px;
border-bottom: 2px solid currentColor;
border-radius: 50%;
transform: skewX(-12deg);
opacity: 0.65;
}
.rail {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 14px;
}
.pill {
border-radius: 999px;
padding: 7px 8px;
font-size: 10px;
font-weight: 900;
text-align: center;
}
.corner .screen {
background: #0c0b09;
color: #f5ead8;
}
.corner .pill {
background: #211e17;
color: #ffbd37;
}
.corner .line {
color: #e23b2e;
}
.lab .screen {
background: #f7fbf8;
color: #12231d;
}
.lab .pill {
background: #e7f4ed;
color: #08724c;
}
.lab .line {
color: #16a06b;
}
.night .screen {
background: #140409;
color: #fff2f2;
}
.night .pill {
background: #35111a;
color: #ff596c;
}
.night .line {
color: #ff2e48;
}
.sensor .screen {
background: #03070a;
color: #d6f7ff;
}
.sensor .pill {
background: #06222c;
color: #5be6ff;
}
.sensor .line {
color: #5be6ff;
}
.minimal .screen {
background: #f7f3eb;
color: #15130f;
}
.minimal .pill {
background: #eee4d3;
color: #15130f;
}
.minimal .line {
color: #15130f;
}
.matrix {
margin-top: 42px;
display: grid;
grid-template-columns: 1.2fr repeat(5, 1fr);
border: 1px solid var(--line);
border-radius: 24px;
overflow: hidden;
background: rgba(255, 255, 255, 0.55);
}
.matrix div {
padding: 14px;
border-bottom: 1px solid var(--line);
border-right: 1px solid var(--line);
font-size: 13px;
}
.matrix div:nth-child(6n) {
border-right: 0;
}
.matrix .head {
font-weight: 950;
background: #fff;
color: var(--ink);
}
.score {
font-weight: 950;
}
.notes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 28px;
}
.note {
background: #fff;
border: 1px solid var(--line);
border-radius: 24px;
padding: 18px;
}
.note b {
display: block;
margin-bottom: 6px;
}
@media (max-width: 1050px) {
.grid {
grid-template-columns: 1fr 1fr;
}
.matrix {
grid-template-columns: 1fr;
}
.matrix div {
border-right: 0;
}
.notes {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.grid {
grid-template-columns: 1fr;
}
.hero {
padding: 22px;
}
.phone {
width: 170px;
height: 320px;
}
}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<div class="eyebrow">FightRate / React Native Redesign</div>
<h1>Antes de reconstruir, escolher a linguagem visual certa.</h1>
<p>
Este pacote propõe cinco direções de design system para um app de
captura contínua de treino, histórico denso, sensor BLE e insights
úteis. Nenhuma direção usa o design antigo como base.
</p>
<div class="actions">
<a class="btn dark" href="01-corner-command.html"
>01 Corner Command</a
>
<a class="btn" href="02-athlete-lab.html">02 Athlete Lab</a>
<a class="btn" href="03-fight-night.html">03 Fight Night</a>
<a class="btn" href="04-sensor-instrument.html"
>04 Sensor Instrument</a
>
<a class="btn" href="05-minimal-performance.html"
>05 Minimal Performance</a
>
</div>
</section>
<section class="grid" aria-label="direções visuais">
<article class="card corner">
<div class="top">
<span class="tag">tático premium</span>
<h3>Corner Command</h3>
<p>Treino real, córner, intensidade, preto/couro/vermelho.</p>
</div>
<div class="phone">
<div class="screen">
<div class="small">captura ativa</div>
<div class="metric">148</div>
<div>bpm médio</div>
<div class="line"></div>
<div class="rail">
<span class="pill">Z4 38%</span
><span class="pill">Pico 181</span>
</div>
</div>
</div>
</article>
<article class="card lab">
<div class="top">
<span class="tag">ciência esportiva</span>
<h3>Athlete Lab</h3>
<p>Limpo, confiável, claro, excelente para dados e relatórios.</p>
</div>
<div class="phone">
<div class="screen">
<div class="small">sessão</div>
<div class="metric">72%</div>
<div>tempo em carga</div>
<div class="line"></div>
<div class="rail">
<span class="pill">TRIMP 96</span
><span class="pill">Rec 18h</span>
</div>
</div>
</div>
</article>
<article class="card night">
<div class="top">
<span class="tag">marca esportiva</span>
<h3>Fight Night</h3>
<p>Editorial, energia de evento, bom para virar marca pública.</p>
</div>
<div class="phone">
<div class="screen">
<div class="small">rounds auto</div>
<div class="metric">6×3</div>
<div>alta intensidade</div>
<div class="line"></div>
<div class="rail">
<span class="pill">Explosão</span
><span class="pill">Ritmo</span>
</div>
</div>
</div>
</article>
<article class="card sensor">
<div class="top">
<span class="tag">instrumentação</span>
<h3>Sensor Instrument</h3>
<p>BLE, telemetria, logs e confiança técnica máxima.</p>
</div>
<div class="phone">
<div class="screen">
<div class="small">h003 conectado</div>
<div class="metric">-61</div>
<div>RSSI estável</div>
<div class="line"></div>
<div class="rail">
<span class="pill">180D</span><span class="pill">2A37</span>
</div>
</div>
</div>
</article>
<article class="card minimal">
<div class="top">
<span class="tag">premium minimal</span>
<h3>Minimal Performance</h3>
<p>Menos interface, mais leitura. Elegante e rápido.</p>
</div>
<div class="phone">
<div class="screen">
<div class="small">hoje</div>
<div class="metric">41m</div>
<div>capturados</div>
<div class="line"></div>
<div class="rail">
<span class="pill">Média 142</span
><span class="pill">Z5 08%</span>
</div>
</div>
</div>
</article>
</section>
<section class="matrix">
<div class="head">Critério</div>
<div class="head">Corner</div>
<div class="head">Lab</div>
<div class="head">Night</div>
<div class="head">Sensor</div>
<div class="head">Minimal</div>
<div>Confiança para atleta</div>
<div class="score">9</div>
<div class="score">9</div>
<div class="score">8</div>
<div class="score">8</div>
<div class="score">9</div>
<div>Potencial de marca</div>
<div class="score">8</div>
<div class="score">7</div>
<div class="score">10</div>
<div class="score">7</div>
<div class="score">8</div>
<div>Clareza de dados</div>
<div class="score">8</div>
<div class="score">10</div>
<div class="score">7</div>
<div class="score">9</div>
<div class="score">9</div>
<div>Energia visual</div>
<div class="score">9</div>
<div class="score">6</div>
<div class="score">10</div>
<div class="score">7</div>
<div class="score">5</div>
<div>Viabilidade React Native</div>
<div class="score">9</div>
<div class="score">10</div>
<div class="score">8</div>
<div class="score">9</div>
<div class="score">10</div>
</section>
<section class="notes">
<div class="note">
<b>Recomendação forte</b
><span
>Escolher entre Corner Command e Athlete Lab. Corner vende melhor a
ideia de combate; Athlete Lab resolve melhor histórico, relatórios e
confiança.</span
>
</div>
<div class="note">
<b>Possível híbrido</b
><span
>Base Athlete Lab com modo treino Corner Command. Assim o app fica
sério no relatório e intenso na captura.</span
>
</div>
<div class="note">
<b>Próximo passo</b
><span
>Escolher 1 direção ou 2 para fundir. Depois criar DESIGN.md final +
tokens React Native + protótipo Expo.</span
>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,732 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate Direction 01 — Corner Command</title>
<style>
:root {
--bg: #090806;
--surface: #12100c;
--surface-2: #1b1710;
--ink: #fff3dd;
--muted: #b7a98e;
--line: #3a3022;
--accent: #e23b2e;
--accent-2: #ffb02e;
--ok: #31c47a;
--blue: #48a7ff;
--r-xs: 8px;
--r-sm: 13px;
--r-md: 18px;
--r-lg: 28px;
--r-xl: 38px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
--fast: 180ms cubic-bezier(0.2, 0.9, 0.2, 1);
--slow: 700ms cubic-bezier(0.19, 1, 0.22, 1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: Bahnschrift, "Arial Narrow", system-ui, sans-serif;
line-height: 1.35;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 20% -10%,
rgba(226, 59, 46, 0.28),
transparent 32%
),
radial-gradient(
circle at 90% 12%,
rgba(255, 176, 46, 0.13),
transparent 28%
),
linear-gradient(
120deg,
rgba(255, 255, 255, 0.055) 0 1px,
transparent 1px 18px
);
opacity: 0.9;
}
.wrap {
width: min(1240px, calc(100% - 28px));
margin: auto;
padding: 34px 0 80px;
position: relative;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--line);
border-radius: 999px;
padding: 10px 12px 10px 18px;
background: rgba(18, 16, 12, 0.84);
backdrop-filter: blur(14px);
position: sticky;
top: 16px;
z-index: 2;
}
.brand {
font-weight: 1000;
letter-spacing: -0.04em;
}
.brand span {
color: var(--accent);
}
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.16em;
margin-left: 18px;
}
.hero {
display: grid;
grid-template-columns: 1.08fr 0.92fr;
gap: 24px;
margin-top: 34px;
}
.panel {
border: 1px solid var(--line);
border-radius: var(--r-xl);
background: linear-gradient(
145deg,
rgba(27, 23, 16, 0.96),
rgba(10, 9, 7, 0.98)
);
box-shadow:
0 32px 90px rgba(0, 0, 0, 0.46),
inset 0 1px rgba(255, 255, 255, 0.06);
position: relative;
overflow: hidden;
}
.panel:after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.055),
transparent
);
transform: translateX(-100%);
animation: scan 5s infinite;
}
@keyframes scan {
60%,
100% {
transform: translateX(100%);
}
}
.pad {
padding: 30px;
}
.eyebrow {
color: var(--accent-2);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.25em;
font-weight: 900;
}
.h1 {
font-size: clamp(54px, 9vw, 118px);
line-height: 0.78;
letter-spacing: -0.09em;
margin: 18px 0;
}
.lead {
font-size: 19px;
color: var(--muted);
max-width: 720px;
}
.cta-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 28px;
}
.btn {
border: 0;
border-radius: 999px;
padding: 14px 18px;
font-weight: 1000;
text-transform: uppercase;
letter-spacing: 0.08em;
background: var(--accent);
color: white;
box-shadow: 0 14px 34px rgba(226, 59, 46, 0.28);
display: inline-flex;
align-items: center;
gap: 12px;
}
.btn.secondary {
background: transparent;
color: var(--ink);
border: 1px solid var(--line);
}
.btn i {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.16);
font-style: normal;
}
.phone-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin-top: 26px;
}
.phone {
height: 560px;
border-radius: 42px;
background: #050403;
padding: 10px;
border: 1px solid #33291c;
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.5);
}
.screen {
height: 100%;
border-radius: 34px;
background: var(--surface);
padding: 18px;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 14px;
}
.topline {
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.mega {
font-size: 82px;
line-height: 0.85;
font-weight: 1000;
letter-spacing: -0.1em;
}
.unit {
font-size: 18px;
color: var(--muted);
letter-spacing: 0;
}
.status {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.chip {
padding: 8px 10px;
border-radius: 999px;
background: #231d14;
color: var(--accent-2);
font-size: 11px;
font-weight: 1000;
text-transform: uppercase;
}
.chart {
height: 118px;
border-radius: 22px;
background: linear-gradient(180deg, #211911, #100d09);
border: 1px solid var(--line);
position: relative;
overflow: hidden;
}
.chart svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--accent);
stroke-width: 4;
stroke-linecap: round;
}
.metric-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.metric {
border: 1px solid var(--line);
border-radius: 18px;
padding: 12px;
background: #16120d;
}
.metric b {
font-size: 22px;
}
.metric span {
display: block;
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.13em;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dock .btn {
justify-content: center;
padding: 14px 10px;
font-size: 11px;
}
.stop {
background: #fff0e4;
color: #100b08;
}
.section {
margin-top: 28px;
}
.section h2 {
font-size: 42px;
letter-spacing: -0.06em;
margin: 0 0 10px;
}
.book {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.spec {
border: 1px solid var(--line);
border-radius: 28px;
padding: 20px;
background: rgba(18, 16, 12, 0.78);
}
.swatches {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
height: 96px;
}
.swatches div {
border-radius: 18px;
padding: 12px;
font-weight: 1000;
}
.type-sample .a {
font-size: 52px;
letter-spacing: -0.09em;
line-height: 0.84;
}
.type-sample .b {
font-size: 18px;
color: var(--muted);
}
.radii {
display: flex;
gap: 10px;
align-items: end;
}
.radii div {
border: 1px solid var(--line);
background: #211a12;
}
.r1 {
width: 54px;
height: 54px;
border-radius: 8px;
}
.r2 {
width: 70px;
height: 70px;
border-radius: 18px;
}
.r3 {
width: 92px;
height: 92px;
border-radius: 32px;
}
.component-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.component {
border: 1px solid var(--line);
border-radius: 24px;
background: #120f0b;
padding: 16px;
min-height: 150px;
}
.field {
border: 1px solid var(--line);
border-radius: 16px;
padding: 12px;
background: #0b0907;
color: var(--muted);
margin-top: 12px;
}
.timeline {
display: grid;
gap: 10px;
}
.event {
display: grid;
grid-template-columns: 54px 1fr;
gap: 10px;
align-items: center;
}
.time {
color: var(--accent-2);
font-weight: 1000;
}
.bar {
height: 12px;
border-radius: 999px;
background: #2b2115;
overflow: hidden;
}
.bar i {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--accent-2), var(--accent));
border-radius: 999px;
}
.rule {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.rule div {
border-radius: 22px;
padding: 16px;
border: 1px solid var(--line);
}
@media (max-width: 1050px) {
.hero,
.book {
grid-template-columns: 1fr;
}
.phone-grid {
grid-template-columns: 1fr;
}
.component-grid {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 620px) {
.nav {
position: relative;
top: 0;
}
.hero {
margin-top: 18px;
}
.pad {
padding: 22px;
}
.phone {
height: 640px;
}
.component-grid,
.rule {
grid-template-columns: 1fr;
}
.h1 {
font-size: 64px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">Fight<span>Rate</span> / Corner Command</div>
<div>
<a href="00-index.html">Índice</a><a href="#book">Design book</a
><a href="#screens">Telas</a>
</div>
</nav>
<section class="hero">
<div class="panel pad">
<div class="eyebrow">Direção 01 / tático premium</div>
<div class="h1">mesa de córner para treino real.</div>
<p class="lead">
Visual de equipamento sério: preto fosco, couro queimado, vermelho
de corte e âmbar de sinal. O app parece uma ferramenta que fica no
canto do tatame/ringue, não uma dashboard genérica.
</p>
<div class="cta-row">
<button class="btn">Iniciar treino <i>↗</i></button
><button class="btn secondary">Ver histórico</button>
</div>
</div>
<div class="panel pad">
<div class="eyebrow">Regra 60 / 30 / 10</div>
<div class="swatches">
<div style="background: #090806; color: #fff3dd">
60% base preta
</div>
<div style="background: #1b1710; color: #fff3dd">
30% superfície couro
</div>
<div style="background: #e23b2e; color: white">10% impacto</div>
</div>
<p class="lead">
O âmbar aparece só como estado e leitura auxiliar. O vermelho é ação
crítica e intensidade.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Exemplos de telas mobile</h2>
<p class="lead">
Seis telas para validar o padrão: início, captura ativa, fim de
treino, histórico, sensor e insight.
</p>
<div class="phone-grid">
<div class="phone">
<div class="screen">
<div class="topline">
<span>pré-treino</span><span>sensor ok</span>
</div>
<div class="mega">00<span class="unit">bpm</span></div>
<p class="lead">
Coloque a cinta, inicie a captura e deixe o celular parado.
</p>
<div class="status">
<span class="chip">H003 pronto</span
><span class="chip">bateria 84%</span>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">check-in</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="topline">
<span>captura ativa</span><span>38:12</span>
</div>
<div class="mega">148<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 260 120">
<path
d="M8 90 C38 80,40 48,70 54 S112 95,140 66 S180 28,204 44 S232 76,252 42"
/>
</svg>
</div>
<div class="metric-row">
<div class="metric"><b>181</b><span>pico</span></div>
<div class="metric"><b>142</b><span>média</span></div>
<div class="metric"><b>Z4</b><span>zona</span></div>
</div>
<div class="dock">
<button class="btn stop">Parar treino</button
><button class="btn secondary">marcar evento</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="topline"><span>resumo</span><span>salvo</span></div>
<div class="mega">47<span class="unit">min</span></div>
<div class="status">
<span class="chip">carga alta</span
><span class="chip">recuperar 18h</span>
</div>
<div class="timeline">
<div class="event">
<span class="time">0-10</span>
<div class="bar"><i style="width: 42%"></i></div>
</div>
<div class="event">
<span class="time">10-30</span>
<div class="bar"><i style="width: 86%"></i></div>
</div>
<div class="event">
<span class="time">30-47</span>
<div class="bar"><i style="width: 64%"></i></div>
</div>
</div>
<div class="dock">
<button class="btn">Relatório</button
><button class="btn secondary">exportar</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="topline">
<span>histórico</span><span>junho</span>
</div>
<div class="metric-row">
<div class="metric"><b>8</b><span>treinos</span></div>
<div class="metric"><b>5h</b><span>tempo</span></div>
<div class="metric"><b>72%</b><span>carga</span></div>
</div>
<div class="timeline">
<div class="event">
<span class="time">Hoje</span>
<div>
<b>Sparring</b><br /><span style="color: var(--muted)"
>47 min · pico 181</span
>
</div>
</div>
<div class="event">
<span class="time">Ontem</span>
<div>
<b>Drill</b><br /><span style="color: var(--muted)"
>32 min · base técnica</span
>
</div>
</div>
<div class="event">
<span class="time">Seg</span>
<div>
<b>Muay Thai</b><br /><span style="color: var(--muted)"
>61 min · alta carga</span
>
</div>
</div>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="topline"><span>sensor</span><span>ble</span></div>
<div class="mega">-61<span class="unit">rssi</span></div>
<div class="component">
<b>Heart Rate Service</b>
<div class="field">180D / 2A37 / notify</div>
<div class="field">Battery 180F / read</div>
<div class="field">Body Location 2A38</div>
</div>
<div class="dock">
<button class="btn secondary">diagnóstico</button
><button class="btn secondary">reconectar</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="topline"><span>insight</span><span>útil</span></div>
<div class="mega">Z4<span class="unit">38%</span></div>
<p class="lead">
Você sustentou intensidade de luta por tempo relevante. Bom
sinal para condicionamento específico.
</p>
<div class="component">
<b>Por que importa?</b>
<p style="color: var(--muted); margin-bottom: 0">
Ajuda a medir capacidade de manter pressão sem depender de
sensação subjetiva.
</p>
</div>
</div>
</div>
</div>
</section>
<section id="book" class="section">
<h2>Design book</h2>
<div class="book">
<div class="spec">
<div class="eyebrow">Cores</div>
<div class="rule">
<div style="background: #090806">Base #090806<br />60%</div>
<div style="background: #1b1710">Surface #1B1710<br />30%</div>
<div style="background: #e23b2e">Accent #E23B2E<br />10%</div>
</div>
</div>
<div class="spec type-sample">
<div class="eyebrow">Tipografia</div>
<div class="a">148 BPM</div>
<div class="b">
Condensed grotesk para números. Corpo com espaçamento curto, tom
técnico e direto.
</div>
</div>
<div class="spec">
<div class="eyebrow">Raio e borda</div>
<div class="radii">
<div class="r1"></div>
<div class="r2"></div>
<div class="r3"></div>
</div>
<p>
Raio grande nas telas e painéis; raio menor só em campos técnicos.
Bordas cor de metal queimado, nunca cinza genérico.
</p>
</div>
<div class="spec">
<div class="eyebrow">Botões</div>
<button class="btn">Primário <i>↗</i></button
><button class="btn secondary" style="margin-left: 8px">
Secundário
</button>
<p>
Primário só para ação decisiva. Parar treino usa fundo claro para
contraste emergencial.
</p>
</div>
</div>
</section>
<section class="section">
<h2>Componentes-base</h2>
<div class="component-grid">
<div class="component">
<div class="eyebrow">Status band</div>
<span class="chip">captura ativa</span
><span class="chip">sensor ok</span>
<div class="field">Usado no topo da captura.</div>
</div>
<div class="component">
<div class="eyebrow">Metric block</div>
<div class="mega" style="font-size: 54px">181</div>
<span class="chip">pico</span>
</div>
<div class="component">
<div class="eyebrow">Input</div>
<div class="field">Nome do treino</div>
<div class="field">RPE 0-10</div>
</div>
<div class="component">
<div class="eyebrow">Alert</div>
<b style="color: var(--accent-2)">Zona alta sustentada</b>
<p style="color: var(--muted)">
Use vibração curta, sem interromper o treino.
</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,652 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate Direction 02 — Athlete Lab</title>
<style>
:root {
--bg: #f4f8f4;
--paper: #fcfffc;
--ink: #102019;
--muted: #607168;
--line: #d7e4dc;
--green: #12a36b;
--lime: #c6ee3f;
--blue: #1769ff;
--warn: #ef8b18;
--r-sm: 10px;
--r-md: 16px;
--r-lg: 24px;
--r-xl: 34px;
--shadow: 0 24px 80px rgba(16, 32, 25, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Aptos", ui-sans-serif, system-ui, sans-serif;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
linear-gradient(rgba(16, 32, 25, 0.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(16, 32, 25, 0.045) 1px, transparent 1px),
radial-gradient(
circle at 85% 4%,
rgba(198, 238, 63, 0.28),
transparent 30%
);
background-size:
28px 28px,
28px 28px,
auto;
}
.wrap {
width: min(1220px, calc(100% - 28px));
margin: auto;
padding: 34px 0 80px;
position: relative;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(252, 255, 252, 0.82);
border: 1px solid var(--line);
border-radius: 22px;
padding: 12px 16px;
position: sticky;
top: 14px;
z-index: 3;
backdrop-filter: blur(16px);
}
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.14em;
margin-left: 16px;
}
.brand {
font-weight: 900;
letter-spacing: -0.04em;
}
.hero {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 18px;
margin-top: 24px;
}
.hero-card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-xl);
padding: 32px;
box-shadow: var(--shadow);
}
.eyebrow {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
font-weight: 900;
color: var(--green);
}
h1 {
font-size: clamp(50px, 8vw, 104px);
line-height: 0.84;
letter-spacing: -0.08em;
margin: 16px 0;
}
h2 {
font-size: 42px;
letter-spacing: -0.06em;
margin: 0 0 10px;
}
.lead {
font-size: 18px;
color: var(--muted);
line-height: 1.45;
}
.token-row {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
margin-top: 20px;
}
.token {
border-radius: 18px;
padding: 16px;
min-height: 98px;
font-weight: 900;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
border: 0;
border-radius: 16px;
background: var(--green);
color: white;
padding: 14px 18px;
font-weight: 900;
box-shadow: 0 16px 36px rgba(18, 163, 107, 0.22);
}
.btn.secondary {
background: #edf6f0;
color: var(--ink);
border: 1px solid var(--line);
box-shadow: none;
}
.btn.ghost {
background: transparent;
color: var(--green);
border: 1px solid var(--green);
box-shadow: none;
}
.section {
margin-top: 30px;
}
.lab-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.phone {
height: 590px;
background: #111;
border-radius: 42px;
padding: 10px;
box-shadow: 0 30px 80px rgba(16, 32, 25, 0.18);
}
.screen {
height: 100%;
background: var(--paper);
border-radius: 34px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
overflow: hidden;
}
.top {
display: flex;
justify-content: space-between;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
font-weight: 900;
}
.number {
font-size: 76px;
letter-spacing: -0.09em;
line-height: 0.86;
font-weight: 950;
}
.unit {
font-size: 18px;
color: var(--muted);
}
.strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.tile {
border: 1px solid var(--line);
border-radius: 18px;
background: #f7fbf8;
padding: 12px;
}
.tile b {
font-size: 24px;
}
.tile span {
display: block;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
font-weight: 900;
}
.chart {
height: 130px;
border: 1px solid var(--line);
border-radius: 22px;
background: linear-gradient(180deg, #fafffa, #edf7f0);
position: relative;
overflow: hidden;
}
.chart svg {
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--green);
stroke-width: 4;
stroke-linecap: round;
}
.zones {
display: flex;
height: 14px;
border-radius: 999px;
overflow: hidden;
}
.zones i:nth-child(1) {
background: #9bd8ff;
flex: 1;
}
.zones i:nth-child(2) {
background: #b7ecb4;
flex: 1.4;
}
.zones i:nth-child(3) {
background: #c6ee3f;
flex: 1.8;
}
.zones i:nth-child(4) {
background: #efb84d;
flex: 1.2;
}
.zones i:nth-child(5) {
background: #e75b4a;
flex: 0.7;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dock .btn {
padding: 14px 8px;
}
.spec-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.spec {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 24px;
padding: 18px;
}
.spec h3 {
margin: 6px 0;
font-size: 22px;
letter-spacing: -0.04em;
}
.type .big {
font-size: 62px;
letter-spacing: -0.08em;
line-height: 0.86;
}
.radii {
display: flex;
gap: 10px;
align-items: end;
}
.radii div {
background: #eaf3ed;
border: 1px solid var(--line);
}
.r1 {
width: 48px;
height: 48px;
border-radius: 10px;
}
.r2 {
width: 68px;
height: 68px;
border-radius: 16px;
}
.r3 {
width: 92px;
height: 92px;
border-radius: 28px;
}
.component-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.component {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 22px;
padding: 14px;
min-height: 154px;
}
.field {
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px;
background: #f7fbf8;
color: var(--muted);
margin-top: 10px;
}
.table {
display: grid;
gap: 8px;
}
.row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
border-bottom: 1px solid var(--line);
padding: 8px 0;
}
.badge {
display: inline-flex;
border-radius: 999px;
padding: 6px 9px;
background: #e7f6ee;
color: #08734c;
font-size: 11px;
font-weight: 900;
text-transform: uppercase;
}
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.principle {
border-left: 5px solid var(--green);
background: var(--paper);
border-radius: 0 22px 22px 0;
padding: 18px;
box-shadow: var(--shadow);
}
@media (max-width: 1000px) {
.hero,
.lab-grid {
grid-template-columns: 1fr;
}
.spec-grid {
grid-template-columns: 1fr 1fr;
}
.component-grid {
grid-template-columns: 1fr 1fr;
}
.principles {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.hero-card {
padding: 22px;
}
.spec-grid,
.component-grid {
grid-template-columns: 1fr;
}
.phone {
height: 650px;
}
.nav {
position: relative;
top: 0;
}
.nav div:last-child {
display: none;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">FightRate / Athlete Lab</div>
<div>
<a href="00-index.html">Índice</a><a href="#screens">Telas</a
><a href="#system">Sistema</a>
</div>
</nav>
<section class="hero">
<div class="hero-card">
<div class="eyebrow">Direção 02 / ciência esportiva</div>
<h1>laboratório limpo para performance real.</h1>
<p class="lead">
A direção mais confiável para relatórios, histórico, sensores e
decisões pós-treino. Visual claro, médico-esportivo sem parecer
hospital.
</p>
<button class="btn">Capturar treino</button>
<button class="btn secondary">Ver relatório</button>
</div>
<div class="hero-card">
<div class="eyebrow">Regra 60 / 30 / 10</div>
<div class="token-row">
<div class="token" style="background: #fcfffc">60% paper</div>
<div class="token" style="background: #eaf3ed">30% lab surface</div>
<div class="token" style="background: #12a36b; color: white">
10% green
</div>
</div>
<p class="lead">
Azul só para informação técnica; amarelo para alerta leve; vermelho
apenas para risco/limite.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Telas mobile</h2>
<div class="lab-grid">
<div class="phone">
<div class="screen">
<div class="top">
<span>pronto para captura</span><span>h003</span>
</div>
<div class="number">--<span class="unit">bpm</span></div>
<p class="lead">
Comece antes do treino. O relatório será construído pela janela
inteira.
</p>
<div class="strip">
<div class="tile"><b>84%</b><span>bateria</span></div>
<div class="tile"><b>-58</b><span>rssi</span></div>
<div class="tile"><b>0</b><span>amostras</span></div>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">check-in</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>captura ativa</span><span>42:18</span>
</div>
<div class="number">154<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 280 130">
<path
d="M8 92 C44 88,54 58,78 64 S118 96,146 76 S196 32,226 48 S250 84,272 58"
/>
</svg>
</div>
<div class="zones"><i></i><i></i><i></i><i></i><i></i></div>
<div class="strip">
<div class="tile"><b>146</b><span>média</span></div>
<div class="tile"><b>183</b><span>pico</span></div>
<div class="tile"><b>41%</b><span>Z4/Z5</span></div>
</div>
<div class="dock">
<button class="btn secondary">Evento</button
><button class="btn" style="background: #e75b4a">Parar</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>relatório</span><span>salvo</span></div>
<div class="number">96<span class="unit">trimp</span></div>
<div class="table">
<div class="row"><span>Tempo em carga</span><b>28m</b></div>
<div class="row"><span>Pico sustentado</span><b>181</b></div>
<div class="row"><span>Recuperação</span><b>18h</b></div>
<div class="row"><span>Consistência</span><b>Boa</b></div>
</div>
<span class="badge">carga alta controlada</span>
<div class="dock">
<button class="btn">Detalhar</button
><button class="btn secondary">Exportar</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>histórico</span><span>4 semanas</span>
</div>
<div class="strip">
<div class="tile"><b>11</b><span>treinos</span></div>
<div class="tile"><b>7h</b><span>total</span></div>
<div class="tile"><b>+8%</b><span>carga</span></div>
</div>
<div class="table">
<div class="row"><span>Sparring — hoje</span><b>96</b></div>
<div class="row"><span>Drills — ontem</span><b>54</b></div>
<div class="row"><span>Muay Thai — seg</span><b>110</b></div>
<div class="row"><span>Boxe — sex</span><b>77</b></div>
</div>
<button class="btn secondary">Comparar semanas</button>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>sensor lab</span><span>ble</span></div>
<div class="number">2A37</div>
<div class="table">
<div class="row"><span>Heart Rate</span><b>notify</b></div>
<div class="row"><span>Battery</span><b>read</b></div>
<div class="row"><span>Body Sensor</span><b>read</b></div>
<div class="row"><span>RSSI</span><b>-61</b></div>
</div>
<span class="badge">perfil estável</span>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>insights</span><span>explicável</span>
</div>
<div class="number">18h<span class="unit">rec</span></div>
<p class="lead">
Carga cardiovascular alta. Priorize sono e treino técnico leve
na próxima sessão.
</p>
<div class="tile">
<b>Por que importa?</b
><span>Evita acumular fadiga sem perder evolução.</span>
</div>
<div class="tile">
<b>Confiança</b><span>Baseada em 2.844 amostras.</span>
</div>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design system</h2>
<div class="spec-grid">
<div class="spec">
<div class="eyebrow">Cores</div>
<h3>Verde = performance</h3>
<p class="lead">
Nunca usar verde em tudo. Ele guia progresso, estabilidade e ação
primária.
</p>
</div>
<div class="spec type">
<div class="eyebrow">Tipo</div>
<div class="big">72%</div>
<p class="lead">
Números gigantes, labels técnicos pequenos, corpo legível.
</p>
</div>
<div class="spec">
<div class="eyebrow">Raios</div>
<div class="radii">
<div class="r1"></div>
<div class="r2"></div>
<div class="r3"></div>
</div>
<p class="lead">Raios médios. Menos agressivo que Corner.</p>
</div>
<div class="spec">
<div class="eyebrow">Bordas</div>
<p class="lead">
Borda suave verde/cinza; elevação quase invisível. Parece
instrumento científico, não card de rede social.
</p>
</div>
</div>
</section>
<section class="section">
<h2>Componentes</h2>
<div class="component-grid">
<div class="component">
<div class="eyebrow">Primary CTA</div>
<button class="btn">Iniciar captura</button
><button class="btn secondary" style="margin-top: 8px">
Secundário
</button>
</div>
<div class="component">
<div class="eyebrow">Form</div>
<div class="field">Tipo de treino</div>
<div class="field">RPE pós-treino</div>
</div>
<div class="component">
<div class="eyebrow">Metric</div>
<div class="number" style="font-size: 54px">146</div>
<span class="badge">bpm médio</span>
</div>
<div class="component">
<div class="eyebrow">Insight</div>
<b>Carga subiu 8%</b>
<p class="lead">Comparado à semana anterior.</p>
</div>
<div class="component">
<div class="eyebrow">Sensor row</div>
<div class="row"><span>2A37</span><b>notify</b></div>
<div class="row"><span>2A19</span><b>read</b></div>
</div>
</div>
</section>
<section class="section">
<h2>Princípios</h2>
<div class="principles">
<div class="principle">
<b>Clareza antes de estilo</b>
<p class="lead">Durante treino, o atleta entende em 1 segundo.</p>
</div>
<div class="principle">
<b>Densidade no histórico</b>
<p class="lead">
Depois do treino, a tela pode ser mais informativa.
</p>
</div>
<div class="principle">
<b>Explicável sempre</b>
<p class="lead">Toda métrica importante diz por que importa.</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,695 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate Direction 03 — Fight Night</title>
<style>
:root {
--bg: #120209;
--ink: #fff4f6;
--muted: #be9aa4;
--panel: #210712;
--panel2: #310b19;
--line: #5b1c2d;
--pink: #ff3156;
--gold: #ffc14a;
--violet: #6c4dff;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 20px;
--r-xl: 32px;
--shadow: 0 34px 90px rgba(0, 0, 0, 0.48);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: Impact, "Arial Black", system-ui, sans-serif;
letter-spacing: -0.02em;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 18% 10%,
rgba(255, 49, 86, 0.36),
transparent 28%
),
radial-gradient(
circle at 80% 8%,
rgba(108, 77, 255, 0.28),
transparent 30%
),
linear-gradient(
135deg,
rgba(255, 255, 255, 0.05) 0 1px,
transparent 1px 22px
);
}
.wrap {
width: min(1240px, calc(100% - 28px));
margin: auto;
padding: 34px 0 90px;
position: relative;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--line);
background: rgba(18, 2, 9, 0.72);
border-radius: 999px;
padding: 10px 16px;
backdrop-filter: blur(14px);
position: sticky;
top: 14px;
z-index: 4;
}
.nav a {
color: var(--muted);
text-decoration: none;
margin-left: 16px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.18em;
}
.brand {
font-size: 22px;
}
.hero {
margin-top: 26px;
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 18px;
}
.poster {
background: linear-gradient(145deg, #270814, #120209);
border: 1px solid var(--line);
border-radius: var(--r-xl);
padding: 34px;
box-shadow: var(--shadow);
overflow: hidden;
position: relative;
}
.poster:before {
content: "FIGHTRATE";
position: absolute;
font-size: 170px;
line-height: 0.7;
letter-spacing: -0.12em;
color: rgba(255, 255, 255, 0.035);
left: -18px;
top: 18px;
}
.eyebrow {
position: relative;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.24em;
color: var(--gold);
font-weight: 900;
}
h1 {
position: relative;
font-size: clamp(62px, 12vw, 150px);
line-height: 0.72;
letter-spacing: -0.08em;
margin: 18px 0;
text-transform: uppercase;
}
.lead {
position: relative;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 18px;
line-height: 1.45;
color: var(--muted);
max-width: 720px;
letter-spacing: 0;
}
.btn {
border: 0;
border-radius: 10px;
background: var(--pink);
color: white;
font-weight: 1000;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 15px 18px;
box-shadow: 0 20px 46px rgba(255, 49, 86, 0.32);
}
.btn.secondary {
background: transparent;
border: 1px solid var(--line);
color: var(--ink);
box-shadow: none;
}
.tokens {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
margin-top: 24px;
}
.token {
border-radius: 12px;
padding: 16px;
min-height: 112px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 900;
}
.section {
margin-top: 30px;
}
h2 {
font-size: 52px;
line-height: 0.82;
letter-spacing: -0.07em;
margin: 0 0 14px;
text-transform: uppercase;
}
.screen-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.phone {
height: 590px;
background: #050104;
border-radius: 40px;
padding: 10px;
border: 1px solid #681b30;
box-shadow: var(--shadow);
}
.screen {
height: 100%;
border-radius: 30px;
background: linear-gradient(180deg, #240713, #13030a);
padding: 18px;
display: flex;
flex-direction: column;
gap: 13px;
overflow: hidden;
}
.top {
font-family: ui-sans-serif, system-ui, sans-serif;
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.score {
font-size: 84px;
line-height: 0.74;
letter-spacing: -0.08em;
}
.unit {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 16px;
color: var(--muted);
letter-spacing: 0;
}
.belt {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.belt span {
font-family: ui-sans-serif, system-ui, sans-serif;
background: #3b0d1d;
color: #ffb8c3;
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 9px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 900;
}
.chart {
height: 125px;
background: #16030b;
border: 1px solid var(--line);
border-radius: 18px;
position: relative;
overflow: hidden;
}
.chart:before {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
90deg,
transparent 0 18px,
rgba(255, 255, 255, 0.04) 18px 19px
);
}
.chart svg {
position: relative;
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--pink);
stroke-width: 5;
stroke-linecap: round;
filter: drop-shadow(0 0 12px rgba(255, 49, 86, 0.9));
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.stat {
border: 1px solid var(--line);
background: #1b0610;
border-radius: 14px;
padding: 11px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stat b {
display: block;
font-size: 22px;
}
.stat span {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.12em;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.dock .btn {
padding: 13px 6px;
font-size: 11px;
}
.system {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.spec {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 22px;
padding: 18px;
}
.spec h3 {
font-size: 28px;
line-height: 0.9;
margin: 8px 0;
text-transform: uppercase;
}
.type-big {
font-size: 68px;
line-height: 0.75;
}
.radii {
display: flex;
gap: 8px;
align-items: end;
}
.radii div {
background: #360b1a;
border: 1px solid var(--line);
}
.r1 {
width: 48px;
height: 48px;
border-radius: 6px;
}
.r2 {
width: 68px;
height: 68px;
border-radius: 12px;
}
.r3 {
width: 92px;
height: 92px;
border-radius: 20px;
}
.components {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.component {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 20px;
padding: 14px;
min-height: 156px;
}
.field {
font-family: ui-sans-serif, system-ui, sans-serif;
background: #13030a;
border: 1px solid var(--line);
border-radius: 12px;
padding: 12px;
color: var(--muted);
margin-top: 10px;
}
.fight-card {
display: grid;
grid-template-columns: 52px 1fr auto;
gap: 10px;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 10px 0;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.round {
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--pink);
font-weight: 1000;
}
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.principle {
background: linear-gradient(145deg, #210712, #120209);
border: 1px solid var(--line);
border-radius: 24px;
padding: 22px;
}
.principle b {
font-size: 24px;
text-transform: uppercase;
}
@media (max-width: 1000px) {
.hero,
.screen-grid {
grid-template-columns: 1fr;
}
.system {
grid-template-columns: 1fr 1fr;
}
.components {
grid-template-columns: 1fr 1fr;
}
.principles {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.nav {
position: relative;
top: 0;
}
.nav div:last-child {
display: none;
}
.poster {
padding: 22px;
}
.system,
.components {
grid-template-columns: 1fr;
}
.phone {
height: 650px;
}
h1 {
font-size: 70px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">FIGHTRATE / FIGHT NIGHT</div>
<div>
<a href="00-index.html">Índice</a><a href="#screens">Telas</a
><a href="#system">Sistema</a>
</div>
</nav>
<section class="hero">
<div class="poster">
<div class="eyebrow">Direção 03 / esportivo editorial</div>
<h1>energia de evento para virar marca.</h1>
<p class="lead">
A direção mais agressiva visualmente. Boa para marketing,
comunidade, social sharing e identidade memorável. Menos clínica;
mais luta, pressão, noite, placar e pôster.
</p>
<button class="btn">Começar captura</button>
<button class="btn secondary">Ver card da noite</button>
</div>
<div class="poster">
<div class="eyebrow">Regra 60 / 30 / 10</div>
<div class="tokens">
<div class="token" style="background: #120209">
60% arena escura
</div>
<div class="token" style="background: #310b19">
30% vinho profundo
</div>
<div class="token" style="background: #ff3156">10% neon golpe</div>
</div>
<p class="lead">
Dourado é prêmio, recorde e destaque editorial. Roxo só para
profundidade.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Telas mobile</h2>
<div class="screen-grid">
<div class="phone">
<div class="screen">
<div class="top"><span>fight night</span><span>h003</span></div>
<div class="score">READY</div>
<p class="lead">
Capture o treino como se fosse seu fight card pessoal.
</p>
<div class="belt">
<span>cinta ok</span><span>pré-treino</span
><span>sem round manual</span>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">perfil</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>ao vivo</span><span>37:54</span></div>
<div class="score">151<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 280 130">
<path
d="M8 98 C34 88,38 44,70 58 S110 104,140 72 S190 20,220 48 S250 92,272 48"
/>
</svg>
</div>
<div class="stats">
<div class="stat"><b>185</b><span>pico</span></div>
<div class="stat"><b>41%</b><span>z4/z5</span></div>
<div class="stat"><b>7</b><span>eventos</span></div>
</div>
<div class="dock">
<button class="btn secondary">Marcar</button
><button class="btn">Parar</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>resultado</span><span>salvo</span></div>
<div class="score">W</div>
<div class="belt">
<span>carga alta</span><span>pico 185</span><span>48 min</span>
</div>
<div class="fight-card">
<span class="round">1</span>
<div>Entrou em ritmo</div>
<b>138</b>
</div>
<div class="fight-card">
<span class="round">2</span>
<div>Pico do treino</div>
<b>181</b>
</div>
<div class="fight-card">
<span class="round">3</span>
<div>Queda controlada</div>
<b>152</b>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>histórico</span><span>cards</span></div>
<div class="fight-card">
<span class="round">S</span>
<div>
<b>Sparring</b><br /><span style="color: var(--muted)"
>Hoje · 48 min</span
>
</div>
<b>96</b>
</div>
<div class="fight-card">
<span class="round">M</span>
<div>
<b>Muay Thai</b><br /><span style="color: var(--muted)"
>Seg · 61 min</span
>
</div>
<b>110</b>
</div>
<div class="fight-card">
<span class="round">B</span>
<div>
<b>Boxe</b><br /><span style="color: var(--muted)"
>Sex · 42 min</span
>
</div>
<b>77</b>
</div>
<button class="btn secondary">Ranking pessoal</button>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>sensor</span><span>corner tech</span></div>
<div class="score">BLE</div>
<div class="belt">
<span>180D</span><span>2A37</span><span>-61 RSSI</span>
</div>
<div class="field">Heart Rate Measurement — notify</div>
<div class="field">Battery Level — read</div>
<div class="field">Body Sensor Location — read</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>insight</span><span>headline</span></div>
<div class="score">PRESSÃO</div>
<p class="lead">
Você sustentou alta intensidade por mais tempo que a média das
últimas sessões.
</p>
<div class="belt"><span>+12%</span><span>boa evolução</span></div>
<div class="field">
Use amanhã para treino técnico, não guerra.
</div>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design book</h2>
<div class="system">
<div class="spec">
<div class="eyebrow">Cores</div>
<h3>neon controlado</h3>
<p class="lead">
Rosa só para ação, pico e vitória. Se tudo brilha, nada importa.
</p>
</div>
<div class="spec">
<div class="eyebrow">Tipo</div>
<div class="type-big">185</div>
<p class="lead">
Display condensado, letras de pôster, corpo neutro só nos textos
longos.
</p>
</div>
<div class="spec">
<div class="eyebrow">Raio</div>
<div class="radii">
<div class="r1"></div>
<div class="r2"></div>
<div class="r3"></div>
</div>
<p class="lead">
Raios menores, mais angular, sensação de esporte e placar.
</p>
</div>
<div class="spec">
<div class="eyebrow">Bordas</div>
<p class="lead">
Bordas vinho/neon, contraste alto, nada de cinza corporativo.
</p>
</div>
</div>
</section>
<section class="section">
<h2>Componentes</h2>
<div class="components">
<div class="component">
<div class="eyebrow">CTA</div>
<button class="btn">Iniciar</button
><button class="btn secondary" style="margin-top: 8px">
Secundário
</button>
</div>
<div class="component">
<div class="eyebrow">Fight card row</div>
<div class="fight-card">
<span class="round">S</span>
<div>Sparring</div>
<b>96</b>
</div>
</div>
<div class="component">
<div class="eyebrow">Badge</div>
<div class="belt"><span>recorde</span><span>z5 08%</span></div>
</div>
<div class="component">
<div class="eyebrow">Input</div>
<div class="field">Nome do treino</div>
<div class="field">Categoria</div>
</div>
<div class="component">
<div class="eyebrow">Share</div>
<p class="lead">
Este padrão é o melhor para gerar imagem compartilhável
pós-treino.
</p>
</div>
</div>
</section>
<section class="section">
<h2>Princípios</h2>
<div class="principles">
<div class="principle">
<b>Memorável</b>
<p class="lead">O usuário deve reconhecer FightRate de longe.</p>
</div>
<div class="principle">
<b>Editorial</b>
<p class="lead">Relatórios viram cards visuais de treino.</p>
</div>
<div class="principle">
<b>Com risco</b>
<p class="lead">Pode cansar visualmente se usado em todo app.</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,642 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate Direction 04 — Sensor Instrument</title>
<style>
:root {
--bg: #02070a;
--ink: #dffaff;
--muted: #7aa6b1;
--panel: #061118;
--panel2: #0a1c25;
--line: #14313c;
--cyan: #5be6ff;
--blue: #2d8cff;
--green: #35ef9f;
--red: #ff4c5d;
--amber: #ffd166;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--r-xl: 26px;
--mono: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
--sans: ui-sans-serif, system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
linear-gradient(rgba(91, 230, 255, 0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(91, 230, 255, 0.07) 1px, transparent 1px),
radial-gradient(
circle at 50% 0%,
rgba(91, 230, 255, 0.18),
transparent 36%
);
background-size:
22px 22px,
22px 22px,
auto;
}
.wrap {
width: min(1240px, calc(100% - 28px));
margin: auto;
padding: 34px 0 86px;
position: relative;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(6, 17, 24, 0.82);
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
font-family: var(--mono);
position: sticky;
top: 14px;
z-index: 3;
backdrop-filter: blur(14px);
}
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 12px;
margin-left: 14px;
}
.brand {
color: var(--cyan);
}
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-top: 24px;
}
.terminal {
background: linear-gradient(180deg, #061118, #03090d);
border: 1px solid var(--line);
border-radius: var(--r-xl);
padding: 26px;
box-shadow:
0 28px 80px rgba(0, 0, 0, 0.5),
inset 0 1px rgba(255, 255, 255, 0.05);
}
.eyebrow {
font-family: var(--mono);
color: var(--cyan);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.16em;
}
h1 {
font-family: var(--mono);
font-size: clamp(38px, 7vw, 82px);
line-height: 0.9;
letter-spacing: -0.05em;
margin: 16px 0;
}
h2 {
font-family: var(--mono);
font-size: 36px;
letter-spacing: -0.04em;
margin: 0 0 12px;
}
.lead {
color: var(--muted);
line-height: 1.5;
font-size: 17px;
}
.cmd {
font-family: var(--mono);
background: #02070a;
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px;
color: var(--green);
margin-top: 16px;
}
.tokens {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
}
.token {
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px;
min-height: 98px;
font-family: var(--mono);
font-weight: 800;
}
.section {
margin-top: 30px;
}
.screen-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.phone {
height: 590px;
background: #000;
border-radius: 38px;
padding: 9px;
border: 1px solid #16404d;
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.58);
}
.screen {
height: 100%;
border-radius: 30px;
background: #041017;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
border: 1px solid #092532;
}
.top {
font-family: var(--mono);
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 10px;
}
.big {
font-family: var(--mono);
font-size: 64px;
line-height: 0.9;
letter-spacing: -0.06em;
color: var(--cyan);
}
.unit {
font-size: 16px;
color: var(--muted);
}
.scope {
height: 120px;
border: 1px solid var(--line);
border-radius: 12px;
background: #02070a;
position: relative;
overflow: hidden;
}
.scope:before {
content: "";
position: absolute;
inset: 0;
background:
repeating-linear-gradient(
0deg,
transparent 0 19px,
rgba(91, 230, 255, 0.06) 19px 20px
),
repeating-linear-gradient(
90deg,
transparent 0 19px,
rgba(91, 230, 255, 0.06) 19px 20px
);
}
.scope svg {
position: relative;
width: 100%;
height: 100%;
}
.scope path {
fill: none;
stroke: var(--cyan);
stroke-width: 3;
filter: drop-shadow(0 0 8px var(--cyan));
}
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.module {
border: 1px solid var(--line);
border-radius: 12px;
background: #061822;
padding: 10px;
font-family: var(--mono);
}
.module b {
color: var(--ink);
}
.module span {
display: block;
color: var(--muted);
font-size: 10px;
margin-top: 4px;
}
.row {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
font-family: var(--mono);
font-size: 12px;
border-bottom: 1px solid var(--line);
padding: 8px 0;
}
.ok {
color: var(--green);
}
.warn {
color: var(--amber);
}
.bad {
color: var(--red);
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.btn {
border: 1px solid var(--cyan);
background: rgba(91, 230, 255, 0.08);
color: var(--cyan);
border-radius: 10px;
padding: 13px 8px;
font-family: var(--mono);
font-weight: 900;
}
.btn.primary {
background: var(--cyan);
color: #021015;
}
.system {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.spec {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 18px;
padding: 16px;
}
.type-big {
font-family: var(--mono);
font-size: 50px;
color: var(--cyan);
}
.radii {
display: flex;
gap: 8px;
align-items: end;
}
.radii div {
background: #061822;
border: 1px solid var(--line);
}
.r1 {
width: 46px;
height: 46px;
border-radius: 6px;
}
.r2 {
width: 64px;
height: 64px;
border-radius: 10px;
}
.r3 {
width: 86px;
height: 86px;
border-radius: 16px;
}
.components {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.component {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 16px;
padding: 14px;
min-height: 150px;
}
.log {
font-family: var(--mono);
background: #02070a;
border-radius: 10px;
padding: 10px;
border: 1px solid var(--line);
font-size: 12px;
color: var(--muted);
margin-top: 8px;
}
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.principle {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 20px;
padding: 18px;
}
.principle b {
font-family: var(--mono);
color: var(--cyan);
}
@media (max-width: 1000px) {
.hero,
.screen-grid {
grid-template-columns: 1fr;
}
.system,
.components {
grid-template-columns: 1fr 1fr;
}
.principles {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.nav {
position: relative;
top: 0;
}
.nav div:last-child {
display: none;
}
.system,
.components {
grid-template-columns: 1fr;
}
.phone {
height: 650px;
}
.terminal {
padding: 20px;
}
h1 {
font-size: 44px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">FightRate::SensorInstrument</div>
<div>
<a href="00-index.html">index</a><a href="#screens">screens</a
><a href="#system">system</a>
</div>
</nav>
<section class="hero">
<div class="terminal">
<div class="eyebrow">Direção 04 / telemetria BLE</div>
<h1>instrumento técnico, confiável, sem decoração inútil.</h1>
<p class="lead">
A melhor direção se a tela Sensor e a confiança na captura forem
diferenciais do app. Parece equipamento de medição, não aplicativo
de lifestyle.
</p>
<div class="cmd">
$ fightrate capture --sensor Magene-H003 --mode continuous
</div>
</div>
<div class="terminal">
<div class="eyebrow">Regra 60 / 30 / 10</div>
<div class="tokens">
<div class="token" style="background: #02070a">
60% console preto
</div>
<div class="token" style="background: #061118">
30% módulo técnico
</div>
<div class="token" style="background: #5be6ff; color: #021015">
10% sinal cyan
</div>
</div>
<p class="lead">
Verde = ok; âmbar = atenção; vermelho = falha. Azul é informação
secundária.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Mobile screens</h2>
<div class="screen-grid">
<div class="phone">
<div class="screen">
<div class="top">
<span>CAPTURE/READY</span><span>BLE OK</span>
</div>
<div class="big">H003</div>
<div class="grid2">
<div class="module"><b>RSSI</b><span>-58 dBm</span></div>
<div class="module"><b>BATT</b><span>84%</span></div>
<div class="module">
<b>HRM</b><span class="ok">notify</span>
</div>
<div class="module"><b>SAMPLES</b><span>0</span></div>
</div>
<div class="dock">
<button class="btn primary">START</button
><button class="btn">CHECK</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>CAPTURE/RUN</span><span>00:42:18</span>
</div>
<div class="big">154<span class="unit">bpm</span></div>
<div class="scope">
<svg viewBox="0 0 280 120">
<path
d="M4 84 C30 82,42 46,70 58 S114 98,146 66 S188 28,220 48 S252 76,276 54"
/>
</svg>
</div>
<div class="grid2">
<div class="module"><b>AVG</b><span>146</span></div>
<div class="module"><b>PEAK</b><span>183</span></div>
<div class="module"><b>Z4/Z5</b><span>41%</span></div>
<div class="module"><b>DROP</b><span class="ok">0</span></div>
</div>
<div class="dock">
<button class="btn">MARK</button
><button class="btn primary">STOP</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>REPORT</span><span>SAVED</span></div>
<div class="big">2844</div>
<div class="row"><span>Samples</span><b class="ok">valid</b></div>
<div class="row"><span>Duration</span><b>47:12</b></div>
<div class="row"><span>Peak sustained</span><b>181</b></div>
<div class="row">
<span>Signal drops</span><b class="ok">0</b>
</div>
<div class="log">
report.source = local.sqlite<br />confidence = high
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>HISTORY</span><span>LOCAL DB</span></div>
<div class="row"><span>2026-06-10 sparring</span><b>96</b></div>
<div class="row"><span>2026-06-09 drill</span><b>54</b></div>
<div class="row"><span>2026-06-07 muay thai</span><b>110</b></div>
<div class="row"><span>2026-06-05 boxe</span><b>77</b></div>
<div class="scope">
<svg viewBox="0 0 280 120">
<path
d="M10 88 L42 78 L80 92 L116 50 L148 72 L188 38 L226 60 L268 44"
/>
</svg>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>SENSOR PROFILE</span><span>GATT</span>
</div>
<div class="big">180D</div>
<div class="row">
<span>2A37 Measurement</span><b class="ok">notify</b>
</div>
<div class="row"><span>2A38 Body Location</span><b>read</b></div>
<div class="row"><span>180F Battery</span><b>read</b></div>
<div class="row">
<span>Device Info</span><b class="warn">maybe</b>
</div>
<div class="dock">
<button class="btn">SCAN</button
><button class="btn primary">LOG</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>INSIGHT ENGINE</span><span>EXPLAIN</span>
</div>
<div class="big">LOAD</div>
<div class="module">
<b>Signal confidence</b
><span class="ok">high: 2.844 valid samples</span>
</div>
<div class="module">
<b>Training note</b
><span>high cardiac load; prioritize recovery</span>
</div>
<div class="module">
<b>Why</b><span>time in Z4/Z5 above personal baseline</span>
</div>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design system</h2>
<div class="system">
<div class="spec">
<div class="eyebrow">Cores</div>
<p class="lead">
Cyan representa sinal. Não usar para botões destrutivos. Verde,
amarelo e vermelho são semânticos.
</p>
</div>
<div class="spec">
<div class="eyebrow">Tipo</div>
<div class="type-big">2A37</div>
<p class="lead">
Monospace real para UUIDs, logs e dados técnicos.
</p>
</div>
<div class="spec">
<div class="eyebrow">Raios</div>
<div class="radii">
<div class="r1"></div>
<div class="r2"></div>
<div class="r3"></div>
</div>
<p class="lead">Mais angular. Parece instrumento.</p>
</div>
<div class="spec">
<div class="eyebrow">Bordas</div>
<p class="lead">
Hairlines cyan escuras. Bordas são grade técnica, não decoração.
</p>
</div>
</div>
</section>
<section class="section">
<h2>Componentes</h2>
<div class="components">
<div class="component">
<div class="eyebrow">Command CTA</div>
<button class="btn primary">START</button
><button class="btn" style="margin-top: 8px">SECONDARY</button>
</div>
<div class="component">
<div class="eyebrow">GATT row</div>
<div class="row"><span>2A37</span><b class="ok">notify</b></div>
<div class="row"><span>2A19</span><b>read</b></div>
</div>
<div class="component">
<div class="eyebrow">Log</div>
<div class="log">connected=true<br />rssi=-61<br />mtu=default</div>
</div>
<div class="component">
<div class="eyebrow">Metric</div>
<div class="big" style="font-size: 42px">-61</div>
<span class="ok">stable</span>
</div>
<div class="component">
<div class="eyebrow">Alert</div>
<span class="bad">signal lost</span>
<div class="log">retry in 2s</div>
</div>
</div>
</section>
<section class="section">
<h2>Princípios</h2>
<div class="principles">
<div class="principle">
<b>Transparência técnica</b>
<p class="lead">
Mostrar o que o sensor expõe e o que o app está usando.
</p>
</div>
<div class="principle">
<b>Confiança</b>
<p class="lead">Dados têm status, origem e qualidade.</p>
</div>
<div class="principle">
<b>Risco</b>
<p class="lead">Pode parecer técnico demais para usuário casual.</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,653 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate Direction 05 — Minimal Performance</title>
<style>
:root {
--bg: #f3eee3;
--paper: #fffaf0;
--ink: #16130e;
--muted: #726a5d;
--line: #d8cdb9;
--accent: #11110f;
--sand: #e9ddc7;
--red: #c83a2a;
--green: #168a5a;
--amber: #d48a12;
--r-xs: 4px;
--r-sm: 9px;
--r-md: 14px;
--r-lg: 22px;
--r-xl: 30px;
--shadow: 0 28px 80px rgba(22, 19, 14, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "New York", Georgia, serif;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 14% 2%,
rgba(200, 58, 42, 0.12),
transparent 34%
),
linear-gradient(rgba(22, 19, 14, 0.035) 1px, transparent 1px);
background-size:
auto,
100% 34px;
}
.wrap {
width: min(1180px, calc(100% - 28px));
margin: auto;
padding: 34px 0 82px;
position: relative;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 16px 0;
}
.nav a {
font-family: ui-sans-serif, system-ui, sans-serif;
color: var(--muted);
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 12px;
margin-left: 16px;
}
.brand {
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 950;
letter-spacing: -0.04em;
}
.hero {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 24px;
margin-top: 38px;
}
.hero-panel {
background: rgba(255, 250, 240, 0.72);
border: 1px solid var(--line);
border-radius: var(--r-xl);
padding: 32px;
box-shadow: var(--shadow);
}
.eyebrow {
font-family: ui-sans-serif, system-ui, sans-serif;
text-transform: uppercase;
letter-spacing: 0.22em;
font-size: 11px;
color: var(--red);
font-weight: 900;
}
h1 {
font-size: clamp(52px, 9vw, 112px);
line-height: 0.86;
letter-spacing: -0.075em;
margin: 16px 0;
}
h2 {
font-size: 44px;
letter-spacing: -0.055em;
line-height: 0.96;
margin: 0 0 12px;
}
.lead {
font-family: ui-sans-serif, system-ui, sans-serif;
color: var(--muted);
font-size: 18px;
line-height: 1.55;
max-width: 720px;
}
.tokens {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
margin-top: 24px;
}
.token {
border-radius: 18px;
padding: 16px;
min-height: 100px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 900;
}
.btn {
border: 1px solid var(--ink);
background: var(--ink);
color: var(--paper);
border-radius: 999px;
padding: 14px 18px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 900;
}
.btn.secondary {
background: transparent;
color: var(--ink);
}
.section {
margin-top: 34px;
}
.screen-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.phone {
height: 590px;
background: #111;
border-radius: 40px;
padding: 9px;
box-shadow: var(--shadow);
}
.screen {
height: 100%;
border-radius: 32px;
background: var(--paper);
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
.top {
display: flex;
justify-content: space-between;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
font-weight: 900;
}
.number {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 82px;
line-height: 0.84;
letter-spacing: -0.09em;
font-weight: 950;
}
.unit {
font-size: 17px;
color: var(--muted);
font-weight: 600;
letter-spacing: 0;
}
.rule-line {
height: 1px;
background: var(--line);
}
.chart {
height: 136px;
border-bottom: 1px solid var(--ink);
position: relative;
}
.chart svg {
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--ink);
stroke-width: 3;
stroke-linecap: round;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.metric {
border-top: 1px solid var(--line);
padding-top: 10px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.metric b {
font-size: 24px;
}
.metric span {
display: block;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 900;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dock .btn {
padding: 14px 8px;
}
.list {
display: grid;
gap: 0;
border-top: 1px solid var(--line);
}
.item {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
padding: 14px 0;
border-bottom: 1px solid var(--line);
font-family: ui-sans-serif, system-ui, sans-serif;
}
.item span {
color: var(--muted);
font-size: 12px;
}
.note {
background: #f3eadb;
border-radius: 18px;
padding: 14px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.system {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.spec {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 24px;
padding: 18px;
}
.spec h3 {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 22px;
letter-spacing: -0.04em;
margin: 8px 0;
}
.typebig {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 62px;
letter-spacing: -0.08em;
line-height: 0.86;
}
.radii {
display: flex;
gap: 9px;
align-items: end;
}
.radii div {
background: var(--sand);
border: 1px solid var(--line);
}
.r1 {
width: 48px;
height: 48px;
border-radius: 4px;
}
.r2 {
width: 68px;
height: 68px;
border-radius: 14px;
}
.r3 {
width: 92px;
height: 92px;
border-radius: 30px;
}
.components {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
.component {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 22px;
padding: 14px;
min-height: 154px;
}
.field {
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px;
background: #fbf3e5;
color: var(--muted);
font-family: ui-sans-serif, system-ui, sans-serif;
margin-top: 10px;
}
.pill {
display: inline-flex;
border-radius: 999px;
border: 1px solid var(--line);
padding: 7px 10px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 11px;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.12em;
}
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.principle {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 24px;
padding: 20px;
}
.principle b {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 22px;
}
@media (max-width: 1000px) {
.hero,
.screen-grid {
grid-template-columns: 1fr;
}
.system,
.components {
grid-template-columns: 1fr 1fr;
}
.principles {
grid-template-columns: 1fr;
}
}
@media (max-width: 620px) {
.nav div:last-child {
display: none;
}
.hero-panel {
padding: 22px;
}
.system,
.components {
grid-template-columns: 1fr;
}
.phone {
height: 650px;
}
h1 {
font-size: 60px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">FightRate / Minimal Performance</div>
<div>
<a href="00-index.html">Índice</a><a href="#screens">Telas</a
><a href="#system">Sistema</a>
</div>
</nav>
<section class="hero">
<div class="hero-panel">
<div class="eyebrow">Direção 05 / premium minimal</div>
<h1>menos interface, mais verdade de treino.</h1>
<p class="lead">
Um app calmo e caro. Ideal se a marca quiser parecer precisa, madura
e confiável, sem parecer hospital nem dashboard corporativa.
</p>
<button class="btn">Iniciar captura</button>
<button class="btn secondary">Histórico</button>
</div>
<div class="hero-panel">
<div class="eyebrow">Regra 60 / 30 / 10</div>
<div class="tokens">
<div class="token" style="background: #fffaf0">
60% paper quente
</div>
<div class="token" style="background: #e9ddc7">30% areia</div>
<div class="token" style="background: #16130e; color: #fffaf0">
10% tinta
</div>
</div>
<p class="lead">
Vermelho e verde aparecem só como semântica, nunca como tema
inteiro.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Telas mobile</h2>
<div class="screen-grid">
<div class="phone">
<div class="screen">
<div class="top"><span>pronto</span><span>sensor ok</span></div>
<div class="number">--<span class="unit">bpm</span></div>
<div class="rule-line"></div>
<p class="lead">
Inicie a captura antes do treino. Todo o restante pode esperar.
</p>
<div class="metrics">
<div class="metric"><b>84%</b><span>bateria</span></div>
<div class="metric"><b>-58</b><span>rssi</span></div>
<div class="metric"><b>0</b><span>amostras</span></div>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">check-in</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>captura</span><span>42:18</span></div>
<div class="number">154<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 280 130">
<path
d="M8 96 C36 88,44 54,76 61 S116 94,145 70 S190 38,222 50 S250 78,272 54"
/>
</svg>
</div>
<div class="metrics">
<div class="metric"><b>146</b><span>média</span></div>
<div class="metric"><b>183</b><span>pico</span></div>
<div class="metric"><b>41%</b><span>z4/z5</span></div>
</div>
<div class="dock">
<button class="btn secondary">Evento</button
><button
class="btn"
style="background: var(--red); border-color: var(--red)"
>
Parar
</button>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>resumo</span><span>salvo</span></div>
<div class="number">47<span class="unit">min</span></div>
<div class="list">
<div class="item">
<div>Tempo em carga<br /><span>Z4/Z5</span></div>
<b>28m</b>
</div>
<div class="item">
<div>Pico sustentado<br /><span>melhor trecho</span></div>
<b>181</b>
</div>
<div class="item">
<div>Recuperação<br /><span>estimada</span></div>
<b>18h</b>
</div>
</div>
<div class="note">
Carga alta controlada. Amanhã prefira técnica leve.
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top">
<span>histórico</span><span>4 semanas</span>
</div>
<div class="metrics">
<div class="metric"><b>11</b><span>treinos</span></div>
<div class="metric"><b>7h</b><span>total</span></div>
<div class="metric"><b>+8%</b><span>carga</span></div>
</div>
<div class="list">
<div class="item">
<div>Sparring<br /><span>Hoje · 47 min</span></div>
<b>96</b>
</div>
<div class="item">
<div>Muay Thai<br /><span>Seg · 61 min</span></div>
<b>110</b>
</div>
<div class="item">
<div>Boxe<br /><span>Sex · 42 min</span></div>
<b>77</b>
</div>
</div>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>sensor</span><span>ble</span></div>
<div class="number">-61<span class="unit">rssi</span></div>
<div class="list">
<div class="item">
<div>Heart Rate<br /><span>2A37</span></div>
<b>notify</b>
</div>
<div class="item">
<div>Battery<br /><span>2A19</span></div>
<b>read</b>
</div>
<div class="item">
<div>Body Sensor<br /><span>2A38</span></div>
<b>read</b>
</div>
</div>
<span class="pill">perfil estável</span>
</div>
</div>
<div class="phone">
<div class="screen">
<div class="top"><span>insight</span><span>curto</span></div>
<div class="number">18h<span class="unit">rec</span></div>
<p class="lead">
Carga alta. Você passou mais tempo em intensidade de luta que na
média recente.
</p>
<div class="note">
<b>Por que importa?</b><br />Ajuda a decidir se o próximo treino
deve ser duro ou técnico.
</div>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design system</h2>
<div class="system">
<div class="spec">
<div class="eyebrow">Cores</div>
<h3>quente, não fitness genérico</h3>
<p class="lead">
Evita verde neon, azul SaaS e gradiente artificial.
</p>
</div>
<div class="spec">
<div class="eyebrow">Tipo</div>
<div class="typebig">154</div>
<p class="lead">Números sans; títulos serifados editoriais.</p>
</div>
<div class="spec">
<div class="eyebrow">Raio</div>
<div class="radii">
<div class="r1"></div>
<div class="r2"></div>
<div class="r3"></div>
</div>
<p class="lead">Raios elegantes. Não exagerar.</p>
</div>
<div class="spec">
<div class="eyebrow">Bordas</div>
<p class="lead">
Linhas finas e quentes. Pouca sombra. Muito respiro.
</p>
</div>
</div>
</section>
<section class="section">
<h2>Componentes</h2>
<div class="components">
<div class="component">
<div class="eyebrow">Botões</div>
<button class="btn">Primário</button
><button class="btn secondary" style="margin-top: 8px">
Secundário
</button>
</div>
<div class="component">
<div class="eyebrow">Campo</div>
<div class="field">Nome do treino</div>
<div class="field">Observação</div>
</div>
<div class="component">
<div class="eyebrow">Métrica</div>
<div class="number" style="font-size: 52px">96</div>
<span class="pill">trimp</span>
</div>
<div class="component">
<div class="eyebrow">Lista</div>
<div class="item">
<div>Sessão</div>
<b>47m</b>
</div>
<div class="item">
<div>Pico</div>
<b>181</b>
</div>
</div>
<div class="component">
<div class="eyebrow">Nota</div>
<div class="note">Texto curto. Uma conclusão por bloco.</div>
</div>
</div>
</section>
<section class="section">
<h2>Princípios</h2>
<div class="principles">
<div class="principle">
<b>Silêncio visual</b>
<p class="lead">Só o dado importante grita.</p>
</div>
<div class="principle">
<b>Marca premium</b>
<p class="lead">Bom para vender seriedade e maturidade.</p>
</div>
<div class="principle">
<b>Risco</b>
<p class="lead">
Pode parecer frio demais se não houver personalidade verbal.
</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,28 @@
# FightRate React Native Redesign — Direções de Design
Este pacote descarta a direção visual antiga do projeto para fins de exploração.
Não é implementação do app. É um conjunto de propostas profissionais para decidir o design system antes de reconstruir o FightRate em React Native.
## Arquivos
- `00-index.html` — visão geral comparativa das direções.
- `01-corner-command.html` — direção tática premium, estilo mesa de córner/treino real.
- `02-athlete-lab.html` — direção científica, limpa, laboratório de performance.
- `03-fight-night.html` — direção editorial/esportiva, energia de evento e combate.
- `04-sensor-instrument.html` — direção técnica, instrumentação BLE e telemetria.
- `05-minimal-performance.html` — direção minimalista premium, foco máximo em dados úteis.
## Regras para próxima etapa
1. Não portar o design antigo.
2. Não começar React Native antes de escolher uma direção visual.
3. Cada direção deve ser avaliada por:
- legibilidade durante treino;
- confiança visual;
- facilidade de virar marca;
- clareza de dados;
- qualidade em telas pequenas;
- viabilidade em React Native.
4. Round é secundário. O fluxo principal é captura contínua do treino.
5. O design system final deve virar tokens React Native: cores, tipografia, espaçamento, raio, bordas, elevação, estados, motion e componentes.
@@ -0,0 +1,544 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate — Design Directions v2</title>
<style>
:root {
--paper: #f4efe5;
--ink: #12100c;
--muted: #70695d;
--line: #d7cab7;
--red: #d33b2f;
--green: #13855d;
--blue: #0f6cff;
--night: #080705;
--r: 30px;
--shadow: 0 32px 100px rgba(18, 16, 12, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family:
ui-sans-serif,
system-ui,
-apple-system,
Segoe UI,
sans-serif;
line-height: 1.42;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 10% 0%,
rgba(211, 59, 47, 0.12),
transparent 34%
),
radial-gradient(
circle at 86% 12%,
rgba(19, 133, 93, 0.12),
transparent 30%
),
linear-gradient(rgba(18, 16, 12, 0.035) 1px, transparent 1px);
background-size:
auto,
auto,
100% 36px;
}
.wrap {
width: min(1240px, calc(100% - 28px));
margin: auto;
padding: 44px 0 86px;
position: relative;
}
.hero {
border: 1px solid var(--line);
border-radius: 38px;
background: rgba(255, 250, 241, 0.7);
padding: 34px;
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.24em;
font-weight: 950;
color: var(--red);
}
h1 {
font-family: Georgia, serif;
font-size: clamp(54px, 10vw, 124px);
line-height: 0.82;
letter-spacing: -0.085em;
margin: 18px 0 20px;
max-width: 1080px;
}
p {
color: var(--muted);
font-size: 18px;
max-width: 820px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin-top: 28px;
}
.direction {
position: relative;
min-height: 620px;
border-radius: 34px;
border: 1px solid var(--line);
background: #fffaf1;
overflow: hidden;
box-shadow: 0 24px 70px rgba(18, 16, 12, 0.08);
}
.direction.dark {
background: #080705;
color: #f7ead8;
border-color: #30261b;
}
.direction.athlete {
background: #f7fbf6;
}
.direction:before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.08),
transparent 40%
);
}
.top {
padding: 24px;
}
.num {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
font-weight: 950;
}
.dark .num {
color: #b7a68a;
}
.direction h2 {
font-size: 44px;
line-height: 0.92;
letter-spacing: -0.06em;
margin: 10px 0 12px;
}
.direction p {
font-size: 15px;
}
.phone {
width: 245px;
height: 500px;
margin: 12px auto 0;
border-radius: 42px;
background: #0c0b09;
padding: 10px;
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.24);
transform: rotate(-2deg);
}
.direction:nth-child(2) .phone {
transform: rotate(1.5deg);
}
.direction:nth-child(3) .phone {
transform: rotate(-1deg);
}
.screen {
height: 100%;
border-radius: 32px;
padding: 18px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dark .screen {
background: linear-gradient(180deg, #15100b, #080705);
color: #f8ead7;
}
.athlete .screen {
background: #fcfffc;
color: #12231b;
}
.editorial .screen {
background: #fff7e8;
color: #15120d;
}
.status {
display: flex;
justify-content: space-between;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.16em;
font-weight: 900;
opacity: 0.7;
}
.bpm {
font-size: 78px;
line-height: 0.85;
letter-spacing: -0.1em;
font-weight: 1000;
margin-top: 18px;
}
.unit {
font-size: 18px;
letter-spacing: 0;
opacity: 0.55;
}
.wave {
height: 140px;
margin-top: 18px;
border-radius: 22px;
position: relative;
overflow: hidden;
}
.dark .wave {
background: #21170f;
border: 1px solid #3b2b1e;
}
.athlete .wave {
background: #edf7f0;
border: 1px solid #d7e4dc;
}
.editorial .wave {
background: #f2e3cb;
border: 1px solid #decdb4;
}
.wave svg {
width: 100%;
height: 100%;
}
.wave path {
fill: none;
stroke: currentColor;
stroke-width: 4;
stroke-linecap: round;
}
.chips {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 18px;
}
.chip {
border-radius: 16px;
padding: 10px 8px;
font-size: 10px;
font-weight: 950;
text-transform: uppercase;
letter-spacing: 0.1em;
text-align: center;
}
.dark .chip {
background: #24190f;
color: #ffbd49;
}
.athlete .chip {
background: #e7f5ed;
color: #08734c;
}
.editorial .chip {
background: #eadbc2;
color: #15120d;
}
.cta {
position: absolute;
left: 24px;
right: 24px;
bottom: 22px;
display: flex;
gap: 10px;
align-items: center;
}
.cta a {
flex: 1;
text-align: center;
text-decoration: none;
border-radius: 999px;
padding: 13px 12px;
font-weight: 950;
color: inherit;
border: 1px solid currentColor;
}
.cta a.primary {
background: var(--ink);
color: var(--paper);
border-color: var(--ink);
}
.dark .cta a.primary {
background: #e23b2e;
border-color: #e23b2e;
color: white;
}
.athlete .cta a.primary {
background: #13855d;
border-color: #13855d;
color: white;
}
.matrix {
margin-top: 32px;
display: grid;
grid-template-columns: 1.25fr repeat(3, 1fr);
border: 1px solid var(--line);
border-radius: 28px;
overflow: hidden;
background: #fffaf1;
}
.matrix div {
padding: 16px;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
font-size: 14px;
}
.matrix div:nth-child(4n) {
border-right: 0;
}
.matrix .head {
font-weight: 950;
background: #fff;
}
.recommend {
margin-top: 26px;
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 18px;
}
.note {
border: 1px solid var(--line);
border-radius: 28px;
background: #fffaf1;
padding: 22px;
box-shadow: 0 18px 50px rgba(18, 16, 12, 0.06);
}
.note b {
display: block;
font-size: 22px;
margin-bottom: 8px;
}
.note ul {
margin: 0;
padding-left: 20px;
color: var(--muted);
}
@media (max-width: 1040px) {
.grid,
.recommend {
grid-template-columns: 1fr;
}
.direction {
min-height: 650px;
}
.matrix {
grid-template-columns: 1fr;
}
.matrix div {
border-right: 0;
}
.phone {
width: 270px;
height: 540px;
}
}
@media (max-width: 620px) {
.wrap {
padding-top: 20px;
}
.hero {
padding: 22px;
}
h1 {
font-size: 56px;
}
.direction h2 {
font-size: 38px;
}
.phone {
width: 250px;
height: 520px;
}
.cta {
position: static;
margin: 20px 24px 24px;
}
}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<div class="eyebrow">FightRate / React Native design directions v2</div>
<h1>Três caminhos fortes. Não cinco ideias fracas.</h1>
<p>
Esta versão substitui os exemplos anteriores. Cada direção agora é
pensada como base real de marca e design system para um app React
Native: captura contínua, histórico, sensor BLE e relatório
pós-treino.
</p>
</section>
<section class="grid">
<article class="direction dark">
<div class="top">
<div class="num">01 / recomendado para treino</div>
<h2>Corner Instrument</h2>
<p>
Equipamento de córner: escuro, intenso, legível, com cara de
ferramenta física. Melhor para a tela de captura ao vivo.
</p>
</div>
<div class="phone">
<div class="screen">
<div class="status">
<span>captura ativa</span><span>42:18</span>
</div>
<div class="bpm">154<span class="unit">bpm</span></div>
<div class="wave">
<svg viewBox="0 0 280 140">
<path
d="M8 98 C36 82,44 48,78 58 S120 102,150 70 S190 26,222 52 S250 88,272 48"
/>
</svg>
</div>
<div class="chips">
<span class="chip">Méd 146</span
><span class="chip">Pico 183</span
><span class="chip">Z4 41%</span>
</div>
</div>
</div>
<div class="cta">
<a class="primary" href="01-corner-instrument-v2.html"
>Abrir direção</a
>
</div>
</article>
<article class="direction athlete">
<div class="top">
<div class="num">02 / recomendado para app base</div>
<h2>Athlete OS</h2>
<p>
Sistema operacional do atleta: científico sem ser hospitalar,
excelente para histórico, relatórios e confiança nos dados.
</p>
</div>
<div class="phone">
<div class="screen">
<div class="status"><span>relatório</span><span>salvo</span></div>
<div class="bpm">96<span class="unit">trimp</span></div>
<div class="wave">
<svg viewBox="0 0 280 140">
<path
d="M8 100 C52 92,62 70,92 75 S134 106,162 72 S204 42,236 56 S256 76,272 54"
/>
</svg>
</div>
<div class="chips">
<span class="chip">28m carga</span
><span class="chip">18h rec</span><span class="chip">Alta</span>
</div>
</div>
</div>
<div class="cta">
<a class="primary" href="02-athlete-os-v2.html">Abrir direção</a>
</div>
</article>
<article class="direction editorial">
<div class="top">
<div class="num">03 / recomendado para marca</div>
<h2>Combat Editorial</h2>
<p>
Marca premium e editorial: minimalista, madura e memorável. Melhor
para identidade pública e experiência pós-treino.
</p>
</div>
<div class="phone">
<div class="screen">
<div class="status">
<span>semana</span><span>4 treinos</span>
</div>
<div class="bpm">+8<span class="unit">%</span></div>
<div class="wave">
<svg viewBox="0 0 280 140">
<path
d="M8 96 L42 80 L74 92 L116 58 L150 70 L190 42 L230 62 L272 50"
/>
</svg>
</div>
<div class="chips">
<span class="chip">Carga</span><span class="chip">Evolução</span
><span class="chip">Risco baixo</span>
</div>
</div>
</div>
<div class="cta">
<a class="primary" href="03-combat-editorial-v2.html"
>Abrir direção</a
>
</div>
</article>
</section>
<section class="matrix">
<div class="head">Critério</div>
<div class="head">Corner Instrument</div>
<div class="head">Athlete OS</div>
<div class="head">Combat Editorial</div>
<div>Melhor uso</div>
<div>Captura ao vivo</div>
<div>App inteiro / relatório</div>
<div>Marca / histórico premium</div>
<div>Energia</div>
<div>Alta</div>
<div>Média controlada</div>
<div>Baixa e sofisticada</div>
<div>Dados densos</div>
<div>Bom</div>
<div>Excelente</div>
<div>Bom</div>
<div>React Native</div>
<div>Viável</div>
<div>Muito viável</div>
<div>Muito viável</div>
<div>Risco</div>
<div>Ficar escuro demais</div>
<div>Ficar clínico demais</div>
<div>Ficar frio demais</div>
</section>
<section class="recommend">
<div class="note">
<b>Minha recomendação agora</b>
<ul>
<li>Base do app: <b>Athlete OS</b>.</li>
<li>Tela de captura: <b>Corner Instrument</b>.</li>
<li>
Identidade pública/relatórios compartilháveis: elementos de
<b>Combat Editorial</b>.
</li>
</ul>
</div>
<div class="note">
<b>Próxima decisão</b>
<p>
Escolher se o app deve parecer mais “equipamento de treino” ou
“sistema científico do atleta”. Depois disso eu crio o DESIGN.md
final e só então começa o Expo/React Native.
</p>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,831 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate v2 — Corner Instrument</title>
<style>
:root {
--bg: #070605;
--ink: #f8ead8;
--muted: #b9a98e;
--dim: #746754;
--panel: #12100c;
--panel2: #1d170f;
--line: #392d1e;
--red: #e23b2e;
--amber: #ffb43d;
--green: #34c77b;
--cream: #fff0dc;
--r1: 10px;
--r2: 16px;
--r3: 24px;
--r4: 36px;
--r5: 48px;
--shadow: 0 34px 110px rgba(0, 0, 0, 0.54);
--bezier: cubic-bezier(0.18, 0.9, 0.18, 1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family:
ui-sans-serif,
system-ui,
-apple-system,
Segoe UI,
sans-serif;
line-height: 1.42;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 12% -8%,
rgba(226, 59, 46, 0.36),
transparent 34%
),
radial-gradient(
circle at 86% 12%,
rgba(255, 180, 61, 0.14),
transparent 30%
),
linear-gradient(
115deg,
rgba(255, 255, 255, 0.045) 0 1px,
transparent 1px 20px
);
opacity: 0.9;
}
.wrap {
width: min(1260px, calc(100% - 28px));
margin: auto;
padding: 30px 0 90px;
position: relative;
}
.nav {
position: sticky;
top: 14px;
z-index: 5;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(18, 16, 12, 0.82);
backdrop-filter: blur(16px);
padding: 10px 12px 10px 18px;
}
.brand {
font-weight: 1000;
letter-spacing: -0.045em;
}
.brand span {
color: var(--red);
}
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.17em;
margin-left: 16px;
}
.hero {
margin-top: 28px;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: 18px;
}
.slab {
border: 1px solid var(--line);
border-radius: var(--r5);
background: linear-gradient(
145deg,
rgba(29, 23, 15, 0.96),
rgba(10, 8, 6, 0.98)
);
box-shadow: var(--shadow);
overflow: hidden;
position: relative;
}
.slab:before {
content: "";
position: absolute;
inset: 1px;
border-radius: calc(var(--r5) - 1px);
box-shadow: inset 0 1px rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.pad {
padding: 34px;
}
.eyebrow {
font-size: 11px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--amber);
font-weight: 950;
}
.title {
font-size: clamp(58px, 10vw, 132px);
line-height: 0.76;
letter-spacing: -0.095em;
margin: 18px 0 20px;
font-weight: 1000;
}
.lead {
font-size: 18px;
color: var(--muted);
max-width: 780px;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 26px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 12px;
border: 0;
border-radius: 999px;
background: var(--red);
color: white;
padding: 14px 18px;
font-weight: 1000;
text-transform: uppercase;
letter-spacing: 0.08em;
box-shadow: 0 18px 42px rgba(226, 59, 46, 0.32);
transition: transform 0.22s var(--bezier);
}
.btn:hover {
transform: translateY(-2px);
}
.btn.secondary {
background: transparent;
color: var(--ink);
border: 1px solid var(--line);
box-shadow: none;
}
.btn .dot {
width: 30px;
height: 30px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.15);
}
.color-strip {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
margin-top: 20px;
}
.color {
min-height: 122px;
border-radius: 22px;
padding: 16px;
font-weight: 1000;
display: flex;
align-items: end;
}
.section {
margin-top: 28px;
}
.section-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.section h2 {
font-size: 48px;
line-height: 0.88;
letter-spacing: -0.065em;
margin: 0;
}
.section p {
color: var(--muted);
}
.phone-wall {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
}
.device {
height: 640px;
border-radius: 48px;
background: #030302;
padding: 10px;
border: 1px solid #3b2d1c;
box-shadow: 0 26px 80px rgba(0, 0, 0, 0.55);
}
.screen {
height: 100%;
border-radius: 38px;
background: linear-gradient(180deg, #19130c, #0b0805);
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
overflow: hidden;
}
.topline {
display: flex;
justify-content: space-between;
color: var(--dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.16em;
font-weight: 950;
}
.bpm {
font-size: 82px;
line-height: 0.82;
letter-spacing: -0.1em;
font-weight: 1000;
}
.unit {
font-size: 18px;
letter-spacing: 0;
color: var(--muted);
}
.chart {
height: 136px;
border-radius: 24px;
background: linear-gradient(180deg, #251a10, #100c08);
border: 1px solid var(--line);
position: relative;
overflow: hidden;
}
.chart:before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(transparent 24px, rgba(255, 255, 255, 0.045) 25px),
linear-gradient(
90deg,
transparent 24px,
rgba(255, 255, 255, 0.045) 25px
);
background-size: 25px 25px;
}
.chart svg {
position: relative;
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--red);
stroke-width: 4.5;
stroke-linecap: round;
filter: drop-shadow(0 0 12px rgba(226, 59, 46, 0.55));
}
.data-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.tile {
border: 1px solid var(--line);
border-radius: 18px;
background: #16110b;
padding: 12px;
}
.tile b {
display: block;
font-size: 24px;
letter-spacing: -0.05em;
}
.tile span {
font-size: 10px;
color: var(--muted);
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 950;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dock .btn {
justify-content: center;
padding: 14px 8px;
font-size: 11px;
}
.stop {
background: var(--cream);
color: #120b07;
box-shadow: none;
}
.tag-row {
display: flex;
gap: 7px;
flex-wrap: wrap;
}
.tag {
border-radius: 999px;
background: #251a10;
color: var(--amber);
padding: 7px 9px;
font-size: 10px;
font-weight: 950;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.list {
display: grid;
gap: 8px;
}
.item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 10px;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 10px 0;
}
.badge {
width: 38px;
height: 38px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--red);
font-weight: 1000;
}
.field {
border: 1px solid var(--line);
background: #0d0906;
border-radius: 16px;
padding: 12px;
color: var(--muted);
}
.system-grid {
display: grid;
grid-template-columns: 1.1fr 0.9fr 1fr;
gap: 18px;
}
.spec {
border: 1px solid var(--line);
border-radius: 30px;
background: rgba(18, 16, 12, 0.8);
padding: 22px;
}
.spec h3 {
font-size: 26px;
letter-spacing: -0.045em;
margin: 8px 0;
}
.scale {
display: grid;
gap: 10px;
}
.scale div {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--line);
padding: 8px 0;
color: var(--muted);
}
.scale b {
color: var(--ink);
}
.radii {
display: flex;
gap: 10px;
align-items: end;
margin-top: 16px;
}
.radii i {
display: block;
background: #24190f;
border: 1px solid var(--line);
}
.ra {
width: 54px;
height: 54px;
border-radius: 10px;
}
.rb {
width: 72px;
height: 72px;
border-radius: 24px;
}
.rc {
width: 96px;
height: 96px;
border-radius: 40px;
}
.component-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.component {
min-height: 160px;
border: 1px solid var(--line);
border-radius: 24px;
background: #120f0b;
padding: 14px;
}
.motion {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.motion-card {
border: 1px solid var(--line);
border-radius: 26px;
background: #120f0b;
padding: 18px;
}
.motion-card b {
color: var(--amber);
}
@media (max-width: 1180px) {
.phone-wall {
grid-template-columns: repeat(2, 1fr);
}
.component-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.hero,
.system-grid,
.motion {
grid-template-columns: 1fr;
}
.section-head {
display: block;
}
.device {
height: 680px;
}
}
@media (max-width: 620px) {
.nav {
position: relative;
top: 0;
}
.nav div:last-child {
display: none;
}
.pad {
padding: 22px;
}
.title {
font-size: 64px;
}
.phone-wall,
.component-grid {
grid-template-columns: 1fr;
}
.device {
height: 700px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">Fight<span>Rate</span> / Corner Instrument</div>
<div>
<a href="00-v2-index.html">v2 index</a><a href="#screens">telas</a
><a href="#system">sistema</a><a href="#components">componentes</a>
</div>
</nav>
<section class="hero">
<div class="slab pad">
<div class="eyebrow">Direção 01 / equipamento de córner</div>
<div class="title">treino ligado. celular esquecido.</div>
<p class="lead">
O app deve parecer uma peça de equipamento no canto do tatame:
forte, confiável e legível de longe. Poucos elementos. Dados
grandes. Ação primária óbvia.
</p>
<div class="actions">
<button class="btn">
Iniciar treino <span class="dot">↗</span></button
><button class="btn secondary">Ver relatório</button>
</div>
</div>
<div class="slab pad">
<div class="eyebrow">Tokens de marca</div>
<div class="color-strip">
<div class="color" style="background: #070605">60% preto lona</div>
<div class="color" style="background: #1d170f">
30% couro queimado
</div>
<div class="color" style="background: #e23b2e">10% golpe</div>
</div>
<p class="lead">
Âmbar sinaliza leitura auxiliar; vermelho é ação, pico ou risco.
Nada de azul SaaS, nada de verde fitness genérico.
</p>
</div>
</section>
<section id="screens" class="section">
<div class="section-head">
<div>
<h2>Telas principais</h2>
<p>O mesmo sistema aplicado a seis momentos reais do produto.</p>
</div>
</div>
<div class="phone-wall">
<div class="device">
<div class="screen">
<div class="topline">
<span>pré-treino</span><span>h003 ok</span>
</div>
<div class="bpm">--<span class="unit">bpm</span></div>
<p class="lead">
Coloque a cinta, toque uma vez e largue o celular.
</p>
<div class="tag-row">
<span class="tag">sensor pronto</span
><span class="tag">bateria 84%</span>
</div>
<div class="data-grid">
<div class="tile"><b>-58</b><span>rssi</span></div>
<div class="tile"><b>0</b><span>drops</span></div>
<div class="tile"><b>0</b><span>amostras</span></div>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">Check-in</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>captura ativa</span><span>42:18</span>
</div>
<div class="bpm">154<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 280 140">
<path
d="M8 104 C36 86,44 46,78 60 S120 106,150 72 S190 28,222 54 S250 92,272 48"
/>
</svg>
</div>
<div class="data-grid">
<div class="tile"><b>146</b><span>média</span></div>
<div class="tile"><b>183</b><span>pico</span></div>
<div class="tile"><b>41%</b><span>Z4/Z5</span></div>
</div>
<div class="dock">
<button class="btn secondary">Evento</button
><button class="btn stop">Parar</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>pós-treino</span><span>salvo</span>
</div>
<div class="bpm">47<span class="unit">min</span></div>
<div class="tag-row">
<span class="tag">carga alta</span
><span class="tag">rec 18h</span>
</div>
<div class="list">
<div class="item">
<span class="badge">1</span>
<div>
Entrada em ritmo<br /><span style="color: var(--muted)"
>0-10 min</span
>
</div>
<b>138</b>
</div>
<div class="item">
<span class="badge">2</span>
<div>
Bloco mais intenso<br /><span style="color: var(--muted)"
>10-31 min</span
>
</div>
<b>181</b>
</div>
<div class="item">
<span class="badge">3</span>
<div>
Queda controlada<br /><span style="color: var(--muted)"
>31-47 min</span
>
</div>
<b>152</b>
</div>
</div>
<div class="dock">
<button class="btn">Relatório</button
><button class="btn secondary">Exportar</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>histórico</span><span>junho</span>
</div>
<div class="data-grid">
<div class="tile"><b>8</b><span>treinos</span></div>
<div class="tile"><b>5h</b><span>tempo</span></div>
<div class="tile"><b>+8%</b><span>carga</span></div>
</div>
<div class="list">
<div class="item">
<span class="badge">S</span>
<div>
Sparring<br /><span style="color: var(--muted)"
>Hoje · 47 min · pico 183</span
>
</div>
<b>96</b>
</div>
<div class="item">
<span class="badge">M</span>
<div>
Muay Thai<br /><span style="color: var(--muted)"
>Seg · 61 min · alta carga</span
>
</div>
<b>110</b>
</div>
<div class="item">
<span class="badge">B</span>
<div>
Boxe<br /><span style="color: var(--muted)"
>Sex · 42 min</span
>
</div>
<b>77</b>
</div>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>sensor</span><span>ble profile</span>
</div>
<div class="bpm">-61<span class="unit">rssi</span></div>
<div class="field">Heart Rate Service · 180D</div>
<div class="field">Measurement · 2A37 · notify</div>
<div class="field">Battery · 2A19 · read</div>
<div class="field">Body Sensor · 2A38 · read</div>
<div class="tag-row">
<span class="tag">perfil estável</span
><span class="tag">0 quedas</span>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>insight</span><span>explicável</span>
</div>
<div class="bpm">18h<span class="unit">rec</span></div>
<p class="lead">
Carga cardiovascular alta. Amanhã: técnica leve ou descanso
ativo.
</p>
<div class="tile">
<b>Por que importa?</b
><span>Você ficou acima da sua média recente em Z4/Z5.</span>
</div>
<div class="tile">
<b>Confiança</b
><span>2.844 amostras válidas. Sem quedas de sinal.</span>
</div>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design book visual</h2>
<div class="system-grid">
<div class="spec">
<div class="eyebrow">Cor</div>
<h3>Escuro com propósito</h3>
<p class="lead">
A tela ativa usa fundo escuro para reduzir distração no treino. O
vermelho não é decoração; é ação, intensidade e risco.
</p>
<div class="color-strip">
<div class="color" style="background: #070605">base</div>
<div class="color" style="background: #1d170f">surface</div>
<div class="color" style="background: #e23b2e">accent</div>
</div>
</div>
<div class="spec">
<div class="eyebrow">Tipografia</div>
<h3>Número primeiro</h3>
<div class="bpm" style="font-size: 72px">154</div>
<p class="lead">
Display condensado/forte para BPM. Labels pequenos, uppercase e
espaçados. Texto longo só no pós-treino.
</p>
</div>
<div class="spec">
<div class="eyebrow">Forma</div>
<h3>Material físico</h3>
<div class="radii">
<i class="ra"></i><i class="rb"></i><i class="rc"></i>
</div>
<p class="lead">
Painéis grandes com raio generoso; campos técnicos com raio médio;
badges redondos para estados.
</p>
</div>
</div>
</section>
<section id="components" class="section">
<h2>Componentes</h2>
<div class="component-grid">
<div class="component">
<div class="eyebrow">Primary button</div>
<button class="btn">Iniciar <span class="dot">↗</span></button
><button class="btn secondary" style="margin-top: 10px">
Secundário
</button>
</div>
<div class="component">
<div class="eyebrow">Stop button</div>
<button class="btn stop">Parar treino</button>
<p class="lead" style="font-size: 13px">
Contraste claro para ação crítica.
</p>
</div>
<div class="component">
<div class="eyebrow">Metric tile</div>
<div class="tile"><b>183</b><span>pico bpm</span></div>
</div>
<div class="component">
<div class="eyebrow">Sensor row</div>
<div class="field">2A37 · notify</div>
<div class="field">2A19 · read</div>
</div>
<div class="component">
<div class="eyebrow">Insight</div>
<div class="tile">
<b>Carga alta</b><span>reduza intensidade amanhã</span>
</div>
</div>
<div class="component">
<div class="eyebrow">Tag states</div>
<div class="tag-row">
<span class="tag">ok</span><span class="tag">z4</span
><span class="tag">rec</span>
</div>
</div>
</div>
</section>
<section class="section">
<h2>Motion e uso no React Native</h2>
<div class="motion">
<div class="motion-card">
<b>Captura</b>
<p class="lead">
Animação mínima: pulso discreto no status ativo e transições de
número. Nada de blur pesado.
</p>
</div>
<div class="motion-card">
<b>Histórico</b>
<p class="lead">
Entrada em lista com fade/translate curto. A densidade vem da
hierarquia, não de cards infinitos.
</p>
</div>
<div class="motion-card">
<b>Sensor</b>
<p class="lead">
Logs e características com expansão progressiva. Dados técnicos
precisam de ordem e monospace pontual.
</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,726 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate v2 — Athlete OS</title>
<style>
:root {
--bg: #f4f7f2;
--paper: #fefffc;
--ink: #102019;
--muted: #65766d;
--line: #d6e1d9;
--surface: #eaf3ed;
--green: #12875d;
--lime: #c9ef55;
--blue: #1769ff;
--red: #d84c3e;
--amber: #d98a15;
--r1: 10px;
--r2: 16px;
--r3: 24px;
--r4: 34px;
--shadow: 0 30px 90px rgba(16, 32, 25, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family:
ui-sans-serif,
system-ui,
-apple-system,
Segoe UI,
sans-serif;
line-height: 1.45;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
linear-gradient(rgba(16, 32, 25, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(16, 32, 25, 0.04) 1px, transparent 1px),
radial-gradient(
circle at 82% 0%,
rgba(201, 239, 85, 0.34),
transparent 30%
);
background-size:
32px 32px,
32px 32px,
auto;
}
.wrap {
width: min(1260px, calc(100% - 28px));
margin: auto;
padding: 30px 0 90px;
position: relative;
}
.nav {
position: sticky;
top: 14px;
z-index: 4;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--line);
border-radius: 22px;
background: rgba(254, 255, 252, 0.78);
backdrop-filter: blur(16px);
padding: 12px 16px;
}
.brand {
font-weight: 950;
letter-spacing: -0.045em;
}
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.17em;
margin-left: 16px;
}
.hero {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 18px;
margin-top: 28px;
}
.panel {
border: 1px solid var(--line);
border-radius: 38px;
background: rgba(254, 255, 252, 0.86);
box-shadow: var(--shadow);
padding: 34px;
}
.eyebrow {
font-size: 11px;
letter-spacing: 0.23em;
text-transform: uppercase;
color: var(--green);
font-weight: 950;
}
.title {
font-size: clamp(56px, 9vw, 118px);
line-height: 0.8;
letter-spacing: -0.09em;
font-weight: 1000;
margin: 18px 0 20px;
}
.lead {
font-size: 18px;
color: var(--muted);
max-width: 800px;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 26px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 18px;
background: var(--green);
color: white;
padding: 14px 18px;
font-weight: 950;
box-shadow: 0 16px 38px rgba(18, 135, 93, 0.22);
}
.btn.secondary {
background: var(--surface);
color: var(--ink);
border: 1px solid var(--line);
box-shadow: none;
}
.token-strip {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
margin-top: 20px;
}
.token {
min-height: 116px;
border-radius: 20px;
padding: 16px;
display: flex;
align-items: end;
font-weight: 950;
}
.section {
margin-top: 30px;
}
.section h2 {
font-size: 48px;
line-height: 0.9;
letter-spacing: -0.065em;
margin: 0 0 12px;
}
.phone-wall {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
}
.device {
height: 640px;
border-radius: 46px;
background: #111;
padding: 10px;
box-shadow: 0 24px 70px rgba(16, 32, 25, 0.18);
}
.screen {
height: 100%;
border-radius: 36px;
background: var(--paper);
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
overflow: hidden;
}
.topline {
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.15em;
font-weight: 950;
}
.metric-main {
font-size: 80px;
line-height: 0.84;
letter-spacing: -0.095em;
font-weight: 1000;
}
.unit {
font-size: 18px;
color: var(--muted);
letter-spacing: 0;
}
.chart {
height: 140px;
border-radius: 24px;
background: linear-gradient(180deg, #f8fff9, #edf7f0);
border: 1px solid var(--line);
overflow: hidden;
position: relative;
}
.chart:before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(transparent 27px, rgba(16, 32, 25, 0.045) 28px),
linear-gradient(90deg, transparent 27px, rgba(16, 32, 25, 0.045) 28px);
background-size: 28px 28px;
}
.chart svg {
position: relative;
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--green);
stroke-width: 4;
stroke-linecap: round;
}
.tile-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.tile {
background: #f7fbf8;
border: 1px solid var(--line);
border-radius: 18px;
padding: 12px;
}
.tile b {
display: block;
font-size: 24px;
letter-spacing: -0.045em;
}
.tile span {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 950;
}
.zones {
display: grid;
grid-template-columns: 1fr 1.2fr 1.7fr 1.2fr 0.7fr;
height: 14px;
border-radius: 999px;
overflow: hidden;
}
.zones i:nth-child(1) {
background: #92d7ff;
}
.zones i:nth-child(2) {
background: #aee8c5;
}
.zones i:nth-child(3) {
background: #c9ef55;
}
.zones i:nth-child(4) {
background: #e7a83f;
}
.zones i:nth-child(5) {
background: #d84c3e;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dock .btn {
padding: 14px 8px;
}
.table {
display: grid;
gap: 0;
border-top: 1px solid var(--line);
}
.row {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
padding: 12px 0;
border-bottom: 1px solid var(--line);
}
.row span {
color: var(--muted);
}
.badge {
display: inline-flex;
width: max-content;
border-radius: 999px;
background: #e6f5ec;
color: #08734c;
padding: 7px 10px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 950;
}
.sensor {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 13px;
}
.system-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 18px;
}
.spec {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 28px;
padding: 22px;
}
.spec h3 {
font-size: 26px;
letter-spacing: -0.045em;
margin: 8px 0;
}
.type-demo {
font-size: 72px;
line-height: 0.86;
letter-spacing: -0.09em;
font-weight: 1000;
}
.radii {
display: flex;
gap: 10px;
align-items: end;
}
.radii i {
display: block;
background: var(--surface);
border: 1px solid var(--line);
}
.ra {
width: 54px;
height: 54px;
border-radius: 10px;
}
.rb {
width: 72px;
height: 72px;
border-radius: 18px;
}
.rc {
width: 96px;
height: 96px;
border-radius: 30px;
}
.component-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.component {
min-height: 160px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 24px;
padding: 14px;
}
.field {
background: #f7fbf8;
border: 1px solid var(--line);
border-radius: 16px;
padding: 12px;
color: var(--muted);
margin-top: 10px;
}
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.principle {
background: var(--paper);
border: 1px solid var(--line);
border-left: 6px solid var(--green);
border-radius: 0 24px 24px 0;
padding: 20px;
}
.principle b {
font-size: 22px;
}
@media (max-width: 1180px) {
.phone-wall {
grid-template-columns: repeat(2, 1fr);
}
.component-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.hero,
.system-grid,
.principles {
grid-template-columns: 1fr;
}
.device {
height: 680px;
}
}
@media (max-width: 620px) {
.nav {
position: relative;
top: 0;
}
.nav div:last-child {
display: none;
}
.panel {
padding: 22px;
}
.title {
font-size: 64px;
}
.phone-wall,
.component-grid {
grid-template-columns: 1fr;
}
.device {
height: 700px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">FightRate / Athlete OS</div>
<div>
<a href="00-v2-index.html">v2 index</a><a href="#screens">telas</a
><a href="#system">sistema</a><a href="#components">componentes</a>
</div>
</nav>
<section class="hero">
<div class="panel">
<div class="eyebrow">Direção 02 / sistema do atleta</div>
<div class="title">ciência de treino sem cara de hospital.</div>
<p class="lead">
A direção mais forte para o app inteiro. O atleta confia porque tudo
tem origem, contexto e explicação. É limpo, mas não vazio.
</p>
<div class="actions">
<button class="btn">Capturar treino</button
><button class="btn secondary">Abrir histórico</button>
</div>
</div>
<div class="panel">
<div class="eyebrow">Tokens de marca</div>
<div class="token-strip">
<div class="token" style="background: #fefffc">
60% paper clínico quente
</div>
<div class="token" style="background: #eaf3ed">
30% superfície lab
</div>
<div class="token" style="background: #12875d; color: white">
10% ação
</div>
</div>
<p class="lead">
Verde é estabilidade e ação primária. Azul só para técnica. Vermelho
só para alerta real.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Telas principais</h2>
<div class="phone-wall">
<div class="device">
<div class="screen">
<div class="topline">
<span>pronto</span><span>sensor ok</span>
</div>
<div class="metric-main">--<span class="unit">bpm</span></div>
<p class="lead">
A janela inteira do treino será registrada. Uma ação, nenhum
controle durante a luta.
</p>
<div class="tile-grid">
<div class="tile"><b>84%</b><span>bateria</span></div>
<div class="tile"><b>-58</b><span>rssi</span></div>
<div class="tile"><b>0</b><span>amostras</span></div>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">Check-in</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>captura ativa</span><span>42:18</span>
</div>
<div class="metric-main">154<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 280 140">
<path
d="M8 100 C42 94,54 62,82 66 S122 98,154 74 S204 38,232 54 S254 78,274 56"
/>
</svg>
</div>
<div class="zones"><i></i><i></i><i></i><i></i><i></i></div>
<div class="tile-grid">
<div class="tile"><b>146</b><span>média</span></div>
<div class="tile"><b>183</b><span>pico</span></div>
<div class="tile"><b>41%</b><span>Z4/Z5</span></div>
</div>
<div class="dock">
<button class="btn secondary">Evento</button
><button class="btn" style="background: var(--red)">
Parar
</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>relatório</span><span>alta confiança</span>
</div>
<div class="metric-main">96<span class="unit">trimp</span></div>
<div class="table">
<div class="row"><span>Tempo em carga</span><b>28m</b></div>
<div class="row">
<span>Pico sustentado</span><b>181 bpm</b>
</div>
<div class="row">
<span>Recuperação sugerida</span><b>18h</b>
</div>
<div class="row"><span>Amostras válidas</span><b>2.844</b></div>
</div>
<span class="badge">carga alta controlada</span>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>histórico</span><span>4 semanas</span>
</div>
<div class="tile-grid">
<div class="tile"><b>11</b><span>treinos</span></div>
<div class="tile"><b>7h</b><span>tempo</span></div>
<div class="tile"><b>+8%</b><span>carga</span></div>
</div>
<div class="table">
<div class="row"><span>Sparring · hoje</span><b>96</b></div>
<div class="row"><span>Muay Thai · seg</span><b>110</b></div>
<div class="row"><span>Boxe · sex</span><b>77</b></div>
<div class="row"><span>Drill · qua</span><b>54</b></div>
</div>
<div class="chart">
<svg viewBox="0 0 280 140">
<path
d="M12 110 L46 94 L82 104 L118 62 L154 78 L194 44 L232 60 L270 48"
/>
</svg>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>sensor lab</span><span>gatt</span>
</div>
<div class="metric-main sensor">2A37</div>
<div class="table sensor">
<div class="row">
<span>Heart Rate Service</span><b>180D</b>
</div>
<div class="row"><span>Measurement</span><b>notify</b></div>
<div class="row"><span>Battery</span><b>read</b></div>
<div class="row"><span>Body Sensor</span><b>read</b></div>
<div class="row"><span>RSSI</span><b>-61</b></div>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>insights</span><span>explicável</span>
</div>
<div class="metric-main">18h<span class="unit">rec</span></div>
<p class="lead">
Você acumulou carga alta com sinal estável. Próximo treino:
técnica ou recuperação ativa.
</p>
<div class="tile">
<b>Por que importa?</b
><span
>Evita somar fadiga quando o corpo já respondeu com alta
intensidade.</span
>
</div>
<div class="tile">
<b>Base</b><span>Comparação com 4 semanas e 11 sessões.</span>
</div>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design book visual</h2>
<div class="system-grid">
<div class="spec">
<div class="eyebrow">Cor</div>
<h3>Confiança antes de impacto</h3>
<p class="lead">
O app base é claro. A captura pode escurecer se adotarmos o
híbrido com Corner Instrument.
</p>
<div class="token-strip">
<div class="token" style="background: #fefffc">paper</div>
<div class="token" style="background: #eaf3ed">surface</div>
<div class="token" style="background: #12875d; color: white">
action
</div>
</div>
</div>
<div class="spec">
<div class="eyebrow">Tipo</div>
<h3>Hierarquia clínica</h3>
<div class="type-demo">96</div>
<p class="lead">
Números grandes. Labels pequenos. Texto explicativo apenas onde
ajuda decisão.
</p>
</div>
<div class="spec">
<div class="eyebrow">Forma</div>
<h3>Raios médios</h3>
<div class="radii">
<i class="ra"></i><i class="rb"></i><i class="rc"></i>
</div>
<p class="lead">
Menos agressivo; mais confiável. Bordas sempre suaves e
funcionais.
</p>
</div>
</div>
</section>
<section id="components" class="section">
<h2>Componentes</h2>
<div class="component-grid">
<div class="component">
<div class="eyebrow">Button</div>
<button class="btn">Primário</button
><button class="btn secondary" style="margin-top: 10px">
Secundário
</button>
</div>
<div class="component">
<div class="eyebrow">Metric</div>
<div class="tile"><b>146</b><span>bpm médio</span></div>
</div>
<div class="component">
<div class="eyebrow">Zone bar</div>
<div class="zones" style="margin-top: 12px">
<i></i><i></i><i></i><i></i><i></i>
</div>
</div>
<div class="component">
<div class="eyebrow">Form</div>
<div class="field">Tipo de treino</div>
<div class="field">RPE 0-10</div>
</div>
<div class="component">
<div class="eyebrow">Sensor</div>
<div class="row sensor"><span>2A37</span><b>notify</b></div>
<div class="row sensor"><span>2A19</span><b>read</b></div>
</div>
<div class="component">
<div class="eyebrow">Insight</div>
<div class="tile">
<b>Carga alta</b><span>recomendação clara + base dos dados</span>
</div>
</div>
</div>
</section>
<section class="section">
<h2>Princípios</h2>
<div class="principles">
<div class="principle">
<b>Todo dado tem origem</b>
<p class="lead">
Amostras, sessão, comparação e confiança aparecem quando importam.
</p>
</div>
<div class="principle">
<b>Captura sem distração</b>
<p class="lead">O usuário não opera round no meio da luta.</p>
</div>
<div class="principle">
<b>Histórico denso</b>
<p class="lead">
Pós-treino aceita mais informação, desde que organizada.
</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,736 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FightRate v2 — Combat Editorial</title>
<style>
:root {
--bg: #efe5d5;
--paper: #fff7e8;
--ink: #15110c;
--muted: #746a5d;
--line: #d8c6aa;
--black: #080705;
--red: #c93a2e;
--gold: #bd7a17;
--olive: #66724e;
--r1: 6px;
--r2: 14px;
--r3: 24px;
--r4: 34px;
--shadow: 0 32px 90px rgba(21, 17, 12, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: Georgia, "Times New Roman", serif;
line-height: 1.42;
}
body:before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
radial-gradient(
circle at 8% 0%,
rgba(201, 58, 46, 0.13),
transparent 34%
),
linear-gradient(rgba(21, 17, 12, 0.04) 1px, transparent 1px);
background-size:
auto,
100% 38px;
}
.wrap {
width: min(1260px, calc(100% - 28px));
margin: auto;
padding: 30px 0 90px;
position: relative;
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 18px 0;
position: sticky;
top: 0;
background: rgba(239, 229, 213, 0.86);
backdrop-filter: blur(14px);
z-index: 3;
}
.brand {
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 950;
letter-spacing: -0.045em;
}
.nav a {
font-family: ui-sans-serif, system-ui, sans-serif;
color: var(--muted);
text-decoration: none;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.17em;
margin-left: 16px;
}
.hero {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 18px;
margin-top: 30px;
}
.editorial {
background: rgba(255, 247, 232, 0.74);
border: 1px solid var(--line);
border-radius: 34px;
padding: 34px;
box-shadow: var(--shadow);
}
.eyebrow {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 11px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--red);
font-weight: 950;
}
.title {
font-size: clamp(58px, 10vw, 130px);
line-height: 0.78;
letter-spacing: -0.085em;
margin: 18px 0 20px;
}
.lead {
font-family: ui-sans-serif, system-ui, sans-serif;
color: var(--muted);
font-size: 18px;
max-width: 790px;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 26px;
}
.btn {
border: 1px solid var(--black);
border-radius: 999px;
background: var(--black);
color: var(--paper);
padding: 14px 18px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 950;
}
.btn.secondary {
background: transparent;
color: var(--ink);
}
.token-strip {
display: grid;
grid-template-columns: 6fr 3fr 1fr;
gap: 8px;
margin-top: 20px;
}
.token {
min-height: 116px;
border-radius: 20px;
padding: 16px;
display: flex;
align-items: end;
font-family: ui-sans-serif, system-ui, sans-serif;
font-weight: 950;
}
.section {
margin-top: 32px;
}
.section h2 {
font-size: 48px;
line-height: 0.9;
letter-spacing: -0.065em;
margin: 0 0 12px;
}
.phone-wall {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
}
.device {
height: 640px;
border-radius: 44px;
background: #111;
padding: 10px;
box-shadow: 0 30px 90px rgba(21, 17, 12, 0.22);
}
.screen {
height: 100%;
border-radius: 34px;
background: var(--paper);
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
overflow: hidden;
}
.topline {
font-family: ui-sans-serif, system-ui, sans-serif;
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.15em;
font-weight: 950;
}
.display {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 80px;
line-height: 0.84;
letter-spacing: -0.1em;
font-weight: 1000;
}
.unit {
font-size: 18px;
letter-spacing: 0;
color: var(--muted);
}
.rule {
height: 1px;
background: var(--ink);
opacity: 0.22;
}
.chart {
height: 140px;
border-bottom: 2px solid var(--ink);
position: relative;
}
.chart:before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 28px;
height: 1px;
background: var(--line);
box-shadow:
0 28px 0 var(--line),
0 56px 0 var(--line);
}
.chart svg {
position: relative;
width: 100%;
height: 100%;
}
.chart path {
fill: none;
stroke: var(--ink);
stroke-width: 3.5;
stroke-linecap: round;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.metric {
border-top: 1px solid var(--line);
padding-top: 10px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.metric b {
font-size: 24px;
}
.metric span {
display: block;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
font-weight: 950;
}
.dock {
margin-top: auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.dock .btn {
padding: 14px 8px;
}
.list {
display: grid;
border-top: 1px solid var(--line);
}
.item {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
border-bottom: 1px solid var(--line);
padding: 14px 0;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.item small {
display: block;
color: var(--muted);
font-size: 12px;
}
.note {
background: #f1e4cf;
border: 1px solid var(--line);
border-radius: 22px;
padding: 14px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.pill {
display: inline-flex;
width: max-content;
border-radius: 999px;
border: 1px solid var(--line);
padding: 7px 10px;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 950;
}
.system-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 18px;
}
.spec {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 28px;
padding: 22px;
}
.spec h3 {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 26px;
letter-spacing: -0.045em;
margin: 8px 0;
}
.type-demo {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 72px;
line-height: 0.86;
letter-spacing: -0.09em;
font-weight: 1000;
}
.radii {
display: flex;
gap: 10px;
align-items: end;
}
.radii i {
display: block;
background: #eadcc5;
border: 1px solid var(--line);
}
.ra {
width: 54px;
height: 54px;
border-radius: 6px;
}
.rb {
width: 72px;
height: 72px;
border-radius: 16px;
}
.rc {
width: 96px;
height: 96px;
border-radius: 30px;
}
.component-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
.component {
min-height: 160px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: 24px;
padding: 14px;
}
.field {
border: 1px solid var(--line);
border-radius: 16px;
padding: 12px;
background: #fbf0dc;
color: var(--muted);
font-family: ui-sans-serif, system-ui, sans-serif;
margin-top: 10px;
}
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.principle {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 28px;
padding: 20px;
}
.principle b {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 22px;
}
.share-card {
border-radius: 28px;
background: var(--black);
color: var(--paper);
padding: 18px;
min-height: 220px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.share-card .display {
font-size: 58px;
}
.share-card .lead {
color: #bea98c;
font-size: 14px;
}
@media (max-width: 1180px) {
.phone-wall {
grid-template-columns: repeat(2, 1fr);
}
.component-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.hero,
.system-grid,
.principles {
grid-template-columns: 1fr;
}
.device {
height: 680px;
}
}
@media (max-width: 620px) {
.nav {
position: relative;
}
.nav div:last-child {
display: none;
}
.editorial {
padding: 22px;
}
.title {
font-size: 64px;
}
.phone-wall,
.component-grid {
grid-template-columns: 1fr;
}
.device {
height: 700px;
}
}
</style>
</head>
<body>
<main class="wrap">
<nav class="nav">
<div class="brand">FightRate / Combat Editorial</div>
<div>
<a href="00-v2-index.html">v2 index</a><a href="#screens">telas</a
><a href="#system">sistema</a><a href="#components">componentes</a>
</div>
</nav>
<section class="hero">
<div class="editorial">
<div class="eyebrow">Direção 03 / marca premium</div>
<div class="title">
relatório de treino como editorial de performance.
</div>
<p class="lead">
Essa direção deixa o FightRate com cara de marca madura: menos neon,
menos dashboard, mais publicação premium de dados pessoais. Ideal
para histórico, relatório e compartilhamento.
</p>
<div class="actions">
<button class="btn">Ver semana</button
><button class="btn secondary">Abrir captura</button>
</div>
</div>
<div class="editorial">
<div class="eyebrow">Tokens de marca</div>
<div class="token-strip">
<div class="token" style="background: #fff7e8">
60% papel quente
</div>
<div class="token" style="background: #e8d8bd">
30% areia editorial
</div>
<div class="token" style="background: #15110c; color: #fff7e8">
10% tinta
</div>
</div>
<p class="lead">
Vermelho é assinatura e alerta. Dourado é conquista. A interface não
precisa gritar para parecer forte.
</p>
</div>
</section>
<section id="screens" class="section">
<h2>Telas principais</h2>
<div class="phone-wall">
<div class="device">
<div class="screen">
<div class="topline">
<span>captura pronta</span><span>h003</span>
</div>
<div class="display">--<span class="unit">bpm</span></div>
<div class="rule"></div>
<p class="lead">
Comece antes do treino. O app some; o dado fica.
</p>
<div class="metrics">
<div class="metric"><b>84%</b><span>bateria</span></div>
<div class="metric"><b>-58</b><span>rssi</span></div>
<div class="metric"><b>0</b><span>drops</span></div>
</div>
<div class="dock">
<button class="btn">Iniciar</button
><button class="btn secondary">Check-in</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline"><span>ao vivo</span><span>42:18</span></div>
<div class="display">154<span class="unit">bpm</span></div>
<div class="chart">
<svg viewBox="0 0 280 140">
<path
d="M8 98 C42 88,52 62,82 68 S122 98,154 74 S204 44,232 58 S254 76,274 54"
/>
</svg>
</div>
<div class="metrics">
<div class="metric"><b>146</b><span>média</span></div>
<div class="metric"><b>183</b><span>pico</span></div>
<div class="metric"><b>41%</b><span>z4/z5</span></div>
</div>
<div class="dock">
<button class="btn secondary">Evento</button
><button
class="btn"
style="background: var(--red); border-color: var(--red)"
>
Parar
</button>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>relatório</span><span>salvo</span>
</div>
<div class="display">47<span class="unit">min</span></div>
<div class="list">
<div class="item">
<div>Tempo em carga<small>Z4/Z5</small></div>
<b>28m</b>
</div>
<div class="item">
<div>Pico sustentado<small>melhor bloco</small></div>
<b>181</b>
</div>
<div class="item">
<div>Recuperação<small>estimativa</small></div>
<b>18h</b>
</div>
</div>
<div class="note">
Carga alta, mas sinal limpo. Bom treino para avaliar pressão
sustentada.
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>histórico</span><span>junho</span>
</div>
<div class="display">+8<span class="unit">%</span></div>
<div class="metrics">
<div class="metric"><b>11</b><span>treinos</span></div>
<div class="metric"><b>7h</b><span>tempo</span></div>
<div class="metric"><b>3</b><span>picos</span></div>
</div>
<div class="list">
<div class="item">
<div>Sparring<small>hoje · 47 min</small></div>
<b>96</b>
</div>
<div class="item">
<div>Muay Thai<small>seg · 61 min</small></div>
<b>110</b>
</div>
<div class="item">
<div>Boxe<small>sex · 42 min</small></div>
<b>77</b>
</div>
</div>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline"><span>sensor</span><span>ble</span></div>
<div class="display">-61<span class="unit">rssi</span></div>
<div class="list">
<div class="item">
<div>Heart Rate<small>2A37</small></div>
<b>notify</b>
</div>
<div class="item">
<div>Battery<small>2A19</small></div>
<b>read</b>
</div>
<div class="item">
<div>Body Sensor<small>2A38</small></div>
<b>read</b>
</div>
</div>
<span class="pill">perfil estável</span>
</div>
</div>
<div class="device">
<div class="screen">
<div class="topline">
<span>share</span><span>fight report</span>
</div>
<div class="share-card">
<div>
<div class="display">181</div>
<p class="lead">
pico do treino · 47 min · carga alta controlada
</p>
</div>
<span class="pill">FightRate Report</span>
</div>
<p class="lead">
Essa direção funciona muito bem para cards compartilháveis e
marca.
</p>
</div>
</div>
</div>
</section>
<section id="system" class="section">
<h2>Design book visual</h2>
<div class="system-grid">
<div class="spec">
<div class="eyebrow">Cor</div>
<h3>Papel, tinta, assinatura</h3>
<p class="lead">
Mais editorial que aplicativo fitness. Reduz ruído e aumenta valor
percebido.
</p>
<div class="token-strip">
<div class="token" style="background: #fff7e8">paper</div>
<div class="token" style="background: #e8d8bd">sand</div>
<div class="token" style="background: #15110c; color: #fff7e8">
ink
</div>
</div>
</div>
<div class="spec">
<div class="eyebrow">Tipo</div>
<h3>Display contido</h3>
<div class="type-demo">+8</div>
<p class="lead">
Serif para títulos da marca; sans pesada para números. Contraste
sem carnaval.
</p>
</div>
<div class="spec">
<div class="eyebrow">Forma</div>
<h3>Premium sem espuma</h3>
<div class="radii">
<i class="ra"></i><i class="rb"></i><i class="rc"></i>
</div>
<p class="lead">
Raios limpos. Linhas finas. Poucas sombras. Muita intenção.
</p>
</div>
</div>
</section>
<section id="components" class="section">
<h2>Componentes</h2>
<div class="component-grid">
<div class="component">
<div class="eyebrow">Button</div>
<button class="btn">Primário</button
><button class="btn secondary" style="margin-top: 10px">
Secundário
</button>
</div>
<div class="component">
<div class="eyebrow">Metric</div>
<div class="metric"><b>183</b><span>pico</span></div>
</div>
<div class="component">
<div class="eyebrow">Field</div>
<div class="field">Nome do treino</div>
<div class="field">Observação</div>
</div>
<div class="component">
<div class="eyebrow">List row</div>
<div class="item">
<div>Sessão<small>47 min</small></div>
<b>96</b>
</div>
</div>
<div class="component">
<div class="eyebrow">Insight</div>
<div class="note">Uma conclusão. Uma explicação. Uma ação.</div>
</div>
<div class="component">
<div class="eyebrow">Share card</div>
<div class="share-card" style="min-height: 130px">
<div class="display" style="font-size: 42px">181</div>
<span class="pill">pico</span>
</div>
</div>
</div>
</section>
<section class="section">
<h2>Princípios</h2>
<div class="principles">
<div class="principle">
<b>Pouco, mas pesado</b>
<p class="lead">Cada elemento precisa merecer espaço.</p>
</div>
<div class="principle">
<b>Marca antes de interface</b>
<p class="lead">
Ideal para transformar FightRate em produto reconhecível.
</p>
</div>
<div class="principle">
<b>Captura simples</b>
<p class="lead">
Pode usar Corner Instrument na captura para mais energia.
</p>
</div>
</div>
</section>
</main>
</body>
</html>
@@ -0,0 +1,38 @@
# FightRate React Native Redesign — v2
Status: **direção escolhida**
A direção oficial para seguir é:
```text
01 — Corner Instrument
```
Arquivo visual de referência:
```text
01-corner-instrument-v2.html
```
Design system canônico:
```text
../../../../FIGHTRATE_DESIGN_SYSTEM.md
```
## Decisão
Vamos seguir com o FightRate como **equipamento de córner / instrumento de captura**.
A experiência deve ser escura, forte, legível e direta. O app não deve parecer dashboard genérico, SaaS, app fitness comum ou coleção de cards.
## Direções não escolhidas
- `02-athlete-os-v2.html` fica apenas como referência de clareza de dados.
- `03-combat-editorial-v2.html` fica apenas como referência futura para relatórios compartilháveis.
Nenhuma dessas duas é base visual principal neste momento.
## Próxima fase
Antes de codar React Native, usar `FIGHTRATE_DESIGN_SYSTEM.md` como contrato visual e transformar os tokens em tema Expo/React Native.
@@ -0,0 +1,336 @@
# FightRate — métricas científicas derivadas de frequência cardíaca e HRV/VFC
> Documento técnico-educacional. Nenhuma métrica abaixo deve ser apresentada como diagnóstico médico. Interpretação clínica exige contexto, validação, exame adequado, controle de artefatos e revisão profissional.
## Escopo
Este relatório consolida fórmulas, algoritmos e limites de interpretação para transformar dados reais do FightRate em indicadores de saúde, desempenho e carga interna. A base foi limitada a literatura acadêmica indexada em PubMed/PMC ou páginas de periódico científico.
## Premissa de dados do app
O FightRate já pode usar:
- `bpm`: frequência cardíaca instantânea enviada pelo Heart Rate Service `180D`, Measurement `2A37`.
- `timestamp`: instante real de cada amostra persistida.
- `rrIntervalsMs`: intervalos RR quando o sensor enviar esse campo no pacote `2A37`.
- `workout_sessions`: janelas de treino marcadas dentro do fluxo contínuo.
- `continuous_heart_rate_samples`: amostras contínuas, inclusive fora da janela de treino.
Separação crítica:
1. **Métricas BPM-only**: podem ser calculadas hoje com FC timestampada.
2. **Métricas dependentes de perfil**: exigem FCmáx, FCrepouso, idade e/ou sexo informados ou estimados.
3. **Métricas HRV/VFC reais**: exigem RR intervals confiáveis, filtragem de artefatos e janela adequada.
## Matriz de implementação
| Métrica | Dados mínimos | Pode entrar já? | Observação |
| --- | --- | --- | --- |
| FC atual/média/mín/máx | BPM + timestamp | Sim | Baixo risco; métrica central. |
| Amplitude e variabilidade de BPM | BPM + timestamp | Sim | Útil para esforço estável vs intervalado. |
| Tempo em zonas fixas | BPM + timestamp | Sim | Usar como zona operacional até existir perfil. |
| Tendência/deriva de FC | BPM + timestamp | Sim | Rotular como tendência, não causa fisiológica. |
| Recuperação pós-janela | BPM contínuo após fim da janela | Sim, quando houver dados pós-esforço | Precisa continuar monitorando após encerrar janela. |
| Zonas por FCmáx | BPM + FCmáx ou idade | Parcial | FCmáx por idade tem erro individual. Preferir FCmáx manual. |
| Karvonen/HRR | BPM + FCmáx + FCrepouso | Parcial | Mais individualizado, mas exige perfil. |
| TRIMP | BPM + duração + FCmáx + FCrepouso + coeficiente | Parcial | Útil como carga estimada; limitações em intervalados. |
| RMSSD/SDNN/pNN50 | RR intervals limpos | Só se RR vier do sensor | Não derivar de BPM médio. |
| LF/HF/frequência | RR + interpolação + janela adequada | Futuro | Alto risco de interpretação; evitar MVP. |
| HRV-guided readiness | RR matinal padronizado | Futuro | Depende de protocolo diário consistente. |
## 1. Frequência cardíaca atual, média, mínima e máxima
**Fonte fisiológica:** monitoramento direto de FC é prático e barato, mas a relação FC↔intensidade é afetada por hidratação, temperatura, fadiga, cafeína, estresse e deriva cardiovascular. Achten & Jeukendrup destacam que FC é útil em campo, mas limitada para estimar gasto energético ou VO₂ individual com alta precisão.
**Dados:** BPM válido e timestamp.
**Fórmulas:**
```text
FC_atual = último BPM válido
FC_média = soma(BPM) / n
FC_mín = min(BPM)
FC_máx = max(BPM)
Amplitude = FC_máx - FC_mín
```
**Uso no FightRate:**
- Mostrar como leitura primária nos Insights.
- Usar `sampleCount` e duração para indicar qualidade/confiança.
- Não exibir quando não houver amostras reais.
## 2. Variabilidade simples de BPM
Esta não é HRV/VFC. É variabilidade da frequência cardíaca agregada por amostras de BPM. Ainda assim, é útil para caracterizar treino contínuo, intervalado ou caótico.
**Dados:** BPM timestampado.
**Fórmulas:**
```text
média = soma(BPM) / n
variância = soma((BPM - média)^2) / n
DP_BPM = sqrt(variância)
CV_BPM = DP_BPM / média
Amplitude = FC_máx - FC_mín
```
**Classificação operacional sugerida:**
```text
CV_BPM < 0.06 = esforço estável
0.06–0.12 = misto / progressivo
> 0.12 = intervalado / oscilante
```
**Limitação:** CV alto pode indicar treino intervalado, sensor instável ou mudanças bruscas de contexto. Rotular como “oscilação de FC”, não HRV.
## 3. FC máxima estimada e zonas por %FCmáx
Tanaka, Monahan e Seals revisaram a fórmula clássica `220 - idade` e propuseram uma equação mais robusta para adultos saudáveis:
```text
FCmáx_estimada = 208 - 0.7 × idade
%FCmáx = BPM / FCmáx
```
**Zonas operacionais comuns:**
| Zona | %FCmáx | Uso prático |
| --- | ---: | --- |
| Z1 | <60% | recuperação / muito leve |
| Z2 | 60–70% | base aeróbia |
| Z3 | 70–80% | moderado / tempo |
| Z4 | 80–90% | alta pressão |
| Z5 | >90% | redline / esforço máximo relativo |
**Limitação:** Tanaka et al. deixam claro que FCmáx é amplamente explicada pela idade em população saudável, mas a estimativa individual pode errar bastante. Para o app, a FCmáx manual do usuário deve ter prioridade sobre fórmula.
## 4. Reserva cardíaca / Karvonen / HRR
Karvonen, Kentala e Mustala popularizaram o uso da faixa entre FC de repouso e FC máxima como referência de intensidade. Swain & Leutholtz demonstraram que `%HRR` se alinha melhor a `%VO₂ reserve` do que a `%VO₂max`.
**Dados necessários:** BPM, FCmáx, FCrepouso.
**Fórmulas:**
```text
Reserva = FCmáx - FCrepouso
Intensidade_HRR = (BPM - FCrepouso) / Reserva
Alvo_BPM = FCrepouso + Intensidade_alvo × Reserva
```
**Uso no FightRate:**
- Quando perfil existir, mostrar zonas por HRR como modo mais personalizado.
- Sem FCrepouso confiável, cair para zonas por %FCmáx ou zonas operacionais fixas.
## 5. Tempo em zona
Tempo em zona é um dos indicadores mais úteis para leitura visual compacta, porque combina intensidade e duração.
**Dados:** BPM timestampado e função `zona(BPM)`.
**Algoritmo:**
```text
para i de 1 até n-1:
delta = timestamp[i] - timestamp[i-1]
delta_limitado = min(delta, gapMaxMs)
zona = zona(BPM[i])
tempo[zona] += delta_limitado
```
**Regras de robustez:**
- Ignorar ou limitar gaps grandes para não atribuir minutos sem sinal a uma zona.
- Exibir duração e percentual.
- Usar o mesmo cálculo para janela de treino, uso contínuo e faixa customizada.
## 6. Carga interna / TRIMP
Banister introduziu o conceito de treinamento como “dose” fisiológica, combinando duração e intensidade. O TRIMP clássico usa FC relativa à reserva e um peso exponencial. Desgorces et al. discutem que TRIMP é histórico e útil, mas não é intercambiável com métodos alternativos e pode falhar em exercícios intermitentes quando usa FC média.
**Dados necessários:** duração, BPM, FCmáx, FCrepouso, coeficiente.
**Banister TRIMP clássico:**
```text
HRr = (FCmédia - FCrepouso) / (FCmáx - FCrepouso)
TRIMP_homem = duração_min × HRr × 0.64 × e^(1.92 × HRr)
TRIMP_mulher = duração_min × HRr × 0.86 × e^(1.67 × HRr)
```
**TRIMP acumulado por amostra, mais adequado ao FightRate:**
```text
para cada intervalo entre amostras:
HRr_i = clamp((BPM_i - FCrepouso) / (FCmáx - FCrepouso), 0, 1.2)
peso_i = coeficiente_a × e^(coeficiente_b × HRr_i)
TRIMP += delta_min × HRr_i × peso_i
```
**Uso no FightRate:**
- Chamar de **carga estimada**.
- Se FCmáx/FCrepouso não existirem, mostrar “perfil incompleto”.
- Para treino de luta intervalado, preferir versão acumulada, não FC média única.
## 7. Recuperação cardíaca pós-esforço (HRR recovery)
Cole et al. definiram recuperação como a queda da FC do pico ao primeiro minuto após o exercício; em teste clínico, queda `≤ 12 bpm` no primeiro minuto foi associada a maior mortalidade. No FightRate, isso deve ser usado apenas como referência educativa, pois o contexto é treino livre, não teste clínico padronizado.
**Dados necessários:** fim da janela de treino e monitoramento contínuo por 1–2 min depois.
**Fórmulas:**
```text
HRR_1min = FC_fim_ou_pico - FC_60s_após
HRR_2min = FC_fim_ou_pico - FC_120s_após
```
**Algoritmo recomendado:**
1. Ao fechar a janela, manter monitor contínuo ativo.
2. Guardar FC no fim da janela e pico dos últimos minutos.
3. Buscar amostra mais próxima de `endedAt + 60s` e `endedAt + 120s`.
4. Exibir apenas se houver amostras suficientes depois da janela.
## 8. Deriva cardiovascular / tendência de drift
Coyle & González-Alonso definem a deriva cardiovascular como queda progressiva do volume sistólico após 10–20 min de exercício, acompanhada por aumento de FC. Sem potência, velocidade, temperatura e hidratação, o app só deve inferir “tendência de deriva de FC”, não causa.
**Dados:** série BPM longa e timestampada.
**Fórmulas operacionais:**
```text
média_inicial = média de uma janela inicial estável
média_final = média da janela final
Deriva_abs = média_final - média_inicial
Deriva_% = Deriva_abs / média_inicial × 100
```
**Janela sugerida para app:**
- Se duração ≥ 20 min: comparar primeiros 20% vs últimos 20%, ignorando aquecimento curto.
- Se duração < 10 min: não calcular deriva; mostrar “janela curta”.
## 9. HRV/VFC com RR intervals
A Task Force ESC/NASPE estabeleceu padrões clássicos de medição e interpretação de HRV. Shaffer & Ginsberg detalham métricas de domínio do tempo, frequência e não lineares, reforçando que duração da janela, artefatos, respiração, idade, sexo e método de medição mudam a interpretação.
**Dados necessários:** RR/NN intervals em milissegundos, limpos de artefatos.
### Domínio do tempo
```text
SDNN = desvio padrão dos intervalos NN
RMSSD = sqrt(média((NN[i+1] - NN[i])²))
NN50 = contagem de pares sucessivos com |ΔNN| > 50 ms
pNN50 = NN50 / número_de_pares × 100
```
**Interpretação resumida:**
- RMSSD é uma das métricas mais usadas para atividade vagal de curto prazo.
- SDNN depende fortemente da duração da janela; 24h e 5 min não são intercambiáveis.
- pNN50 é correlacionado com RMSSD, mas RMSSD costuma ser preferido.
### Domínio de frequência
Bandas clássicas:
```text
VLF: 0.0033–0.04 Hz
LF: 0.04–0.15 Hz
HF: 0.15–0.40 Hz
LF/HF = potência_LF / potência_HF
```
**Limitação importante:** LF/HF é controverso como “balanço simpático-vagal”; respiração, postura e não estacionaridade podem distorcer a leitura. Para MVP, não usar LF/HF como score principal.
### Não lineares
```text
SD1 = dispersão perpendicular no Poincaré; relacionado a RMSSD
SD2 = dispersão longitudinal no Poincaré
SampEn = regularidade/complexidade da série
DFA α1 = correlação fractal de curto prazo
```
**Uso no FightRate:** futuro, apenas após validação de RR do sensor e filtros de artefato.
## 10. HRV-guided readiness / prontidão
Kiviniemi et al. testaram prescrição diária guiada por HRV em treino de endurance: quando HRV matinal caía significativamente, o treino era reduzido para baixa intensidade ou repouso. Buchheit argumenta que medidas quase diárias de 5 min em repouso e FC submáxima podem ser úteis quando interpretadas individualmente com erro de medida, menor mudança importante e contexto de carga.
**Requisitos para FightRate:**
- Protocolo matinal padronizado, não amostra aleatória de treino.
- RR intervals reais.
- Baseline individual de vários dias.
- Exibir como prontidão/protocolo, não diagnóstico.
**Algoritmo futuro:**
```text
baseline = média móvel de LnRMSSD dos últimos 7–10 dias
limite = baseline - desvio_ou_SWC
se LnRMSSD_hoje < limite:
sugestão = reduzir intensidade / recuperar
senão:
sugestão = treino planejado permitido
```
## 11. Alertas e anomalias
**Dados:** BPM, timestamp, perfil opcional.
Regras não clínicas úteis:
```text
Sem sinal = nenhuma amostra válida por N segundos
Pico abrupto = |BPM_atual - BPM_anterior| > limiar em poucos segundos
Alta sustentada = BPM > limite por N segundos
Queda pós-pico = FC cai X bpm em 60/120s após janela
```
**Cuidado:** alertas de saúde reais exigem validação médica. No app, usar linguagem operacional: “verifique sensor”, “pausa recomendada”, “zona alta sustentada”.
## Recomendações diretas para a tela Insights
1. Substituir lista simples por uma **grade tabular de instrumentos** com linhas compactas.
2. Priorizar métricas calculáveis hoje:
- FC atual/média/mín/máx;
- duração e amostras;
- tempo em zonas;
- variabilidade BPM;
- carga observada/carga estimada;
- tendência de deriva;
- recuperação pós-janela quando existir.
3. Exibir métricas dependentes de perfil como **bloqueadas** ou “perfil necessário”, sem inventar FCmáx/FCrepouso silenciosamente.
4. Exibir HRV/VFC como **indisponível** quando não houver RR intervals confiáveis.
5. Cada métrica deve ter:
- valor;
- unidade;
- status (`OK`, `perfil`, `RR`, `janela curta`, `sem dados`);
- frase curta de leitura;
- base de cálculo/confiança.
6. Para uso espacial eficiente, usar tabela em linhas densas, trilhos e separadores do Corner Instrument — não cards genéricos.
## Referências acadêmicas
1. Task Force ESC/NASPE. *Heart rate variability: standards of measurement, physiological interpretation and clinical use*. Circulation. 1996. PubMed: https://pubmed.ncbi.nlm.nih.gov/8598068/
2. Shaffer F, Ginsberg JP. *An Overview of Heart Rate Variability Metrics and Norms*. Front Public Health. 2017. PMC: https://pmc.ncbi.nlm.nih.gov/articles/PMC5624990/
3. Tanaka H, Monahan KD, Seals DR. *Age-predicted maximal heart rate revisited*. J Am Coll Cardiol. 2001. PubMed: https://pubmed.ncbi.nlm.nih.gov/11153730/
4. Karvonen MJ, Kentala E, Mustala O. *The effects of training on heart rate; a longitudinal study*. Ann Med Exp Biol Fenn. 1957. PubMed: https://pubmed.ncbi.nlm.nih.gov/13470504/
5. Swain DP, Leutholtz BC. *Heart rate reserve is equivalent to %VO2 reserve, not to %VO2max*. Med Sci Sports Exerc. 1997. PubMed: https://pubmed.ncbi.nlm.nih.gov/9139182/
6. Cole CR, Blackstone EH, Pashkow FJ, Snader CE, Lauer MS. *Heart-rate recovery immediately after exercise as a predictor of mortality*. N Engl J Med. 1999. PubMed: https://pubmed.ncbi.nlm.nih.gov/10536127/
7. Achten J, Jeukendrup AE. *Heart rate monitoring: applications and limitations*. Sports Med. 2003. PubMed: https://pubmed.ncbi.nlm.nih.gov/12762827/
8. Coyle EF, González-Alonso J. *Cardiovascular drift during prolonged exercise: new perspectives*. Exerc Sport Sci Rev. 2001. PubMed: https://pubmed.ncbi.nlm.nih.gov/11337829/
9. Buchheit M. *Monitoring training status with HR measures: do all roads lead to Rome?* Front Physiol. 2014. PubMed/PMC: https://pubmed.ncbi.nlm.nih.gov/24578692/
10. Kiviniemi AM, Hautala AJ, Kinnunen H, Tulppo MP. *Endurance training guided individually by daily heart rate variability measurements*. Eur J Appl Physiol. 2007. PubMed: https://pubmed.ncbi.nlm.nih.gov/17849143/
11. Desgorces FD, Hourcade JC, Dubois R, Toussaint JF, Noirez P. *Training load quantification of high intensity exercises: discrepancies between original and alternative methods*. PLoS One. 2020. PMC: https://pmc.ncbi.nlm.nih.gov/articles/PMC7398532/
12. Foster C, Florhaug JA, Franklin J, et al. *A new approach to monitoring exercise training*. J Strength Cond Res. 2001. PubMed: https://pubmed.ncbi.nlm.nih.gov/11708692/
@@ -0,0 +1,147 @@
# FightRate Android MVP Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build the first Android-native FightRate app scaffold for live combat-sport heart-rate tracking with Magene H003 BLE support.
**Architecture:** Android-native Kotlin app using Jetpack Compose for UI, pure Kotlin domain/parse logic, Android BLE APIs isolated behind a BLE package, and local persistence planned with Room. The first implementation slice creates a buildable app shell plus a tested Heart Rate Measurement parser.
**Tech Stack:** Kotlin, Gradle Kotlin DSL, Android Gradle Plugin, Jetpack Compose Material 3, Coroutines/Flow, Android BLE APIs, JUnit.
---
## File Structure
```text
settings.gradle.kts Project module declaration
build.gradle.kts Root Gradle plugin versions
gradle.properties Android/Kotlin build flags
app/build.gradle.kts Android app dependencies and config
app/src/main/AndroidManifest.xml Permissions, activity, BLE feature flags
app/src/main/java/com/healthtracking/fightrate/MainActivity.kt
app/src/main/java/com/healthtracking/fightrate/ble/HeartRateParser.kt
app/src/main/java/com/healthtracking/fightrate/domain/model/FightSport.kt
app/src/main/java/com/healthtracking/fightrate/domain/model/HeartRateMeasurement.kt
app/src/main/java/com/healthtracking/fightrate/ui/theme/FightRateTheme.kt
app/src/main/java/com/healthtracking/fightrate/ui/theme/Color.kt
app/src/main/java/com/healthtracking/fightrate/ui/screens/HomeScreen.kt
app/src/test/java/com/healthtracking/fightrate/ble/HeartRateParserTest.kt
```
---
## Task 1: Android project scaffold
**Files:**
- Create: `settings.gradle.kts`
- Create: `build.gradle.kts`
- Create: `gradle.properties`
- Create: `app/build.gradle.kts`
- Create: `app/src/main/AndroidManifest.xml`
- Create: `app/src/main/java/com/healthtracking/fightrate/MainActivity.kt`
- [ ] **Step 1: Create Gradle project files**
Use Android app module `:app`, package `com.healthtracking.fightrate`, min SDK 26, target/compile SDK 35, Kotlin 2.x compatible Compose.
- [ ] **Step 2: Create manifest**
Declare Bluetooth permissions for Android 12+, legacy Bluetooth/location permissions for Android 11 and below, BLE feature, foreground service permissions, and `MainActivity`.
- [ ] **Step 3: Create minimal MainActivity**
Use `ComponentActivity`, `setContent`, `FightRateTheme`, and `HomeScreen`.
- [ ] **Step 4: Verify**
Run: `./gradlew :app:assembleDebug`
Expected: build succeeds when Gradle/Android SDK are installed.
---
## Task 2: Heart Rate Measurement parser, TDD first
**Files:**
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateParser.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/domain/model/HeartRateMeasurement.kt`
- Create: `app/src/test/java/com/healthtracking/fightrate/ble/HeartRateParserTest.kt`
- [ ] **Step 1: Write tests**
Cover these Bluetooth Heart Rate Measurement payloads:
```kotlin
byteArrayOf(0x00, 72) // uint8 BPM
byteArrayOf(0x01, 0x2C, 0x01) // uint16 BPM = 300
byteArrayOf(0x08, 90, 0x34, 0x12) // energy expended = 0x1234
byteArrayOf(0x10, 120, 0x00, 0x04) // RR interval = 1024 -> 1000ms
byteArrayOf() // invalid empty payload
```
- [ ] **Step 2: Implement parser**
Parse flags, BPM format, optional energy expended, optional RR intervals. Throw `IllegalArgumentException` for malformed payloads.
- [ ] **Step 3: Verify**
Run: `./gradlew :app:testDebugUnitTest`
Expected: all parser tests pass.
---
## Task 3: FightRate visual shell
**Files:**
- Create: `app/src/main/java/com/healthtracking/fightrate/ui/theme/Color.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ui/theme/FightRateTheme.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/domain/model/FightSport.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ui/screens/HomeScreen.kt`
- [ ] **Step 1: Define design tokens**
Use the FightRate palette from the design system: black canvas, charcoal surfaces, fight orange, redline red, recovery green, muted gray text.
- [ ] **Step 2: Build static HomeScreen**
Include static UI cards for sensor status, current BPM, round timer, zone bar, sport chips, and session metrics.
- [ ] **Step 3: Verify**
Run: `./gradlew :app:assembleDebug`
Expected: app compiles and opens to a dark FightRate dashboard.
---
## Task 4: BLE connection layer placeholder
**Files:**
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateBleConstants.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateBleScanner.kt`
- Create: `app/src/main/java/com/healthtracking/fightrate/ble/HeartRateBleClient.kt`
- [ ] **Step 1: Define UUID constants**
```kotlin
HEART_RATE_SERVICE = 0000180d-0000-1000-8000-00805f9b34fb
HEART_RATE_MEASUREMENT = 00002a37-0000-1000-8000-00805f9b34fb
BODY_SENSOR_LOCATION = 00002a38-0000-1000-8000-00805f9b34fb
BATTERY_SERVICE = 0000180f-0000-1000-8000-00805f9b34fb
BATTERY_LEVEL = 00002a19-0000-1000-8000-00805f9b34fb
```
- [ ] **Step 2: Scanner contract**
Expose a Kotlin API that can later be wired to `BluetoothLeScanner` and emits scan results.
- [ ] **Step 3: Client contract**
Expose connect/disconnect and `Flow<HeartRateMeasurement>` API.
---
## Current Execution Choice
The user asked to proceed with app creation, so start with **Inline Execution** for Tasks 1–3, stopping after verification evidence and reporting environment blockers if Android SDK/Gradle is unavailable.
@@ -0,0 +1,52 @@
# Implementation Plan: FightRate P1 Essential Features
## Overview
Implement the next FightRate feature batch as incremental, testable slices: workout sessions, round timer, configurable alerts, more robust BLE state, and sensor metadata (battery/body location). Keep PT-BR visible copy and English code identifiers.
## Architecture Decisions
- Keep local-first SQLite as source of truth.
- Add session data without replacing existing sample storage.
- Prefer pure domain logic for round timer and alerts so behavior is unit-testable.
- Keep BLE foreground service as owner of the device connection.
- Expose sensor metadata through existing service broadcasts to avoid direct BLE ownership in the ViewModel.
## Task List
### Phase 1: Domain foundation
- [ ] Add `WorkoutSession`, `WorkoutSessionStatus`, `RoundTimerConfig`, `RoundTimerSnapshot`, and calculators.
- [ ] Add unit tests for timer transitions and session summary calculations.
### Phase 2: Persistence
- [ ] Add `workout_sessions` table and nullable `session_id` on `heart_rate_samples`.
- [ ] Add safe migration from current database version.
- [ ] Add store methods to start/pause/resume/finish active sessions and insert samples with active session id.
### Phase 3: UI/ViewModel integration
- [ ] Add session and round timer state to `LiveHeartRateUiState`.
- [ ] Add ViewModel actions: start/pause/resume/finish session and update timer config.
- [ ] Add visible Compose controls in Live screen.
### Phase 4: Alerts
- [ ] Add persisted alert settings with safe defaults.
- [ ] Add configurable vibration/sound toggles and zone/limit thresholds.
- [ ] Trigger alerts from service/ViewModel without spamming.
### Phase 5: BLE hardening and metadata
- [ ] Add BLE status events for scan/connect/service/descriptor failures and unexpected disconnects.
- [ ] Add conservative reconnect/backoff in foreground service.
- [ ] Read battery level and body sensor location when available.
- [ ] Show metadata in Sensor screen.
## Review/Verification
- [ ] Run unit tests after domain/persistence slices.
- [ ] Run `./gradlew :app:testDebugUnitTest :app:assembleDebug` before final response.
- [ ] Run code review and front/UI review via fresh-context reviewers.
- [ ] Reconcile findings and fix required issues.
## Risks
| Risk | Impact | Mitigation |
|---|---|---|
| Database migration loses samples | High | Add additive migration, no DROP for current versions |
| BLE changes break screen-off tracking | High | Keep service ownership; add metadata/status only |
| UI becomes cluttered | Medium | Compact cards and PT-BR labels |
| Alert spam | Medium | Throttle alerts and use safe defaults |
@@ -0,0 +1,207 @@
# FightRate — Plano Data-First para histórico, gráficos e UI profissional
## Situação crítica
O feedback atual mostra dois bugs de produto, não apenas problema visual:
1. O gráfico tem espaço vazio demais e exige rolar horizontalmente por muito tempo até encontrar leituras.
2. O histórico não está confiável: dados parecem não salvar ou não ficam acessíveis depois.
Conclusão: dados e persistência devem voltar a ser prioridade absoluta antes de polimento visual.
## Objetivo
Transformar o FightRate em um app data-first:
- Cada batimento recebido deve ser persistido localmente.
- Cada treino deve ter uma janela temporal confiável.
- Histórico deve mostrar dados reais recuperáveis.
- Gráficos devem usar domínio baseado apenas nas amostras reais disponíveis.
- Não pode existir scroll infinito/área vazia até achar leitura.
- Interface deve priorizar dados e navegação histórica, seguindo o design system Combat Lab.
## Skills aplicadas
- `fightrate-quality-loop`: spec → auditoria → implementação em fatias → build.
- `systematic-debugging`: encontrar causa raiz antes de corrigir.
- `test-driven-development`: criar testes de regressão para domínio de gráfico/histórico.
- `design-system` e `ui-ux-pro-max`: estado vazio útil, chart/data UX, consistência visual.
## Hipóteses a investigar
### H1 — Domínio X do Vico está incluindo janela cheia, não amostras reais
Sintoma: usuário rola para a direita “para sempre” até encontrar leituras.
Possível causa:
- O gráfico usa `windowStartMillis`/`windowEndMillis` grandes, mas as amostras estão concentradas em pequeno trecho.
- O X é calculado relativo à janela selecionada, então se a leitura está no fim da janela, sobra um enorme vazio à esquerda/direita.
- O scroll/zoom inicial pode estar mostrando início/fim errado.
Correção esperada:
- Domínio padrão do gráfico deve ser `min(sample.timestamp)` → `max(sample.timestamp)`.
- A janela selecionada filtra dados, mas não força o domínio visual a incluir todo o período vazio.
- Exibir texto com a janela consultada e o intervalo real encontrado.
### H2 — Histórico busca janela errada ou estado não atualiza após persistência
Sintoma: dados históricos não aparecem/salvam.
Possíveis causas:
- `publishLiveSample` salva, mas histórico não recarrega após cada nova amostra.
- `selectedPeriod` usa uma janela curta ou errada.
- Sessão finalizada tem samples com `sessionId` nulo.
- `finishCurrentSession` não associa amostras sem sessão dentro da janela.
- UI exibe `historySamples`, mas dados reais ficam apenas em `visibleSamples`/`dailySamples`.
Correções esperadas:
- Confirmar que `HeartRateSampleStore.save` recebe `sessionId` ativo.
- Confirmar que `samplesForSession` funciona depois do finish.
- Confirmar que `samplesBetween(start, end)` recupera dados persistidos.
- Atualizar estado de histórico depois de salvar amostra e depois de finalizar treino.
### H3 — UI não oferece navegação histórica suficiente
Sintoma: “os dados devem poder ser vistos de qualquer maneira”.
Correção esperada:
- Histórico com modos claros:
- Últimos 10 min
- Última hora
- Hoje
- Sessão atual/finalizada
- Se janela consultada não tem dados, mostrar “nenhum dado nessa janela” e sugerir janela que tem dados.
- Se existe sessão finalizada, exibir relatório e gráfico daquela sessão.
## Arquitetura desejada
### Domínio puro novo/revisado
Criar `ChartDataWindow.kt` se necessário.
Responsabilidades:
- Receber samples + janela consultada.
- Retornar:
- `visibleSamples`: somente amostras reais e úteis.
- `dataStartMillis` / `dataEndMillis`: domínio real do gráfico.
- `queryStartMillis` / `queryEndMillis`: janela consultada.
- `status`: Empty, SinglePoint, Ready.
- resumo: duração real, média, min, max, densidade.
- Não depender de Compose/Vico/Android.
### Persistência
Reutilizar `HeartRateSampleStore`.
Necessário auditar/corrigir:
- insert de samples;
- active session id;
- associação de sample com session id;
- queries por sessão;
- queries por período;
- refresh após write.
Não fazer migração destrutiva.
### ViewModel
- Não mover BLE para ViewModel.
- Garantir que o estado de histórico seja atualizado após persistência.
- Expor chart windows já preparados para UI.
- Evitar que UI calcule regras complexas.
### UI / Gráfico
- `HeartRateLineChart` deve receber domínio real de dados.
- X deve ser relativo a `dataStartMillis`, não ao começo da janela vazia.
- Se `samples.size < 2`, não chamar Vico.
- Mostrar estado vazio editorial.
- Mostrar uma linha de contexto:
- “Consultado: Hoje”
- “Dados encontrados: 14:03–14:17”
- Zoom/scroll habilitado, mas sem espaço fantasma.
- Não usar Canvas custom chart.
## Testes obrigatórios
### `ChartDataWindowTest`
1. Janela grande com dados no fim não gera domínio vazio gigante.
2. Sem dados retorna Empty.
3. Um ponto retorna SinglePoint.
4. Dados fora de ordem são ordenados.
5. Dados filtrados preservam timestamp real.
6. Downsampling mantém primeiro e último ponto.
### Store/persistência
Se for viável em teste local unitário:
1. Salvar sample e recuperar por período.
2. Salvar sample em sessão ativa e recuperar por session id.
3. Finalizar sessão e recuperar samples da sessão.
Se não for viável sem Robolectric/instrumented test, adicionar diagnóstico via código e validar pelo fluxo ViewModel/build.
## Implementação em fatias
### Fase 1 — Auditoria e causa raiz
- Ler `HeartRateSampleStore`.
- Ler `LiveHeartRateViewModel`.
- Ler `LiveHeartRateUiState`.
- Ler `HomeScreen` chart/history.
- Localizar onde sample é salvo.
- Localizar onde `historySamples` é atualizado.
- Localizar como Vico define X/domain.
### Fase 2 — Domínio de janela de gráfico
- Criar `ChartDataWindow.kt`.
- Criar `ChartDataWindowTest.kt`.
- Garantir domínio real `dataStartMillis`→`dataEndMillis`.
- Garantir estado Empty/SinglePoint/Ready.
### Fase 3 — Persistência e refresh
- Corrigir sample/session association se necessário.
- Corrigir reload de histórico depois de salvar/finalizar.
- Adicionar método de query se faltar.
### Fase 4 — Gráfico profissional
- Atualizar `HeartRateLineChart` para usar domínio real.
- Remover scroll fantasma.
- Adicionar zoom/scroll Vico com domínio real.
- Remover render de gráfico vazio.
- Mostrar resumo consultado vs encontrado.
### Fase 5 — Interface data-first
- Histórico como centro de dados, não card secundário.
- Sessões e dados recentes com hierarquia clara.
- Evitar dezenas de cards iguais.
- Estados vazios úteis e específicos.
### Fase 6 — Validação final
- `./gradlew :app:testDebugUnitTest :app:assembleDebug`
- Atualizar Obsidian roadmap.
- Enviar APK Telegram.
## Critérios de sucesso
- Usuário abre Histórico e vê dados reais sem procurar em espaço vazio.
- O gráfico começa/termina no intervalo real dos dados encontrados.
- Sem dados: nenhum gráfico é desenhado.
- Dados salvos são recuperáveis por período e por sessão.
- Testes de domínio passam.
- Build completo passa.
- APK enviado.
@@ -0,0 +1,691 @@
# Spec: FightRate P0 — Perfil Avançado e Insights do Lutador
## Objective
Criar a primeira versão do Perfil Avançado do Lutador para coletar dados pessoais, cardiovasculares, subjetivos e de recuperação suficientes para gerar insights úteis de treino sem deixar o app inchado.
O objetivo não é diagnóstico médico. O objetivo é apoio esportivo: entender carga, recuperação, hidratação, intensidade e evolução do atleta usando os batimentos reais do sensor BLE somados a dados simples informados pelo usuário.
Usuário principal: lutador ou praticante de treino de combate que usa o FightRate para acompanhar batimentos durante treinos, rounds e sessões.
## Scope P0
Implementar somente o núcleo inicial:
1. Perfil do Lutador.
2. Check-in pré-treino.
3. Registro pós-treino.
4. Insights básicos do treino e recuperação.
Não implementar neste P0:
- exames médicos avançados;
- integração com wearable externo além do sensor BLE atual;
- Health Connect;
- prescrição médica;
- VO2max avançado;
- IA generativa para recomendação clínica;
- dashboard muito complexo.
## Assumptions
1. O app continua Android nativo Kotlin + Jetpack Compose.
2. Textos visíveis continuam em PT-BR.
3. Identificadores de código continuam em inglês.
4. SQLite local-first continua como fonte de verdade.
5. O sensor BLE continua sendo fonte dos batimentos em tempo real.
6. Cálculos de saúde serão apresentados como estimativas esportivas, não diagnósticos.
7. O P0 deve priorizar campos que geram cálculos imediatos.
## Tech Stack
- Kotlin
- Jetpack Compose Material3
- SQLite via `SQLiteOpenHelper` existente em `HeartRateSampleStore`
- Gradle Kotlin DSL
- JUnit 4 para testes unitários
- Vico para gráficos existentes
- Lucide para ícones existentes
## Commands
Build completo:
```bash
./gradlew :app:testDebugUnitTest :app:assembleDebug
```
Teste unitário focado:
```bash
./gradlew :app:testDebugUnitTest --tests "*AthleteInsights*"
```
Compilação rápida:
```bash
./gradlew :app:compileDebugKotlin
```
Instalação debug:
```bash
./gradlew :app:installDebug
```
## Existing Code to Reuse
Reaproveitar antes de criar novo:
- `AthleteProfile.kt`
- `HeartRateZones.kt`
- `WorkoutSession.kt`
- `HeartRateStats.kt`
- `HeartRateSampleStore.kt`
- `LiveHeartRateViewModel.kt`
- `LiveHeartRateUiState.kt`
- `HomeScreen.kt`
- `FightCard`
- `MetricTile`
- `LiveMetricPill`
- cards de Perfil existentes
- SQLite `fightrate.db`
## Data Model — P0 Fields
### 1. Perfil básico
Campos:
- `age`
- `biologicalSex`
- `heightCm`
- `weightKg`
- `fightSport`
- `weightClassKg`
- `trainingGoal`
Uso:
- personalizar zonas;
- calcular IMC;
- acompanhar categoria de peso;
- contextualizar insights.
### 2. Perfil cardiovascular
Campos:
- `restingHeartRate`
- `measuredMaxHeartRate`
Uso:
- limite cardíaco personalizado;
- zonas por percentual;
- reserva cardíaca;
- alertas de esforço.
### 3. Check-in pré-treino
Campos por sessão:
- `preWorkoutEnergy` escala 1–5
- `preWorkoutFatigue` escala 1–5
- `muscleSoreness` escala 1–5
- `sleepHours`
- `sleepQuality` escala 1–5
- `preWorkoutWeightKg`
- `waterIntakeMl`
- `symptomsNote`
Uso:
- prontidão;
- risco de fadiga;
- contexto para batimentos altos;
- hidratação estimada.
### 4. Pós-treino
Campos por sessão:
- `postWorkoutRpe` escala 1–10
- `postWorkoutFatigue` escala 1–5
- `postWorkoutSoreness` escala 1–5
- `postWorkoutWeightKg`
- `postWorkoutNote`
Uso:
- carga subjetiva;
- perda hídrica;
- recuperação;
- comparação entre percepção e batimentos.
## Calculations — P0
### 1. IMC
Entrada:
- peso
- altura
Resultado:
- `bmi`
- faixa textual simples
### 2. Reserva cardíaca
Entrada:
- limite cardíaco real ou estimado
- FC repouso
Resultado:
- `heartRateReserve`
### 3. Zonas por Karvonen
Entrada:
- limite cardíaco
- FC repouso
Resultado:
- zonas mais personalizadas para o atleta.
### 4. Score de treino
Entrada:
- duração da sessão
- BPM médio
- BPM pico
- tempo em zonas
- RPE pós-treino
Resultado:
- score 0–100
- rótulo: leve, moderado, intenso, extremo
### 5. Carga subjetiva
Entrada:
- RPE pós-treino
- duração em minutos
Resultado:
- `sessionLoad = RPE × durationMinutes`
### 6. Score de recuperação
Entrada:
- sono
- qualidade do sono
- energia antes
- fadiga antes
- dor muscular
- FC repouso opcional
Resultado:
- score 0–100
- rótulo: baixa, ok, boa, excelente
### 7. Hidratação estimada
Entrada:
- peso antes
- peso depois
- água ingerida
Resultado:
- perda de peso no treino
- perda hídrica estimada
- alerta se perda relativa for alta
### 8. Tempo em zona de luta
Entrada:
- samples de BPM da sessão
- zonas cardíacas
Resultado:
- tempo em zona alta/intensa
- percentual do treino em zona de luta
### 9. Alertas de segurança esportiva
Entrada:
- sintomas
- fadiga alta
- sono ruim
- dor alta
- esforço cardíaco alto
Resultado:
- mensagens de atenção no app.
Exemplos:
- “Sono baixo + fadiga alta: considere treino técnico ou leve.”
- “Perda de peso alta no treino: reponha líquidos.”
- “Esforço muito alto para uma recuperação baixa.”
## UI/UX
### Tela Perfil
Adicionar seções recolhíveis ou cards:
1. Básico
2. Cardiovascular
3. Luta
Evitar formulário gigante em uma tela só.
### Tela Check-in
Fluxo simples antes de iniciar sessão:
- energia
- fadiga
- dor
- sono
- peso
- água
- sintomas
Pode ser card ou bottom sheet antes de iniciar treino.
### Tela Pós-treino
Após encerrar sessão:
- RPE
- fadiga
- dor
- peso pós
- observação
### Tela Insights do Lutador
Primeira versão com cards:
- Score do treino
- Recuperação
- Hidratação
- Tempo em zona de luta
- Carga subjetiva
- Alertas
## Project Structure
Novos arquivos prováveis:
```text
app/src/main/java/com/healthtracking/fightrate/domain/AthleteInsights.kt
app/src/main/java/com/healthtracking/fightrate/domain/ReadinessScore.kt
app/src/main/java/com/healthtracking/fightrate/domain/HydrationEstimate.kt
app/src/test/java/com/healthtracking/fightrate/domain/AthleteInsightsTest.kt
```
Arquivos existentes a alterar:
```text
AthleteProfile.kt
WorkoutSession.kt
HeartRateSampleStore.kt
LiveHeartRateUiState.kt
LiveHeartRateViewModel.kt
HomeScreen.kt
```
A criação de novos arquivos deve ser mínima e justificada por responsabilidade clara.
## Code Style
Exemplo esperado:
```kotlin
data class ReadinessInput(
val sleepHours: Double?,
val sleepQuality: Int?,
val energy: Int?,
val fatigue: Int?,
val soreness: Int?,
)
object ReadinessScoreCalculator {
fun calculate(input: ReadinessInput): ReadinessScore {
val score = 100
.minus(input.fatigue.orDefaultPenalty(multiplier = 8))
.minus(input.soreness.orDefaultPenalty(multiplier = 6))
.plus(input.energy.orDefaultBonus(multiplier = 5))
.coerceIn(0, 100)
return ReadinessScore(value = score)
}
}
```
Convenções:
- código em inglês;
- UI em PT-BR;
- cálculo puro no domínio;
- Compose apenas renderiza estado;
- evitar mágica sem teste.
## Testing Strategy
Testar com JUnit:
- IMC
- reserva cardíaca
- Karvonen
- score de recuperação
- carga subjetiva
- hidratação estimada
- tempo em zona de luta
- alertas esportivos
Comandos:
```bash
./gradlew :app:testDebugUnitTest --tests "*AthleteInsights*"
./gradlew :app:testDebugUnitTest :app:assembleDebug
```
## Boundaries
### Always
- Criar cálculo no domínio antes da UI.
- Usar testes para cálculo.
- Manter campos opcionais quando possível.
- Explicar que cálculos são estimativas esportivas.
- Reaproveitar `AthleteProfile`, `WorkoutSession`, `HeartRateSampleStore`.
- Salvar dados localmente.
### Ask first
- Adicionar dependência nova.
- Adicionar Health Connect.
- Criar tela nova separada se puder ser card inicialmente.
- Fazer alteração destrutiva no banco.
- Criar cálculo médico sensível.
- Coletar dados muito pessoais sem justificativa.
### Never
- Apresentar diagnóstico médico.
- Bloquear treino como decisão médica.
- Criar formulário gigante difícil de usar.
- Adicionar campos que não alimentam nenhum cálculo.
- Criar código morto para métricas futuras.
- Misturar todos os cálculos dentro da UI.
## Success Criteria
- Usuário consegue preencher dados P0 sem fricção excessiva.
- App calcula pelo menos:
- IMC;
- reserva cardíaca;
- score de treino;
- carga subjetiva;
- score de recuperação;
- hidratação estimada;
- tempo em zona de luta.
- Dados ficam persistidos localmente.
- Insights aparecem em PT-BR.
- Cálculos puros têm testes.
- Build final passa.
- Nenhuma dependência nova é adicionada sem aprovação.
## Decisions Approved
1. Check-in pré-treino será obrigatório antes de iniciar uma sessão.
2. Insights será uma nova aba no menu inferior.
3. Sexo biológico será coletado como `Masculino` ou `Feminino`.
4. Energia, fadiga, dor e sono usarão escala 0–10.
5. Campos avançados devem poder ficar ocultos/expandidos para manter a interface limpa.
## BLE Reliability and Battery Plan
### Objective
A leitura do sensor deve continuar enquanto o app estiver em treino, inclusive com tela bloqueada, salvo quando:
1. o usuário desconectar manualmente;
2. o usuário encerrar o treino;
3. o Android remover o processo por condição extrema fora do controle do app.
Quando houver queda por distância/sinal, o serviço deve tentar reconectar automaticamente quando o usuário se aproximar novamente. Se o usuário desconectar manualmente, o app não deve reconectar sozinho.
### BLE Rules
- O `HeartRateForegroundService` continua sendo dono da conexão BLE.
- O ViewModel não deve abrir conexão direta com o sensor.
- Toda queda inesperada deve ser registrada como estado de conexão e mensagem visível.
- Desconexão manual deve salvar uma flag `manualDisconnect = true`.
- Queda inesperada deve salvar `manualDisconnect = false` e iniciar retry/backoff.
- O último sensor selecionado deve ser preservado para reconexão automática.
### Reconnect Strategy
Estados sugeridos:
```text
Idle
Connecting
Connected
LostSignal
RetryWaiting
ManualDisconnected
Failed
```
Backoff conservador:
```text
1ª tentativa: 2s
2ª tentativa: 5s
3ª tentativa: 10s
4ª tentativa em diante: 30s
```
Regras:
- Não tentar reconectar se `manualDisconnect = true`.
- Não tentar reconectar se Bluetooth estiver desligado; aguardar próximo ciclo.
- Não tentar reconectar sem permissão Bluetooth.
- Não fazer scan agressivo contínuo.
- Para sensor já conhecido, tentar reconectar por `deviceId` direto antes de scan.
- Scan só deve ser usado quando não houver deviceId válido.
### Battery Strategy
Para menor consumo:
- Foreground service único durante sessão.
- Sem polling pesado no ViewModel a cada batimento.
- SQLite insert apenas por amostra recebida.
- Notificação com throttle.
- Reconexão com backoff, nunca loop apertado.
- Metadata do sensor lida na conexão e depois em intervalo longo, não a cada amostra.
- Gráficos atualizam pelo estado recebido, sem reconsultar o banco inteiro.
- Evitar wake locks permanentes. Usar foreground service e orientação de ignorar otimização de bateria.
### Acceptance BLE
- Tela bloqueada não interrompe leitura durante treino.
- Distância/sinal perdido mostra status de sensor perdido.
- Ao aproximar novamente, o app tenta reconectar sozinho.
- Se usuário tocar em Desconectar, reconexão automática fica desativada.
- Se usuário escolher o sensor/iniciar novamente, reconexão automática volta a valer.
- Build passa.
## Recommended First Implementation Plan
Após aprovação desta spec:
1. Criar domínio puro de insights e testes.
2. Expandir modelo de perfil e sessão com campos P0.
3. Adicionar migração SQLite aditiva.
4. Criar UI de Perfil Avançado com seções expansíveis.
5. Criar check-in pré-treino obrigatório antes de iniciar sessão.
6. Criar pós-treino ao encerrar sessão.
7. Criar nova aba Insights com cards básicos.
8. Melhorar BLE com desconexão manual vs queda inesperada, reconexão automática e backoff.
9. Rodar build/test completo.
---
# Spec Expansion: Gráficos, Janela de Treino e Relatório Científico
## Objective
Transformar a sessão do FightRate em uma janela de coleta confiável com relatório pós-treino detalhado, gráficos Vico mais legíveis/interativos e métricas esportivas fundamentadas em frequência cardíaca real do sensor BLE.
O objetivo é interpretação esportiva para lutas, não diagnóstico médico. Toda métrica estimada deve ser rotulada como estimativa.
## Research Summary
Fontes consultadas antes da implementação:
1. Cole et al., NEJM 1999, PubMed: `https://pubmed.ncbi.nlm.nih.gov/10536127/`
- HR Recovery é queda da FC do pico para 1 minuto pós-exercício.
- Queda <= 12 bpm em 1 min foi considerada anormal no estudo clínico.
2. Brubaker & Kitzman, Circulation review, PMC: `https://pmc.ncbi.nlm.nih.gov/articles/PMC3065291/`
- Resposta cronotrópica é capacidade de aumentar FC com demanda.
- Critérios clínicos exigem teste progressivo e contexto; no app usar só interpretação esportiva.
- HRR 1 min < 12 bpm, 2 min < 42 bpm aparecem como thresholds de risco em contexto clínico, mas o app não diagnostica.
3. Uth et al., Eur J Appl Physiol 2004, PubMed: `https://pubmed.ncbi.nlm.nih.gov/14624296/`
- VO2max estimado pode usar `15.3 * (HRmax / HRrest)` em população treinada.
- Aplicabilidade fora desse perfil deve ser tratada como aproximação.
4. Boxing HR response, PMC: `https://pmc.ncbi.nlm.nih.gov/articles/PMC3871409/`
- Boxe simulado passou grande parte acima de VT2; HR aumenta por round.
- Reforça foco em tempo em zonas altas, recuperação entre rounds e drift.
5. Muay Thai physiological response, Ingenta: `https://www.ingentaconnect.com/content/uwic/ujpa/2012/00000012/00000003/art00004`
- Muay Thai competitivo teve FC média próxima do máximo e alto estresse cardiovascular.
6. Edwards/TRIMP summaries: `https://www.trainingimpulse.com/edwards-trimp` e `https://journals.lww.com/nsca-scj/fulltext/2018/04000/quantification_of_training_load_in_endurance.12.aspx`
- Edwards TRIMP é prático por zonas, mas coeficientes são arbitrários.
- Banister usa reserva cardíaca e ponderação exponencial; ambos devem ser apresentados como carga interna estimada.
7. HRV em Muay Thai, PMC: `https://pmc.ncbi.nlm.nih.gov/articles/PMC12143277/`
- RMSSD/SDNN exigem RR/NN intervals confiáveis. O app hoje coleta BPM, portanto HRV fica fora do escopo até o parser/persistência salvarem RR intervals.
## Implementable Metrics From Current Data
Viáveis agora:
- duração total da sessão;
- duração ativa aproximada e pausada;
- BPM inicial/final/mínimo/médio/máximo;
- desvio padrão;
- qualidade dos dados por densidade, gaps e duração;
- tempo e percentual por zona;
- tempo em zona de luta, RedLine e Limite;
- maior sequência contínua em zona intensa;
- picos e quedas relevantes;
- HR Recovery 1min/2min quando houver amostras pós-pico;
- drift primeira metade vs segunda metade;
- slope de BPM por minuto;
- resposta cronotrópica inicial aproximada;
- tempo até zona vigorosa/luta;
- Edwards TRIMP;
- Banister TRIMP quando houver FC repouso e limite cardíaco;
- carga subjetiva RPE x duração;
- VO2max estimado quando houver FC repouso e limite cardíaco;
- hidratação estimada com peso pré/pós e água;
- interpretação cruzada com energy/fatigue/soreness/sleep/symptoms/RPE.
Não viável agora sem novos dados:
- HRV RMSSD/SDNN/pNN50 real;
- diagnóstico de arritmia;
- limiar ventilatório real;
- VO2 real;
- economia cardíaca precisa entre sessões sem normalizar tipo/duração/intensidade.
## Data Collection / Time Window
Reaproveitar `workout_sessions`:
- `startedAtMillis` define início do range.
- `endedAtMillis` define fim do range.
- `session_id` em `heart_rate_samples` associa amostra à sessão ativa.
- Ao finalizar, buscar amostras por `session_id`; fallback por intervalo se necessário.
- Foreground service continua dono do BLE.
- ViewModel não assume BLE.
- Migração SQLite só aditiva se relatório persistido exigir cache.
## Domain Design
Criar ou expandir domínio puro para relatório:
- `TrainingReportMetrics`
- `SessionReportBuilder`
- `DataQualityAnalyzer`
- `ZoneDistributionCalculator`
- `HeartRateRecoveryCalculator`
- `CardiacDriftCalculator`
- `ChronotropicResponseCalculator`
- `TrainingLoadCalculator`
- `HydrationTrainingInterpreter`
Business rules fora do Compose.
## UI / Charts
Reaproveitar `HeartRateLineChart`, `FightCard`, `MetricTile`.
Adicionar:
- chart summary pills de mínimo/médio/máximo;
- legenda de zonas;
- destaque textual para tempo em zona de luta/limite;
- estados profissionais para poucos dados;
- leitura de eixo X por tempo real;
- relatório em seções expansíveis:
1. Resumo do treino;
2. Gráfico principal;
3. Zonas cardíacas;
4. Recuperação;
5. Carga de treino;
6. Drift e fadiga;
7. Hidratação;
8. Comparação com histórico;
9. Alertas e interpretação.
Evitar Canvas customizado e dependências novas.
## Success Criteria
- Spec atualizada antes do código.
- Código existente auditado e reaproveitado.
- Testes JUnit cobrem todos os cálculos de domínio novos.
- Sessão finalizada gera relatório com amostras do range correto.
- Histórico permite abrir relatório de sessão.
- UI deixa claro estimativa esportiva vs dado medido.
- BLE continua no foreground service.
- Sem migração destrutiva.
- `./gradlew :app:testDebugUnitTest :app:assembleDebug` termina com `BUILD SUCCESSFUL`.
- APK gerado e tentativa de `telegram_attach(localPath)` realizada.
@@ -0,0 +1,27 @@
# FightRate RN — Background Capture + Corner Instrument Redesign
## Objective
Make the bare React Native Android app capture heart-rate data continuously until the athlete manually stops the workout, including when the screen is locked or the app is backgrounded. Replace foreground-JS BLE ownership and generic card UI with native Android capture ownership, local SQLite persistence, and Corner Instrument screens.
## Success Criteria
- Capture is started/stopped from React Native, but BLE scan/connect/monitor runs in an Android foreground service.
- Android manifest declares Bluetooth, notification, foreground service, and battery optimization permissions needed for continuous capture.
- The foreground service repeats scan/reconnect attempts unless stopped manually.
- Heart Rate Service `180D` and Heart Rate Measurement `2A37` are used for real BLE BPM data.
- Each BPM sample is timestamped and persisted locally in SQLite.
- History and insights use persisted real sessions/samples; no mock BPM/history/insight data is displayed.
- UI follows `FIGHTRATE_DESIGN_SYSTEM.md` / Corner Instrument: dark physical instrument surfaces, dominant BPM, technical strips, no nested generic card dashboard.
- Release APK builds as an installable arm64 artifact and is sent to Telegram.
## Boundaries
- React Native bare CLI only; no Expo.
- Android-first implementation; iOS background BLE is out of scope.
- Physical Magene H003 validation remains required on device.
- Manual destructive deletion of non-design project files is avoided.
## Verification
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `cd FightRateRN/android && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,86 @@
# FightRate — Redesign de gráficos e sistema visual
## Objetivo
Corrigir a direção visual do FightRate: gráficos profissionais, úteis e interativos; interface menos genérica/card-heavy; design system original e obrigatório para próximas telas.
## Problemas atuais
- Gráficos aparecem mesmo sem dados úteis.
- Estados vazios parecem gráficos quebrados/amadores.
- Vico está subutilizado: pouca interação, pouco contexto visual, sem zoom/scroll claro.
- UI tem excesso de cards iguais, pouca hierarquia e pouco padrão visual.
- O aplicativo parece montado por IA, não por um produto esportivo premium.
## Decisões
- Manter Vico. Pesquisa em 2026-06 mostra Vico como biblioteca Compose/Kotlin madura com suporte a `scrollState` e `zoomState`.
- Não trocar dependências nesta fase.
- Não desenhar chart customizado em Canvas.
- Não renderizar gráfico se houver menos de 2 pontos reais.
- Se houver poucos dados, mostrar insight/empty state, não eixos vazios.
- O eixo temporal deve continuar baseado em timestamps reais.
- O design system passa a ser a fonte visual do app.
## Design System: FightRate Combat Lab
### Conceito
Interface de laboratório de performance para lutas: escura, precisa, atlética, com sensação de equipamento esportivo premium. Menos cartões genéricos, mais painéis editoriais, faixas de dados, chips compactos, e superfícies com função clara.
### Tokens principais
- Fundo: `#090A0B` — preto quente de OLED.
- Surface: `#111315` — bloco primário.
- Surface Elevated: `#181B1E` — módulo de leitura.
- Ink: `#F6F0E7` — texto principal quente.
- Muted: `#A79C8F` — texto secundário.
- Dim: `#6E665D` — metadados.
- Accent: `#FF6A1A` — FightRate Orange.
- Amber: `#F6B44B` — atenção.
- Redline: `#FF3B30` — limite/risco.
- Cyan: `#48C7D8` — recuperação/ciência.
- Line: `#2A2E32` — divisores sutis.
### Forma
- Telas: blocos editoriais com seções claras, não grade de cards repetidos.
- Painéis principais: radius 28–32 dp, borda hairline, padding 20–24 dp.
- Métricas pequenas: strips/pills, não cards grandes.
- Gráfico: painel amplo, prioridade visual máxima, legenda compacta.
- Empty state: peça editorial com próxima ação, nunca gráfico vazio.
### Tipografia
- Números grandes, tabulares quando possível.
- Títulos curtos, fortes, em PT-BR.
- Corpo pequeno, mas legível. Nada abaixo de 10sp para informação essencial.
- Evitar muitos pesos/tamanhos diferentes.
### Gráficos
- Mostrar gráfico somente se `samples.size >= 2` e duração > 0.
- Para 1 ponto: mostrar leitura única e instrução de continuar medindo.
- Para 0 pontos: mostrar estado vazio contextual.
- Linha com espessura alta, cor FightRate Orange, área/ambiente escuro.
- Scroll/zoom habilitado via Vico quando possível.
- Controles explícitos de janela/zoom fora do gráfico.
- Resumo sempre visível: média, pico, mínima, duração, densidade de amostras.
- Não usar índices como tempo; usar segundos/minutos reais desde o início da janela.
## Fatias de implementação
1. Consolidar tokens em tema/design system.
2. Criar componentes visuais base: `FightSection`, `FightMetricStrip`, `FightEmptyState`, `FightChartPanel`.
3. Atualizar `HeartRateLineChart` para não renderizar vazio e usar zoom/scroll Vico.
4. Reorganizar Home/Histórico para priorizar gráfico e dados úteis.
5. Atualizar documentação e roadmap.
6. Rodar testes/build e gerar APK.
## Critérios de sucesso
- Nenhum gráfico vazio é renderizado.
- Gráfico com dados tem zoom/scroll ou controle equivalente claro.
- Histórico sem dados mostra estado vazio premium.
- UI usa tokens e componentes do design system.
- Build final passa com `./gradlew :app:testDebugUnitTest :app:assembleDebug`.
@@ -0,0 +1,41 @@
# FightRate — Correção de produto: captura contínua, gráfico e sensor
## Contexto
Feedback do usuário: o app serve para monitorar batimento cardíaco enquanto o atleta treina. Durante luta/treino, o usuário não vai pegar o celular para iniciar/parar rounds manualmente.
## Objetivos
1. Tornar captura contínua de treino inteiro o fluxo principal.
2. Manter modo round como secundário/opcional, sem dominar a tela principal.
3. Corrigir gráfico para abrir com a janela selecionada completa, sem zoom inicial estranho.
4. Tornar eixo temporal adaptativo e menos poluído.
5. Melhorar tela de sensor para exibir dados/serviços disponíveis do dispositivo.
6. Melhorar insights com informação densa, útil e explicável sem textão.
## Regras de produto
- Fluxo principal: iniciar monitoramento antes do treino, largar celular, parar ao final, analisar sessão inteira.
- Round é alternativa opcional. Preferir segmentação automática pré-configurada em vez de controles manuais durante treino.
- Histórico precisa ser rico e organizado: sessões, janela, zonas, carga, recuperação, pico, média, densidade de amostras.
- Gráfico deve priorizar legibilidade e fidelidade temporal.
## Critérios de aceitação desta fatia
- Tela Live comunica “captura contínua” como ação principal.
- Botões de sessão usam linguagem de treino inteiro: iniciar/parar captura/treino.
- Round continua disponível/visível como informação secundária, mas não como conceito central.
- Gráfico inicia mostrando a janela de dados selecionada, sem pre-zoom e com `initialZoom = null` quando possível.
- Labels do eixo X usam granularidade por duração:
- até 10 min: HH:mm
- até 1 h: HH:mm
- até 24 h: HH'h'
- zoom/duração muito curta: HH:mm:ss apenas quando necessário.
- Tela Sensor exibe painel de capacidades do sensor: BPM, local do sensor, bateria, metadados, RSSI e serviços BLE conhecidos/disponíveis.
- Build final passa com `./gradlew :app:testDebugUnitTest :app:assembleDebug`.
## Fora desta fatia
- Implementar segmentação automática completa por round.
- Persistir todos os valores de serviços BLE desconhecidos.
- Reescrever toda a arquitetura visual do app.
@@ -0,0 +1,69 @@
# FightRate — monitoramento contínuo, histórico customizado e indicadores BPM
## Objetivo
Corrigir o fluxo para que o FightRate se comporte como um monitor contínuo de sensor cardíaco, e não como um app dependente da janela de treino. A janela de treino deve apenas destacar um intervalo dentro do fluxo contínuo. Também adicionar consulta histórica customizada por data/hora/minuto, estabilizar a UI do histórico e documentar indicadores úteis derivados de BPM.
## Problemas relatados
1. BPM em tempo real só aparece depois de abrir e fechar uma janela de treino.
2. Histórico precisa consultar intervalo preciso por dia, hora e minuto, inclusive múltiplos dias.
3. Histórico tem divisores redundantes entre controles, janelas salvas e registros.
4. Em “Uso Total”, o item expandido colapsa quando entra novo dado em tempo real.
5. É necessário mapear indicadores técnicos/científicos extraíveis de BPM para evolução futura.
## Critérios de sucesso
- Ao abrir o app, se o monitor estava ativo ou há sensor salvo, o app retoma o monitoramento contínuo automaticamente.
- Após conectar ao sensor peitoral, os dados começam a atualizar o app inteiro sem depender de janela de treino.
- Abrir/fechar janela de treino não interrompe nem inicia artificialmente o stream contínuo; só marca um intervalo para histórico.
- Histórico permite modo “Faixa” com início/fim definidos por dia, hora e minuto.
- Faixa customizada funciona no mesmo dia e entre dias diferentes.
- Uso Total mantém o item expandido mesmo quando novas amostras chegam em segundo plano.
- Divisores do histórico ficam lineares: uma linha sutil por seção/registro, sem duplicação visual.
- Pesquisa de indicadores BPM gera relatório com fórmulas, dados necessários, limites e recomendações.
- Sem dados fake; gráficos e métricas continuam derivados de amostras reais.
## Arquitetura proposta
### Monitoramento contínuo
- Manter BLE e background capture no serviço nativo Android.
- No hook RN, diferenciar claramente:
- `isMonitoring`: stream contínuo ativo.
- `isCapturing`: janela de treino ativa.
- `samples`: amostras contínuas em memória recente.
- Na inicialização do hook, depois de `getCaptureSnapshot`, se o snapshot indicar monitor ativo/sensor salvo, retomar/confirmar `startMonitoring` sem abrir janela.
### Histórico customizado
- Criar lógica pura para filtrar amostras por intervalo `[start, end]`.
- Usar a mesma fonte de verdade (`continuous_heart_rate_samples`) para “Uso Total” e faixa customizada.
- Representar a seleção com componentes simples sem dependência nova: campos de data e hora/minuto via `TextInput`, com validação e presets.
### Estabilidade do Uso Total
- Não usar ID derivado de `startedAt-endedAt-count` como única identidade quando blocos vivos mudam com novas amostras.
- Manter expansão por `startedAt` do bloco e re-resolver o bloco depois de cada atualização.
### Indicadores BPM
- Produzir documento separado com métricas implementáveis depois:
- FC média/mínima/máxima;
- zonas de intensidade;
- tempo em zona;
- carga interna/TRIMP;
- recuperação pós-esforço;
- deriva cardiovascular;
- variabilidade de FC quando houver RR intervals;
- alertas de limiar e anomalias.
## Fora de escopo
- Trocar BLE stack.
- Adicionar dependências nativas de calendário/data.
- Alterar design aprovado fora dos pontos pedidos.
- Criar diagnóstico médico automatizado sem validação clínica.
## Testes esperados
- Testes unitários para agrupamento de uso contínuo estável.
- Testes unitários para filtro de intervalo customizado.
- Testes existentes continuam passando.
## Verificação
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,25 @@
# FightRate RN — Continuous Monitoring + Training Windows
## Objective
Change FightRate so BLE heart-rate monitoring is continuous and locally persisted, while a workout/training session is only a marked time window inside that continuous stream. Starting a workout begins a highlighted window; stopping it closes that window and keeps monitoring alive.
## Success Criteria
- The native Android foreground service can start monitoring without creating a workout window.
- Samples are saved continuously even when no workout window is active.
- Start/stop workout only opens/closes a `workout_sessions` window; it does not stop BLE monitoring.
- History lists saved windows and each item can be opened to inspect details from samples inside that window.
- Insight and history screens use only real timestamped samples.
- Capture/History/Insights UI removes generic card-heavy containers and uses Corner Instrument rails, strips, dividers, and instrument surfaces.
- Existing bare React Native CLI constraint remains; no Expo.
## Boundaries
- No new dependencies unless unavoidable.
- Do not delete existing tables destructively; migrate by adding a continuous sample table and deriving session summaries from timestamps.
- Android physical BLE validation remains required after APK install.
## Verification
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `cd FightRateRN/android && ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,81 @@
# FightRate Insights Científicos — Mini-spec
## Objetivo
Implementar uma nova visualização de cálculos e insights na aba `Insights`, usando métricas derivadas do relatório científico `docs/research/bpm-indicators-fightrate-2026-06-10.md`, sem inventar dados e sem abandonar o design Corner Instrument.
## Escopo
### Incluir agora
- Grade/tabela compacta de métricas com melhor aproveitamento vertical.
- Cálculos com dados reais já disponíveis:
- FC atual, média, mínima e máxima;
- duração observada e contagem de amostras;
- amplitude de BPM;
- desvio padrão de BPM;
- coeficiente de variação de BPM;
- classificação operacional de estabilidade/oscilação;
- tempo em zonas já calculadas;
- carga observada existente;
- tendência de deriva de FC quando a janela for longa o suficiente;
- recuperação pós-janela quando houver amostras contínuas após o fim da sessão;
- status de HRV/VFC baseado na presença de `rrIntervalsMs`.
- Estados honestos para métricas que exigem perfil ou RR intervals.
### Não incluir agora
- Cadastro de perfil do atleta.
- Diagnóstico médico.
- LF/HF, frequência espectral, SampEn ou DFA reais sem pipeline de RR validado.
- Mudanças amplas de design fora de `Insights`.
- Novas dependências.
## Arquitetura
- Reutilizar `src/training/trainingMetrics.ts` para cálculos puros.
- Estender `src/training/trainingTypes.ts` com tipos de resumo de insights.
- Atualizar `src/screens/InsightsScreen.tsx` para renderizar uma tabela/grade de instrumentos, não lista simples.
- Atualizar `src/app/FightRateApp.tsx` para passar `getContinuousSamples` ao Insights, permitindo cálculo de recuperação pós-janela com dados persistidos.
- Testar cálculos puros em `__tests__/trainingMetrics.test.ts`.
## Critérios de sucesso
- Insights usa somente amostras reais ou mostra estado indisponível.
- Visual evita cards genéricos; usa trilhos, linhas tabulares, separadores e linguagem Corner Instrument.
- Métricas BPM-only aparecem com valor/unidade/status/leitura curta.
- HRV/VFC aparece como disponível somente quando existirem RR intervals; caso contrário, informa que requer RR.
- Recuperação pós-janela só aparece quando há amostras depois de `endedAt`.
- Typecheck, lint e testes passam.
- Build release Android é gerado e enviado ao Telegram ao final.
## Reuso vs criação
### Reutilizar
- `calculateLiveMetrics`, `calculateZones`, `formatDuration` em `trainingMetrics.ts`.
- `HeartRateSample`, `CompletedTrainingSession` em `trainingTypes.ts`.
- Tokens existentes em `src/theme`.
- Fluxo nativo `getContinuousSamples` já exposto pelo hook.
### Criar/alterar
- Criar funções puras em `trainingMetrics.ts`:
- `buildInsightDashboard()`;
- `calculateBpmVariability()`;
- `calculateHeartRateDrift()`;
- `calculatePostSessionRecovery()`;
- `summarizeRrAvailability()`.
- Criar tipos em `trainingTypes.ts`:
- `InsightMetricStatus`;
- `InsightMetricRow`;
- `InsightDashboard`.
- Reescrever o layout interno de `InsightsScreen.tsx` para matriz tabular.
## Plano de execução
1. Escrever testes para cálculos puros.
2. Implementar funções puras em `trainingMetrics.ts`.
3. Passar `getContinuousSamples` para `InsightsScreen` e carregar amostras persistidas de forma segura.
4. Recriar UI da aba Insights com tabela compacta e blocos de instrumentos.
5. Rodar validação completa.
@@ -0,0 +1,108 @@
# FightRate React Native Bare Rebuild — Mini-spec
Data: 2026-06-10
Status: implementação autorizada
Stack: **React Native Community CLI, sem Expo**
## Objetivo
Criar a nova base do FightRate em React Native bare, sem Expo, usando a direção visual oficial **Corner Instrument**.
A implementação inicial deve provar:
- identidade visual escolhida;
- fluxo principal de captura contínua;
- estrutura de código React Native escalável;
- telas base para Captura, Histórico, Sensor e Insights;
- dados mockados apenas como estado de protótipo local, não como gráfico ou amostra real persistida.
## Fora de escopo nesta primeira fatia
- BLE real Magene H003.
- SQLite real.
- Build de APK final assinado.
- Migração de todos os recursos do Android legado.
- Expo ou qualquer dependência Expo.
## Decisões
- Criar novo app em `FightRateRN/` para não destruir o Android legado antes da base RN compilar.
- Usar React Native Community CLI.
- Usar TypeScript.
- Começar com UI e estado local; BLE/persistência entram em fatias posteriores.
- Fonte visual obrigatória: `FIGHTRATE_DESIGN_SYSTEM.md`.
## Estrutura desejada
```text
FightRateRN/
src/
app/
FightRateApp.tsx
components/
ActionButton.tsx
MetricTile.tsx
SectionPanel.tsx
StatusTag.tsx
TrainingChart.tsx
screens/
CaptureScreen.tsx
HistoryScreen.tsx
SensorScreen.tsx
InsightsScreen.tsx
theme/
colors.ts
radius.ts
spacing.ts
typography.ts
index.ts
data/
demoTraining.ts
```
## Critérios de aceite
1. Projeto React Native bare criado sem Expo.
2. `package.json` não contém dependências Expo.
3. App inicial mostra a direção Corner Instrument.
4. Captura contínua é o fluxo principal.
5. Round manual não domina a interface.
6. Design tokens estão centralizados em `src/theme`.
7. Componentes principais reutilizáveis existem.
8. Telas visíveis:
- Captura;
- Histórico;
- Sensor;
- Insights.
9. Verificação mínima:
```bash
cd FightRateRN
npm install
npm run typecheck
npm run lint
npm test -- --runInBand
```
Build Android debug validado neste ambiente:
```bash
cd FightRateRN/android
./gradlew assembleDebug
```
Observação local: o PATH atual tem JRE, mas não `javac`. Para o build Gradle RN,
use o JBR do Android Studio como `JAVA_HOME`:
```bash
export JAVA_HOME="/c/Users/Evandro/AppData/Local/Programs/Android Studio/jbr"
export PATH="$JAVA_HOME/bin:$PATH"
cd FightRateRN/android
./gradlew assembleDebug
```
## Riscos
- React Native CLI pode mudar template conforme versão atual.
- Ambiente local pode não ter Android SDK/NDK adequado para build RN.
- BLE real exigirá dependência nativa futura e teste físico.
@@ -0,0 +1,93 @@
# FightRate RN — Real BLE / No Mocks Corrective Slice
Data: 2026-06-10
Status: implementado / APK enviado ao Telegram
Stack: React Native Community CLI bare, sem Expo
## Problema
A primeira versão React Native enviada era instalável, mas não funcional como app de treino real:
- usava dados de demonstração;
- não escaneava BLE;
- indicava estados como conectado/pronto sem sensor real;
- gráficos não eram alimentados por amostras reais;
- histórico e insights pareciam conteúdo estático;
- navegação inferior estava visualmente fraca;
- logo/ícone do projeto não aparecia na UI.
## Objetivo desta fatia
Transformar a versão RN em uma base real de captura:
1. Remover dados mockados da UI.
2. Adicionar BLE real com `react-native-ble-plx`.
3. Pedir permissões Android Bluetooth/localização.
4. Escanear sensores com Heart Rate Service `180D`.
5. Conectar ao sensor encontrado.
6. Monitorar Heart Rate Measurement `2A37`.
7. Parsear BPM real a partir do payload BLE.
8. Atualizar BPM, média, pico e amostras em tempo real.
9. Renderizar gráfico somente com amostras reais recebidas.
10. Exibir histórico/insights como estados vazios honestos quando não houver sessão real.
11. Melhorar bottom nav: ícones, barra flutuante, sem faixa preta atrás.
12. Remover indicador `READY` do topo.
13. Usar logo existente do projeto na UI.
14. Gerar APK release e enviar ao Telegram.
## Dependências aprovadas pela necessidade do pedido
- `react-native-ble-plx`: BLE real no React Native bare.
- `react-native-svg`: gráfico real leve e ícones SVG.
- `lucide-react-native`: ícones para navegação/estado.
## Critérios de aceite
- App não mostra BPM mockado.
- Tela de captura mostra estado de permissão/scan/conexão real.
- Botão de captura dispara permissões e scanning BLE.
- Se nenhum sensor for encontrado, a UI diz isso claramente.
- Se sensor conectar, BPM vem do characteristic `2A37`.
- Gráfico só aparece depois de amostras reais.
- Histórico não finge sessões inexistentes.
- Insights não fingem análise sem dados.
- APK release abre sem Metro.
## Validação
```bash
cd FightRateRN
npm run typecheck
npm run lint
npm test -- --runInBand
npm audit --audit-level=moderate
```
Resultado:
```text
tsc OK
ESLint: No issues found
Tests: 2 suites / 4 tests passed
found 0 vulnerabilities
```
Build Android release arm64:
```bash
cd FightRateRN/android
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a
```
Resultado:
```text
BUILD SUCCESSFUL
APK: FightRateRN/android/app/build/outputs/apk/release/app-release.apk
Tamanho: 29.7 MB
Telegram: enviado
```
## Limitação ainda aberta
A conexão BLE real precisa ser validada no aparelho físico com a cinta Magene H003. O app agora escaneia/conecta/monitora BLE real, mas só o teste com o sensor confirma comportamento final de campo.
@@ -0,0 +1,54 @@
# FightRate — Sensor contínuo e histórico geral
## Objetivo
Separar claramente três conceitos:
1. **Sensor**: descobrir, mostrar disponibilidade e permitir conexão ao sensor BLE.
2. **Monitoramento contínuo**: depois de conectado, receber, mostrar e salvar BPM real o tempo todo no app.
3. **Janela de treino**: ação disponível somente em Captura; marca um intervalo dentro do fluxo contínuo para destaque no histórico.
## Critérios de sucesso
- A tela Sensor não mostra botão de abrir/iniciar janela de treino.
- A tela Sensor mostra se o sensor está disponível/conectado e permite conectar/desconectar/monitorar pelo fluxo de sensor.
- Conectar o sensor inicia o recebimento contínuo via serviço nativo e os dados alimentam Captura, Sensor, Histórico e Insights.
- A janela de treino continua existindo apenas na tela Captura e não interrompe o monitoramento ao fechar.
- Histórico mantém a lista de janelas de treino e adiciona uma visão de **uso total** baseada em `continuous_heart_rate_samples`.
- O histórico geral não usa dados falsos: se não houver amostras contínuas, mostra estado vazio honesto.
- BLE services/characteristics ficam mais legíveis, com hierarquia compacta e sem espaçamento excessivo.
- Código visível ao usuário em PT-BR; identificadores em inglês.
## Arquitetura recomendada
- Reusar `FightRateCaptureStore` como fonte local-first.
- Reusar `continuous_heart_rate_samples` como verdade para histórico geral.
- Criar uma pequena camada de domínio TS para agrupar amostras contínuas em blocos de uso por lacunas de tempo. Essa lógica é pura e testável.
- Expor do módulo nativo um método de leitura paginada/resumida das amostras contínuas, sem mudar o contrato BLE.
- Manter `workout_sessions` apenas para janelas destacadas.
## Responsabilidades
### Reusar
- `useHeartRateMonitor`: estado app-wide recebido do módulo nativo.
- `TrainingChart`: gráfico real e interativo com amostras timestampadas.
- `FightRateCaptureStore`: persistência SQLite local.
- `HistoryScreen`: entrada natural para alternar entre janelas e uso total.
### Criar/alterar
- `src/training/continuousUsage.ts`: agrupar amostras contínuas em blocos de uso e sumarizar BPM/duração.
- Teste Jest para `continuousUsage`.
- API nativa `getContinuousSamples(limit?)`.
- Wrapper TS para `getContinuousSamples`.
- UI de Histórico com seção/alternância para “Janelas” e “Uso total”.
- UI de Sensor sem ação de treino e com diagnóstico BLE mais claro.
## Fora de escopo
- Não trocar biblioteca BLE.
- Não adicionar dependências.
- Não mudar o design aprovado fora dos pontos pedidos.
- Não criar dados demo/fake.
- Não mexer no app Android legado.
## Verificação
- `npm run typecheck`
- `npm run lint`
- `npm test -- --runInBand`
- `npm audit --audit-level=moderate`
- `./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a`
@@ -0,0 +1,190 @@
# FightRate RN — Gráfico dinâmico: zoom, pan, pin e auto-follow
## Objetivo
Substituir o `TrainingChart` atual (estático, 120 últimos samples, sem zoom) por um gráfico dinâmico com viewport temporal, zoom por pinch, pan, granularidade adaptativa do eixo X, e seleção pinada que **sobrevive a novas amostras em tempo real**. O comportamento de auto-follow deve retomar automaticamente quando o usuário arrasta o viewport até a borda direita (último ponto conhecido).
Aplica-se aos três call sites atuais: `CaptureScreen` (tempo real), `HistoryScreen` (janela salva) e `HistoryScreen` (faixa customizada).
## Problema atual
`src/components/TrainingChart.tsx` (versão vigente):
```ts
const chart = useMemo(() => buildChart(samples.slice(-120)), [samples]);
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
```
- `samples.slice(-120)` corta o array por contagem, não por janela temporal. Janelas longas (1h+) ficam visualmente compactadas a 120 pontos; janelas curtas (10s) ficam visualizadas com 120 pontos (impossível na prática, mas o problema é simétrico).
- `selectedIndex` é a **posição no array de pontos**. Quando novas amostras chegam, `buildChart` re-cria o array `points`, e o índice anteriormente válido passa a apontar para uma amostra diferente — o ponto pinado "pula" e, no limite, some do viewport. Em tempo real isso é especialmente grave: o atleta clica num pico, e o gráfico "come" o pin a cada BLE notify.
- Sem zoom (não há pinch). O gráfico é uma janela fixa do início ao fim do dataset.
- Eixo X fixo, em segundos totais. Granularidade dos labels nunca se adapta.
- Hit-testing em `onResponderMove` usa `event.nativeEvent.locationX` dividido por `layoutWidth` para mapear ao viewBox 320-wide — perde precisão fora do viewBox e não tolera gestos de 2 dedos.
## Decisões
### Stack
- Manter `react-native-svg` como renderer.
- Adicionar `react-native-gesture-handler` (~v2) e `react-native-reanimated` (~v3) para pinch/pan nativos. Setup padrão de RN bare: import `react-native-gesture-handler` no topo de `index.js`, envolver `App` com `GestureHandlerRootView`. Habilitar reanimated via Babel plugin (`react-native-reanimated/plugin` em `babel.config.js`).
- Sem troca de chart lib. `react-native-svg` continua sendo o motor de render.
### Modelo conceitual
Substituir "render dos últimos N samples" por **viewport temporal**:
```ts
type Viewport = {
readonly startMs: number; // primeira timestamp visível
readonly endMs: number; // última timestamp visível (inclusive)
readonly liveTail: boolean; // true quando endMs === lastSampleTimestamp
};
```
```ts
type ChartPin = {
readonly timestampMs: number;
readonly sample: HeartRateSample;
} | null;
```
Estado interno do `TrainingChart` (hooks, não Redux):
```ts
const [viewport, setViewport] = useState<Viewport>(initialViewport);
const [pin, setPin] = useState<ChartPin>(null);
```
`viewport` é o único "contrato" entre zoom/pan e o render. O SVG é derivado do `viewport` + samples. `pin` é independente de posição no array — é por `timestampMs`. Amostras novas entram, o `viewport` é recalculado somente se `liveTail` estiver `true`; o `pin` continua válido.
### Granularidade adaptativa do eixo X
A duração visível do viewport (`endMs - startMs`) decide a granularidade dos ticks:
| Duração visível | Granularidade dos ticks principais | Label |
|---|---|---|
| `>= 5 min` | 1 min | `HH:mm` |
| `>= 1 min` | 30 s | `HH:mm:ss` |
| `>= 30 s` | 15 s | `HH:mm:ss` |
| `>= 10 s` | 10 s | `HH:mm:ss` |
| `>= 2 s` | 1 s | `HH:mm:ss` |
| `< 2 s` | 0.5 s | `HH:mm:ss.S` |
Regras:
- Mostrar no máximo 4–5 ticks para não poluir.
- Tick 0 (início do viewport) sempre renderizado.
- Não usar ticks decorativos. Se o dataset não tem amostra no tick, o tick vira referência de eixo apenas.
### Zoom
- **Pinch** (dois dedos): escala o viewport em torno do ponto focado (ponto médio entre os dois dedos em coordenadas de tempo).
- **Botões `+` / `−`** no canto superior direito do painel gráfico: zoom in/out de 2x mantendo o centro do viewport.
- **Double-tap**: zoom in de 2x no ponto clicado; três taps em 1.2 s volta ao fit da janela selecionada (de acordo com o `windowSamples` que o pai passou).
- Limites: mínimo 1 s visível; máximo = janela total selecionada (fit).
- Os gestos de pinch e pan **não devem disparar tap**; gesto horizontal no gráfico move viewport; gesto vertical isolado é ignorado (a altura do gráfico não muda).
### Pan
- 1 dedo arrastando = pan. `pan` ajusta `startMs` e `endMs` proporcionalmente ao movimento em pixels (relação `pixelPorMs = layoutWidth / (endMs - startMs)`).
- Quando o usuário inicia pan, `liveTail` vai para `false` imediatamente.
- `liveTail` retorna a `true` **somente** quando, ao final do gesto, o `endMs` do viewport estiver a menos de `5%` da duração visível da última amostra conhecida (snap zone).
- Quando `liveTail` está `true` e novas amostras chegam, o viewport estende o `endMs` automaticamente (a "esteira" do tempo real).
- Quando `liveTail` está `false`, novas amostras **não** movem o viewport (mas o `lastPoint` continua sendo o último da coleção, para render do "tail off-screen indicator").
### Pin (clique em ponto)
- Tap em um ponto = `setPin({ timestampMs, sample })`.
- Pin sobrevive a:
- novas amostras chegarem (o `timestampMs` continua existindo no array de samples, a menos que o ponto esteja fora do novo viewport);
- zoom in/out (o pin continua naquele `timestampMs`);
- pan (idem).
- Pin some se:
- o usuário tocar fora do gráfico;
- o usuário tocar de novo no mesmo ponto pinado (toggle);
- o `timestampMs` do pin ficar **fora do viewport** e o usuário não voltar para ele (auto-clear opcional, mas default é manter — fica "off-screen" e um pequeno indicador pode avisar).
- Render do pin: círculo âmbar + linha tracejada vertical até o eixo X (visual atual mantido, refinado).
- O `readout` (painel inferior) mostra o sample pinado. Sem pin, mostra "TOQUE/ARRASTE PARA INSPECIONAR" e mantém o BPM atual.
### Hit-testing
- O gesto de tap é resolvido por `Gesture.Tap().onEnd((e) => pickPointAtX(e.x, viewport, samples))`.
- `pickPointAtX` mapeia `e.x` (em pixels no SVG) para `timestampMs = startMs + (e.x - PAD_X) / (CHART_WIDTH - 2*PAD_X) * (endMs - startMs)`, e então busca a amostra de `samples` mais próxima no tempo (busca binária por timestamp, fallback linear para datasets pequenos).
- Tolerância de snap: se a distância para a amostra mais próxima for maior que `10%` da duração visível, **não** seta pin (considera tap em área vazia).
## Estrutura de código
### Novos arquivos
- `src/training/chartViewport.ts` — funções puras (testáveis) para:
- `granularityForSpan(spanMs: number): Granularity`
- `formatTickLabel(timestampMs: number, granularity: Granularity): string`
- `buildChartViewport(samples, options)` — calcula o viewport inicial
- `applyZoomIn(viewport, focusMs)` / `applyZoomOut(viewport, focusMs)`
- `applyPan(viewport, dxPixels, pixelsPerMs)`
- `isLiveTailSnap(viewport, lastSampleTimestampMs)` — regra do snap zone
- `pickSampleAtTimestamp(samples, timestampMs, toleranceMs)`
- tipos: `Viewport`, `ChartPin`, `Granularity`
- `src/training/__tests__/chartViewport.test.ts` — testes TDD das funções puras.
### Modificados
- `src/components/TrainingChart.tsx` — reescrito. Mantém:
- viewBox 320×148 (compatibilidade com tokens);
- cor de linha `colors.action`, fundo `colors.surfaceHot`, raio `radius.md`;
- painel `readout` (label + valor) abaixo do gráfico;
- pin: círculo âmbar + linha tracejada.
Acrescenta:
- pinch + pan via `react-native-gesture-handler`;
- tap handler para pin;
- eixo X com ticks adaptativos (função pura);
- barra/cluster de botões `+` / `−` no canto;
- indicador "off-screen latest" quando `!liveTail` (uma seta/marca à direita mostrando que há dados mais novos).
- `src/components/ActionButton.tsx` — pode receber uso novo (botão pequeno icon-only para +/−). Avaliar reuso antes de criar componente novo.
- `FightRateRN/index.js` — adicionar `import 'react-native-gesture-handler'` no topo.
- `FightRateRN/babel.config.js` — adicionar `react-native-reanimated/plugin` (deve ser o último da lista).
- `FightRateRN/App.tsx` — envolver conteúdo em `GestureHandlerRootView` (verificar se já está).
- `package.json` — adicionar `react-native-gesture-handler` e `react-native-reanimated`.
- `android/app/src/main/java/com/healthtracking/fightrate/MainActivity.kt` — verificar se herda de `ReactActivity` (gesture-handler detecta automaticamente; nada a fazer em geral).
- `docs/FIGHTRATE_DEVELOPMENT_WORKFLOW.md` — atualizar a nota de que a skill `fightrate-quality-loop` recomenda aprovação antes de adicionar dependências (já cobre).
### Não modificados
- `useHeartRateMonitor.ts` — o gráfico consome `samples` como prop. O comportamento de auto-follow é responsabilidade do gráfico, não do hook.
- `CaptureScreen.tsx`, `HistoryScreen.tsx` — call sites do `TrainingChart`. A interface do componente (`samples`) permanece compatível; nenhum `prop` novo obrigatório é exposto ao pai nesta fatia (zoom/pan/pin são internos).
## Critérios de sucesso
1. **Auto-follow funcional**: ao entrar no `CaptureScreen` e a captura estar rodando, o gráfico se mantém mostrando os últimos N segundos; cada novo BPM real reposiciona o viewport para incluir a nova amostra à direita.
2. **Pin sobrevive a novas amostras**: tocar num ponto, deixar o sensor entregar 5 novas leituras → o pin continua visível na mesma posição, com o mesmo BPM e timestamp. O gráfico pode crescer à direita.
3. **Pan sai do live**: arrastar para a esquerda → `liveTail = false`; novas amostras param de mover o viewport. O indicador "off-screen latest" aparece à direita.
4. **Retomada do live**: arrastar de volta até a snap zone (últimos 5% da duração visível) → `liveTail = true` e o gráfico volta a rolar.
5. **Pinch**: 2 dedos abrindo → zoom in em torno do ponto médio; fechar → zoom out. Não dispara tap acidental.
6. **Botões `+` / `−`**: zoom in/out de 2x no centro.
7. **Granularidade adaptativa**: 10 min de janela visível → labels em `HH:mm`; 1 min → `HH:mm:ss` 30s; 30 s → 15 s; 10 s → 10 s; 2 s → 1 s; < 2 s → 0.5 s. Trocas suaves, sem flicker.
8. **Hit-test estável**: tap a 30% da largura do gráfico fixa o pin na amostra mais próxima no tempo, mesmo que o layout tenha width real diferente de `CHART_WIDTH`.
9. **Hit-test fora da área**: tap longe de qualquer amostra (gap > 10% da duração) não seta pin; o readout continua mostrando "TOQUE/ARRASTE PARA INSPECIONAR".
10. **Sem regressão visual**: tokens do Corner Instrument mantidos (cores, raio, borda, tipografia label, fundo `colors.surfaceHot`).
11. **Sem gráfico fake**: se `samples.length < 2`, mostra o estado vazio atual (sem render de linha). Nenhuma mudança nesse fluxo.
12. **Testes passam**: `npm test -- --runInBand` cobre `chartViewport.ts` (granularidade, zoom, pan, snap, pick). `App.test.tsx` continua passando.
13. **Build OK**: `npm run typecheck` e `npm run lint` exit 0. Build Android (`./gradlew :app:assembleDebug` em `FightRateRN/android`) só é rodado se JDK/JBR estiver disponível no ambiente.
## Riscos e mitigações
- **Risco**: gesture-handler requer setup de imports no Android (já é o caso em RN 0.86, mas o `MainActivity` pode precisar de `createReactActivityDelegate` atualizado).
**Mitigação**: seguir o passo-a-passo da documentação oficial; se o build falhar, voltar à abordagem `PanResponder + botões` e reusar o resto do spec.
- **Risco**: reanimated v3 + RN 0.86 — compatibilidade.
**Mitigação**: travar versões compatíveis (`react-native-reanimated@~3.16.0` recomendado para RN 0.86). Documentar a versão exata no `package.json` após smoke test.
- **Risco**: conflito entre gesto de pan do `ScrollView` pai e pan do gráfico.
**Mitigação**: usar `simultaneousWithExternalGesture` no `GestureDetector` do gráfico para desativar o pan do `ScrollView` apenas dentro da área do gráfico; testar com `CaptureScreen` (que está dentro de `ScrollView`).
- **Risco**: a busca binária de `pickSampleAtTimestamp` precisa de `samples` ordenados. Garantir invariante no hook e/ou ordenar defensivamente.
**Mitigação**: função pura de ordenação dedicada; teste cobre o caso de samples fora de ordem.
## Não-objetivos (fora do escopo)
- Crosshair, brush selection, comparação de duas janelas no mesmo gráfico.
- Exportar gráfico como imagem.
- Reorganizar layout da `CaptureScreen` ou da `HistoryScreen` (apenas a parte do `TrainingChart`).
- Trocar a paleta de cores.
- Adicionar leitura de RR no eixo Y (permanece em BPM).
- Sincronizar dois gráficos lado a lado.
- Persistir o último zoom/pan entre sessões (state efêmero).