diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php
new file mode 100644
index 0000000..80cfe39
--- /dev/null
+++ b/app/Controllers/AuthController.php
@@ -0,0 +1,515 @@
+user = $user;
+
+ $this->loginAttempt = $loginAttempt;
+ }
+
+
+ /**
+ * Processa o acesso à página de login.
+ *
+ * GET:
+ * Exibe o formulário.
+ *
+ * POST:
+ * Processa a tentativa de autenticação.
+ */
+ public function login(): void
+ {
+ if ($this->usuarioAutenticado()) {
+ header('Location: /dashboard.php');
+ exit;
+ }
+
+ /*
+ * Garante que a sessão possua
+ * um token CSRF.
+ */
+ $this->gerarTokenCsrf();
+
+ $erro = '';
+
+ $email = trim(
+ (string)($_POST['email'] ?? '')
+ );
+
+ $senha = (string)(
+ $_POST['senha'] ?? ''
+ );
+
+ /*
+ * Obtém o endereço IP real do cliente.
+ */
+ $ip = $this->obterIpCliente();
+
+ /*
+ * Processa somente requisições POST.
+ */
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+
+ $erro = $this->processarLogin(
+ $email,
+ $senha,
+ $ip
+ );
+ }
+
+ /*
+ * Carrega a View responsável
+ * pela apresentação do formulário.
+ */
+ require __DIR__ . '/../Views/auth/login.php';
+ }
+
+
+ /**
+ * Verifica se existe um usuário autenticado
+ * na sessão atual.
+ */
+ private function usuarioAutenticado(): bool
+ {
+ return isset(
+ $_SESSION['usuario_id']
+ );
+ }
+
+
+ /**
+ * Cria o token CSRF caso ainda
+ * não exista na sessão.
+ */
+ private function gerarTokenCsrf(): void
+ {
+ if (empty($_SESSION['csrf_token'])) {
+
+ $_SESSION['csrf_token'] = bin2hex(
+ random_bytes(32)
+ );
+ }
+ }
+
+
+ /**
+ * Processa todas as etapas de uma
+ * tentativa de autenticação.
+ */
+ private function processarLogin(
+ string $email,
+ string $senha,
+ string $ip
+ ): string {
+
+ /*
+ * Valida o token CSRF enviado
+ * pelo formulário.
+ */
+ if (!$this->validarCsrf()) {
+ return 'E-mail ou senha inválidos.';
+ }
+
+ /*
+ * Valida os dados básicos enviados
+ * pelo usuário.
+ */
+ if (
+ !$this->validarDadosLogin(
+ $email,
+ $senha
+ )
+ ) {
+ return 'E-mail ou senha inválidos.';
+ }
+
+ /*
+ * Verifica o limite de tentativas
+ * para a conta.
+ */
+ if ($this->contaBloqueada($email)) {
+ return 'E-mail ou senha inválidos.';
+ }
+
+ /*
+ * Verifica o limite de tentativas
+ * para o endereço IP.
+ */
+ if ($this->ipBloqueado($ip)) {
+ return 'E-mail ou senha inválidos.';
+ }
+
+ /*
+ * Solicita ao Model o usuário
+ * correspondente ao e-mail informado.
+ */
+ $usuario = $this->user->buscarPorEmail(
+ $email
+ );
+
+ /*
+ * Verifica as credenciais fornecidas.
+ */
+ if (
+ $usuario !== null &&
+ (int)$usuario['ativo'] === 1 &&
+ password_verify(
+ $senha,
+ $usuario['senha']
+ )
+ ) {
+
+ /*
+ * Registra a tentativa bem-sucedida.
+ */
+ $this->loginAttempt->registrar(
+ $email,
+ $ip,
+ true
+ );
+
+ /*
+ * Cria a sessão autenticada.
+ */
+ $this->criarSessaoAutenticada(
+ $usuario
+ );
+
+ /*
+ * Atualiza o hash da senha caso
+ * seja necessário utilizar parâmetros
+ * mais atuais.
+ */
+ $this->atualizarHashSeNecessario(
+ $usuario,
+ $senha
+ );
+
+ header('Location: /dashboard.php');
+ exit;
+ }
+
+ /*
+ * Registra a tentativa inválida.
+ */
+ $this->loginAttempt->registrar(
+ $email,
+ $ip,
+ false
+ );
+
+ /*
+ * Pequeno atraso para dificultar
+ * ataques automatizados.
+ */
+ usleep(300000);
+
+ return 'E-mail ou senha inválidos.';
+ }
+
+
+ /**
+ * Valida o token CSRF enviado
+ * pelo formulário de login.
+ */
+ private function validarCsrf(): bool
+ {
+ $token = (string)(
+ $_POST['csrf_token'] ?? ''
+ );
+
+ return (
+ $token !== '' &&
+ isset($_SESSION['csrf_token']) &&
+ hash_equals(
+ $_SESSION['csrf_token'],
+ $token
+ )
+ );
+ }
+
+
+ /**
+ * Valida os dados básicos utilizados
+ * durante a autenticação.
+ */
+ private function validarDadosLogin(
+ string $email,
+ string $senha
+ ): bool {
+
+ if (strlen($email) > 150) {
+ return false;
+ }
+
+ if (
+ $email === '' ||
+ $senha === ''
+ ) {
+ return false;
+ }
+
+ if (
+ filter_var(
+ $email,
+ FILTER_VALIDATE_EMAIL
+ ) === false
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Obtém o endereço IP real do cliente.
+ *
+ * Quando a requisição passa pelo Traefik,
+ * REMOTE_ADDR corresponde ao IP do Traefik.
+ *
+ * Nesse caso utilizamos X-Real-IP,
+ * conforme configurado no ambiente.
+ */
+ private function obterIpCliente(): string
+ {
+ $remoteAddr =
+ $_SERVER['REMOTE_ADDR'] ?? '';
+
+ if ($remoteAddr === '172.18.0.3') {
+
+ $ip =
+ $_SERVER['HTTP_X_REAL_IP'] ?? '';
+
+ if (
+ filter_var(
+ $ip,
+ FILTER_VALIDATE_IP
+ ) !== false
+ ) {
+ return $ip;
+ }
+ }
+
+ if (
+ filter_var(
+ $remoteAddr,
+ FILTER_VALIDATE_IP
+ ) !== false
+ ) {
+ return $remoteAddr;
+ }
+
+ return '0.0.0.0';
+ }
+
+
+ /**
+ * Verifica se o endereço de e-mail
+ * atingiu o limite de tentativas inválidas.
+ */
+ private function contaBloqueada(
+ string $email
+ ): bool {
+
+ $quantidade =
+ $this->loginAttempt
+ ->contarFalhasPorEmail(
+ $email,
+ self::JANELA_MINUTOS
+ );
+
+ return $quantidade
+ >= self::MAX_TENTATIVAS_CONTA;
+ }
+
+
+ /**
+ * Verifica se o endereço IP atingiu
+ * o limite de tentativas inválidas.
+ */
+ private function ipBloqueado(
+ string $ip
+ ): bool {
+
+ $quantidade =
+ $this->loginAttempt
+ ->contarFalhasPorIp(
+ $ip,
+ self::JANELA_MINUTOS
+ );
+
+ return $quantidade
+ >= self::MAX_TENTATIVAS_IP;
+ }
+
+
+ /**
+ * Cria a sessão do usuário após
+ * uma autenticação bem-sucedida.
+ *
+ * A regeneração do ID impede ataques
+ * de session fixation.
+ */
+ private function criarSessaoAutenticada(
+ array $usuario
+ ): void {
+
+ session_regenerate_id(true);
+
+ /*
+ * Gera um novo token CSRF depois
+ * da autenticação.
+ */
+ $_SESSION['csrf_token'] = bin2hex(
+ random_bytes(32)
+ );
+
+ $_SESSION['usuario_id'] =
+ $usuario['id'];
+
+ $_SESSION['usuario_nome'] =
+ $usuario['nome'];
+
+ $_SESSION['usuario_email'] =
+ $usuario['email'];
+
+ $_SESSION['usuario_perfil'] =
+ $usuario['perfil'];
+
+ /*
+ * Registra os tempos utilizados
+ * pelo controle de expiração da sessão.
+ */
+ $_SESSION['login_time'] = time();
+
+ $_SESSION['last_activity'] = time();
+ }
+
+
+ /**
+ * Atualiza o hash da senha caso o algoritmo
+ * ou seus parâmetros tenham evoluído.
+ *
+ * A operação de banco é realizada pelo
+ * Model User.
+ */
+ private function atualizarHashSeNecessario(
+ array $usuario,
+ string $senha
+ ): void {
+
+ if (
+ !password_needs_rehash(
+ $usuario['senha'],
+ PASSWORD_DEFAULT
+ )
+ ) {
+ return;
+ }
+
+ $novoHash = password_hash(
+ $senha,
+ PASSWORD_DEFAULT
+ );
+
+ $this->user->atualizarSenha(
+ (int)$usuario['id'],
+ $novoHash
+ );
+ }
+
+
+ /**
+ * Encerra a sessão do usuário.
+ *
+ * Remove os dados da sessão, invalida
+ * o cookie de sessão e destrói a sessão
+ * existente no servidor.
+ */
+ public function logout(): void
+ {
+ /*
+ * Remove todos os dados armazenados
+ * na sessão atual.
+ */
+ $_SESSION = [];
+
+ /*
+ * Remove o cookie de sessão
+ * do navegador.
+ */
+ if (ini_get('session.use_cookies')) {
+
+ $params =
+ session_get_cookie_params();
+
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ (bool)$params['secure'],
+ (bool)$params['httponly']
+ );
+ }
+
+ /*
+ * Destrói a sessão no servidor.
+ */
+ session_destroy();
+
+ /*
+ * Retorna o usuário para
+ * a tela de login.
+ */
+ header('Location: /login.php');
+
+ exit;
+ }
+}
\ No newline at end of file
diff --git a/app/Controllers/Controller.php b/app/Controllers/Controller.php
new file mode 100644
index 0000000..d50a5fc
--- /dev/null
+++ b/app/Controllers/Controller.php
@@ -0,0 +1,15 @@
+cat > /home/opc/html/app/Controllers/Controller.php <<'EOF'
+ $_SESSION['usuario_id'],
+ 'nome' => $_SESSION['usuario_nome'] ?? '',
+ 'email' => $_SESSION['usuario_email'] ?? '',
+ 'perfil' => $_SESSION['usuario_perfil'] ?? ''
+ ];
+
+ /*
+ * Carrega a View do dashboard.
+ *
+ * A View recebe os dados através da variável
+ * $usuario e fica responsável somente pela
+ * apresentação das informações.
+ */
+ require __DIR__ . '/../Views/dashboard/index.php';
+ }
+}
diff --git a/app/Controllers/SecurityController.php b/app/Controllers/SecurityController.php
new file mode 100644
index 0000000..1bfdd8b
--- /dev/null
+++ b/app/Controllers/SecurityController.php
@@ -0,0 +1,285 @@
+ 0
+ && ($agora - $ultimoAcesso)
+ > self::SESSAO_INATIVIDADE_SEGUNDOS
+ ) {
+ $this->encerrarSessao(
+ 'inatividade'
+ );
+
+ return;
+ }
+
+ /*
+ * Verifica se a sessão atingiu seu tempo
+ * máximo absoluto.
+ */
+ if (
+ $inicioSessao > 0
+ && ($agora - $inicioSessao)
+ > self::SESSAO_MAXIMA_SEGUNDOS
+ ) {
+ $this->encerrarSessao(
+ 'tempo_maximo'
+ );
+
+ return;
+ }
+
+ /*
+ * Registra a última atividade do usuário.
+ */
+ $_SESSION['last_activity'] = $agora;
+ }
+
+ /**
+ * Encerra uma sessão expirada e redireciona
+ * o usuário para a tela de login.
+ *
+ * O motivo da expiração é enviado através
+ * de um parâmetro na URL.
+ */
+ private function encerrarSessao(
+ string $motivo
+ ): void {
+ /*
+ * Remove todos os dados armazenados
+ * na sessão atual.
+ */
+ $_SESSION = [];
+
+ /*
+ * Remove o cookie da sessão do navegador.
+ */
+ if (ini_get('session.use_cookies')) {
+ $params = session_get_cookie_params();
+
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ (bool) $params['secure'],
+ (bool) $params['httponly']
+ );
+ }
+
+ /*
+ * Destrói a sessão no servidor.
+ */
+ session_destroy();
+
+ /*
+ * Redireciona para o login informando
+ * que a sessão foi encerrada.
+ */
+ header(
+ 'Location: /login.php?expirada=1&motivo='
+ . urlencode($motivo)
+ );
+
+ exit;
+ }
+
+ /**
+ * Executa a sequência básica de segurança
+ * necessária para uma requisição da aplicação.
+ *
+ * Este método permite que as páginas utilizem
+ * apenas uma chamada para inicializar a segurança.
+ */
+ public function iniciarSeguranca(): void
+ {
+ /*
+ * Configura o cookie antes de iniciar a sessão.
+ */
+ $this->configurarSessao();
+
+ /*
+ * Inicia a sessão.
+ */
+ $this->iniciarSessao();
+
+ /*
+ * Aplica os headers HTTP de segurança.
+ */
+ $this->aplicarHeadersSeguranca();
+
+ /*
+ * Verifica se uma sessão autenticada
+ * expirou por tempo.
+ */
+ $this->verificarExpiracaoSessao();
+ }
+}
+?>
+
diff --git a/app/Models/.gitkeep b/app/Models/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Models/LoginAttempt.php b/app/Models/LoginAttempt.php
new file mode 100644
index 0000000..2051370
--- /dev/null
+++ b/app/Models/LoginAttempt.php
@@ -0,0 +1,136 @@
+pdo = $pdo;
+ }
+
+
+ /**
+ * Conta as tentativas inválidas de um endereço
+ * de e-mail dentro da janela de tempo informada.
+ */
+ public function contarFalhasPorEmail(
+ string $email,
+ int $janelaMinutos
+ ): int {
+
+ $janelaMinutos = max(
+ 1,
+ $janelaMinutos
+ );
+
+ $sql = "
+ SELECT COUNT(*)
+ FROM tentativas_login
+ WHERE email = :email
+ AND sucesso = 0
+ AND criado_em >= DATE_SUB(
+ NOW(),
+ INTERVAL {$janelaMinutos} MINUTE
+ )
+ ";
+
+ $stmt = $this->pdo->prepare($sql);
+
+ $stmt->execute([
+ 'email' => $email
+ ]);
+
+ return (int)$stmt->fetchColumn();
+ }
+
+
+ /**
+ * Conta as tentativas inválidas de um endereço
+ * IP dentro da janela de tempo informada.
+ */
+ public function contarFalhasPorIp(
+ string $ip,
+ int $janelaMinutos
+ ): int {
+
+ $janelaMinutos = max(
+ 1,
+ $janelaMinutos
+ );
+
+ $sql = "
+ SELECT COUNT(*)
+ FROM tentativas_login
+ WHERE ip_address = :ip
+ AND sucesso = 0
+ AND criado_em >= DATE_SUB(
+ NOW(),
+ INTERVAL {$janelaMinutos} MINUTE
+ )
+ ";
+
+ $stmt = $this->pdo->prepare($sql);
+
+ $stmt->execute([
+ 'ip' => $ip
+ ]);
+
+ return (int)$stmt->fetchColumn();
+ }
+
+
+ /**
+ * Registra uma tentativa de autenticação
+ * realizada pelo usuário.
+ */
+ public function registrar(
+ string $email,
+ string $ip,
+ bool $sucesso
+ ): bool {
+
+ $sql = "
+ INSERT INTO tentativas_login
+ (
+ email,
+ ip_address,
+ sucesso
+ )
+ VALUES
+ (
+ :email,
+ :ip,
+ :sucesso
+ )
+ ";
+
+ $stmt = $this->pdo->prepare($sql);
+
+ return $stmt->execute([
+ 'email' => $email,
+ 'ip' => $ip,
+ 'sucesso' => $sucesso ? 1 : 0
+ ]);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
new file mode 100644
index 0000000..fb935fd
--- /dev/null
+++ b/app/Models/User.php
@@ -0,0 +1,96 @@
+pdo = $pdo;
+ }
+
+
+ /**
+ * Localiza um usuário através do endereço de e-mail.
+ *
+ * Retorna os dados necessários para o processo
+ * de autenticação.
+ *
+ * Retorna null quando o usuário não existe.
+ */
+ public function buscarPorEmail(string $email): ?array
+ {
+ $sql = "
+ SELECT
+ id,
+ nome,
+ email,
+ senha,
+ perfil,
+ ativo
+ FROM usuarios
+ WHERE email = :email
+ LIMIT 1
+ ";
+
+ $stmt = $this->pdo->prepare($sql);
+
+ $stmt->execute([
+ 'email' => $email
+ ]);
+
+ $usuario = $stmt->fetch();
+
+ if ($usuario === false) {
+ return null;
+ }
+
+ return $usuario;
+ }
+
+
+ /**
+ * Atualiza o hash da senha de um usuário.
+ *
+ * Utilizado quando password_needs_rehash()
+ * identificar que o algoritmo ou os parâmetros
+ * utilizados para criar o hash foram atualizados.
+ */
+ public function atualizarSenha(
+ int $id,
+ string $novoHash
+ ): bool {
+
+ $sql = "
+ UPDATE usuarios
+ SET senha = :senha
+ WHERE id = :id
+ ";
+
+ $stmt = $this->pdo->prepare($sql);
+
+ return $stmt->execute([
+ 'senha' => $novoHash,
+ 'id' => $id
+ ]);
+ }
+}
diff --git a/app/Services/.gitkeep b/app/Services/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Views/auth/.gitkeep b/app/Views/auth/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php
new file mode 100644
index 0000000..8b38a31
--- /dev/null
+++ b/app/Views/auth/login.php
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+
+
+
+
+
+Acesso - Finanças+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ACESSO AO SISTEMA
+
+
+
+
+
+ Acesse sua área financeira.
+
+
+
+
+
+
+
+
+ = htmlspecialchars(
+ $erro,
+ ENT_QUOTES | ENT_SUBSTITUTE,
+ 'UTF-8'
+ ) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/dashboard/.gitkeep b/app/Views/dashboard/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/app/Views/dashboard/index.php b/app/Views/dashboard/index.php
new file mode 100644
index 0000000..52dbc98
--- /dev/null
+++ b/app/Views/dashboard/index.php
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Dashboard - Finanças+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ÁREA DO USUÁRIO
+
+
+
+ Olá, = htmlspecialchars(
+ $usuario['nome'],
+ ENT_QUOTES | ENT_SUBSTITUTE,
+ 'UTF-8'
+ ) ?>!
+
+
+
+ Bem-vindo ao seu painel financeiro.
+
+
+
+
+
+
+
+
+
+ ??
+
+
+
+ PATRIMÔNIO
+
+
+
+ R$ 0,00
+
+
+
+ Seu patrimônio será exibido aqui.
+
+
+
+
+
+
+
+ ??
+
+
+
+ RECEITAS
+
+
+
+ R$ 0,00
+
+
+
+ Acompanhe suas receitas mensais.
+
+
+
+
+
+
+
+ ??
+
+
+
+ INVESTIMENTOS
+
+
+
+ R$ 0,00
+
+
+
+ Acompanhe a evolução dos seus investimentos.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/Views/layouts/.gitkeep b/app/Views/layouts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/config/database.php b/config/database.php
new file mode 100644
index 0000000..234ea3d
--- /dev/null
+++ b/config/database.php
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/config/security.php b/config/security.php
new file mode 100644
index 0000000..e4a5019
--- /dev/null
+++ b/config/security.php
@@ -0,0 +1,350 @@
+ 0 &&
+ ($agora - $ultimoAcesso)
+ > SESSAO_INATIVIDADE_SEGUNDOS
+ ) {
+
+ destruirSessao();
+
+ header(
+ 'Location: /login.php?expirada=1'
+ );
+
+ exit;
+ }
+
+
+ /*
+ * =====================================================
+ * EXPIRAÇÃO PELO TEMPO MÁXIMO
+ * =====================================================
+ *
+ * Mesmo que o usuário continue ativo,
+ * a sessão não poderá ultrapassar 8 horas.
+ */
+ if (
+ $inicioSessao > 0 &&
+ ($agora - $inicioSessao)
+ > SESSAO_MAXIMA_SEGUNDOS
+ ) {
+
+ destruirSessao();
+
+ header(
+ 'Location: /login.php?expirada=1'
+ );
+
+ exit;
+ }
+
+
+ /*
+ * =====================================================
+ * ATUALIZAÇÃO DA ATIVIDADE
+ * =====================================================
+ *
+ * A cada requisição autenticada, registra o momento
+ * do último acesso.
+ */
+ $_SESSION['last_activity'] = $agora;
+}
+
+
+/*
+ * =========================================================
+ * FUNÇÃO DE DESTRUIÇÃO DA SESSÃO
+ * =========================================================
+ *
+ * Centraliza o procedimento utilizado quando uma sessão
+ * precisa ser encerrada.
+ */
+function destruirSessao(): void
+{
+ /*
+ * Remove todos os dados armazenados na sessão.
+ */
+ $_SESSION = [];
+
+
+ /*
+ * Remove o cookie da sessão no navegador.
+ */
+ if (ini_get('session.use_cookies')) {
+
+ $params =
+ session_get_cookie_params();
+
+ setcookie(
+ session_name(),
+ '',
+ time() - 42000,
+ $params['path'],
+ $params['domain'],
+ (bool)$params['secure'],
+ (bool)$params['httponly']
+ );
+ }
+
+
+ /*
+ * Destrói a sessão existente no servidor.
+ */
+ session_destroy();
+}
diff --git a/css/style.css b/css/style.css
index 371f050..cab5ebd 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1 +1,357 @@
:root{--bg:#f6f8fc;--white:#fff;--text:#142238;--muted:#68778b;--blue:#2563eb;--green:#10b981;--border:#e5eaf1;--shadow:0 25px 70px #10264d16;--radius:22px;--max:1180px}*{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:Inter,Arial,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}a{text-decoration:none;color:inherit}.container{width:min(var(--max),calc(100% - 40px));margin:auto}.header{position:fixed;z-index:10;top:0;width:100%;background:#ffffffd9;backdrop-filter:blur(15px);border-bottom:1px solid #e5eaf1aa}.nav{height:76px;display:flex;align-items:center;justify-content:space-between}.brand{display:flex;align-items:center;gap:9px;font-weight:800}.brand>span:last-child{color:var(--blue)}.mark{display:grid;place-items:center;width:35px;height:35px;border-radius:10px;background:var(--blue);color:#fff!important}.links{display:flex;gap:30px;color:#526177;font-size:.9rem;font-weight:600}.links a:hover{color:var(--blue)}.menu{display:none;border:0;background:none;font-size:1.5rem}.hero{min-height:760px;padding:150px 0 100px;position:relative;overflow:hidden;color:#fff;background:linear-gradient(135deg,#0a1b35,#123f79 48%,#176a72)}.orb{position:absolute;border-radius:50%;filter:blur(5px);opacity:.25}.one{width:450px;height:450px;right:-100px;top:100px;background:#38bdf8}.two{width:380px;height:380px;left:-180px;bottom:-170px;background:#22c55e}.hero-grid{position:relative;z-index:1;display:grid;grid-template-columns:1.02fr .98fr;align-items:center;gap:65px}.eyebrow{display:inline-block;color:#2d72ed;font-size:.72rem;font-weight:800;letter-spacing:.14em;margin-bottom:13px}.hero .eyebrow{color:#8bd7ff}h1{font-size:clamp(3rem,5vw,5.1rem);line-height:1.03;letter-spacing:-.055em}h1 em{font-style:normal;color:#7de2c4}.hero-copy>p{max-width:610px;margin:25px 0;color:#d4e1f0;font-size:1.08rem}.actions{display:flex;gap:13px;flex-wrap:wrap}.btn{border:0;border-radius:13px;padding:14px 21px;font:inherit;font-weight:700;cursor:pointer;display:inline-flex;justify-content:center;gap:10px;transition:.2s}.btn.primary{background:var(--blue);color:#fff;box-shadow:0 10px 30px #0b2b6640}.btn.primary:hover{transform:translateY(-2px);filter:brightness(1.08)}.btn.outline{border:1px solid #ffffff40;color:#fff}.trust{margin-top:27px;color:#bfd0e5;font-size:.76rem}.visual{min-height:450px;display:grid;place-items:center;position:relative}.dashboard{width:min(470px,100%);padding:28px;border:1px solid #ffffff25;border-radius:28px;background:#ffffff13;box-shadow:0 35px 90px #0005;backdrop-filter:blur(16px)}.dash-top,.dash-bottom{display:flex;justify-content:space-between;color:#bfd0e5;font-size:.77rem}.dash-top b{color:#4ade80}.dashboard>strong{display:block;font-size:2.35rem;margin:8px 0}.mini{height:180px}.mini svg{width:100%;height:100%}.line{fill:none;stroke:#78e4c1;stroke-width:5;stroke-linecap:round}.fill{fill:#78e4c120}.dash-bottom{color:#7de2c4}.float{position:absolute;display:flex;align-items:center;gap:11px;background:#fff;color:var(--text);padding:13px 17px;border-radius:15px;box-shadow:var(--shadow)}.float small{display:block;color:var(--muted);font-size:.65rem}.float b{display:block;font-size:.85rem}.goal{right:0;top:45px}.saved{left:0;bottom:45px}.section{padding:105px 0}.heading{max-width:720px;margin-bottom:45px}.heading h2,.sim-copy h2{font-size:clamp(2rem,3.5vw,3.1rem);line-height:1.12;letter-spacing:-.04em}.heading p,.sim-copy>p{color:var(--muted);margin-top:14px}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}.card{padding:30px;background:#fff;border:1px solid var(--border);border-radius:var(--radius);transition:.25s}.card:hover{transform:translateY(-6px);box-shadow:var(--shadow)}.icon{font-size:2rem;margin-bottom:20px}.card small{color:#9aa7b7;font-weight:800;font-size:.7rem}.card h3{font-size:1.35rem;margin:5px 0}.card p{color:var(--muted);font-size:.9rem}.sim-section{padding:105px 0;background:#edf4fb}.sim-grid,.goal-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:70px;align-items:center}.tip{display:flex;gap:12px;margin-top:28px;padding:17px;background:#fff;border:1px solid var(--border);border-radius:16px}.tip p{color:var(--muted);font-size:.75rem;margin-top:3px}.sim-card{background:#fff;border:1px solid var(--border);border-radius:26px;padding:30px;box-shadow:var(--shadow)}.inputs{display:grid;grid-template-columns:1fr 1fr;gap:18px}.inputs label{font-size:.75rem;font-weight:700;color:#506076}.input{display:flex;align-items:center;margin-top:7px;padding:0 11px;border:1px solid #dce3ec;border-radius:11px;background:#fbfcfe}.input span{color:#8996a7;font-size:.74rem}.input input{width:100%;padding:12px 7px;border:0;outline:0;background:none;font:inherit;color:var(--text)}.full{width:100%;margin-top:22px}.results{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:24px}.results>div{padding:15px;border-radius:14px;background:#f5f7fa}.results small{display:block;color:var(--muted);font-size:.65rem}.results b{display:block;margin-top:4px}.results .highlight{background:#eaf8f2}.highlight b{color:#079669}.sim-card canvas{width:100%;height:190px;margin-top:15px;border-top:1px solid var(--border)}.goal-card{padding:30px;background:#fff;border:1px solid var(--border);border-radius:24px;box-shadow:var(--shadow)}.goal-head,.goal-values,.goal-foot{display:flex;justify-content:space-between;align-items:center}.goal-head strong{color:var(--blue)}.progress{height:12px;margin:20px 0 8px;background:#edf1f6;border-radius:20px;overflow:hidden}.progress span{display:block;width:64%;height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--blue),#25b99a);transition:.7s}.goal-values,.goal-foot{font-size:.76rem;color:var(--muted)}.goal-foot{margin-top:24px}.goal-foot button{border:0;background:none;color:var(--blue);font-weight:700;cursor:pointer}.quote{padding:70px 0;background:#fff;text-align:center}.quote div{max-width:800px}.quote span{font-size:5rem;color:#bdd5ff;line-height:.7}.quote p{font-size:1.4rem;font-weight:600}footer{padding:50px 0;background:#0d1b30;color:#aebed1}.footer{display:grid;grid-template-columns:1.5fr 1fr auto;gap:30px;align-items:end}.footer .brand{color:#fff}.footer p{font-size:.78rem;margin-top:10px}.footer-links{display:flex;gap:22px;font-size:.8rem}.footer-links a:hover{color:#fff}footer small{font-size:.7rem}.reveal{opacity:0;transform:translateY(25px);transition:.7s}.reveal.visible{opacity:1;transform:none}.delay1{transition-delay:.12s}.delay2{transition-delay:.24s}@media(max-width:900px){.hero-grid,.sim-grid,.goal-grid{grid-template-columns:1fr}.hero{padding-top:125px}.cards{grid-template-columns:1fr}.links{display:none}.menu{display:block}.nav.open .links{display:flex;position:absolute;top:76px;left:0;right:0;background:#fff;padding:20px;flex-direction:column;gap:16px;border-bottom:1px solid var(--border)}.footer{grid-template-columns:1fr}.footer-links{order:3}}@media(max-width:560px){.container{width:calc(100% - 28px)}h1{font-size:2.8rem}.hero{min-height:auto;padding-bottom:70px}.visual{min-height:340px}.dashboard{padding:20px}.float{padding:10px 12px}.goal{right:-3px}.saved{left:-3px}.section,.sim-section{padding:75px 0}.inputs,.results{grid-template-columns:1fr}.quote p{font-size:1.15rem}}
+
+/* =========================================================
+LOGIN — FINANÇAS+
+========================================================= */
+
+body.login-page {
+min-height: 100vh;
+display: flex;
+flex-direction: column;
+background:
+radial-gradient(circle at 10% 10%, #dbeafe 0, transparent 28%),
+radial-gradient(circle at 90% 90%, #d1fae5 0, transparent 25%),
+var(--bg);
+}
+
+/* Cabeçalho simplificado do login */
+
+.login-header {
+height: 76px;
+display: flex;
+align-items: center;
+border-bottom: 1px solid var(--border);
+background: #ffffffd9;
+backdrop-filter: blur(15px);
+}
+
+.login-header .container {
+display: flex;
+align-items: center;
+justify-content: space-between;
+}
+
+.login-back {
+color: var(--muted);
+font-size: .85rem;
+font-weight: 600;
+transition: .2s;
+}
+
+.login-back:hover {
+color: var(--blue);
+}
+
+/* Área principal */
+
+.login-main {
+flex: 1;
+display: flex;
+align-items: center;
+justify-content: center;
+padding: 60px 20px;
+}
+
+/* Cartão */
+
+.login-card {
+width: min(440px, 100%);
+padding: 42px;
+background: var(--white);
+border: 1px solid var(--border);
+border-radius: 26px;
+box-shadow: var(--shadow);
+}
+
+/* Cabeçalho do cartão */
+
+.login-title {
+text-align: center;
+margin-bottom: 32px;
+}
+
+.login-title .mark {
+width: 48px;
+height: 48px;
+margin: 0 auto 18px;
+border-radius: 13px;
+font-size: 1rem;
+}
+
+.login-title h1 {
+font-size: 1.9rem;
+line-height: 1.15;
+letter-spacing: -.035em;
+color: var(--text);
+}
+
+.login-title h1 em {
+font-style: normal;
+color: var(--blue);
+}
+
+.login-title p {
+margin-top: 8px;
+color: var(--muted);
+font-size: .86rem;
+}
+
+/* Mensagem de erro */
+
+.login-error {
+display: flex;
+align-items: flex-start;
+gap: 9px;
+margin-bottom: 22px;
+padding: 13px 15px;
+border: 1px solid #fecaca;
+border-radius: 12px;
+background: #fef2f2;
+color: #b91c1c;
+font-size: .8rem;
+line-height: 1.45;
+}
+
+/* Campos */
+
+.login-form {
+display: flex;
+flex-direction: column;
+gap: 19px;
+}
+
+.login-field label {
+display: block;
+margin-bottom: 7px;
+color: #506076;
+font-size: .76rem;
+font-weight: 700;
+}
+
+.login-input {
+width: 100%;
+padding: 13px 14px;
+border: 1px solid #dce3ec;
+border-radius: 11px;
+outline: none;
+background: #fbfcfe;
+color: var(--text);
+font: inherit;
+font-size: .9rem;
+transition: .2s;
+}
+
+.login-input::placeholder {
+color: #a5afbc;
+}
+
+.login-input:hover {
+border-color: #c7d2df;
+}
+
+.login-input:focus {
+border-color: var(--blue);
+background: #fff;
+box-shadow: 0 0 0 3px #2563eb18;
+}
+
+/* Botão */
+
+.login-button {
+width: 100%;
+margin-top: 5px;
+border: 0;
+border-radius: 12px;
+padding: 14px 20px;
+background: var(--blue);
+color: #fff;
+font: inherit;
+font-weight: 700;
+cursor: pointer;
+box-shadow: 0 10px 25px #2563eb30;
+transition: .2s;
+}
+
+.login-button:hover {
+transform: translateY(-2px);
+filter: brightness(1.08);
+}
+
+.login-button:active {
+transform: translateY(0);
+}
+
+/* Informação inferior */
+
+.login-info {
+margin-top: 25px;
+padding-top: 20px;
+border-top: 1px solid var(--border);
+text-align: center;
+color: var(--muted);
+font-size: .72rem;
+line-height: 1.5;
+}
+
+.login-info strong {
+color: var(--text);
+}
+
+/* Rodapé */
+
+.login-footer {
+padding: 22px;
+text-align: center;
+color: #8b99aa;
+font-size: .7rem;
+}
+
+/* Acessibilidade */
+
+.login-input:focus-visible,
+.login-button:focus-visible,
+.login-back:focus-visible {
+outline: 3px solid #2563eb35;
+outline-offset: 2px;
+}
+
+/* =========================================================
+LOGIN — RESPONSIVO
+========================================================= */
+
+@media (max-width: 560px) {
+
+```
+.login-header {
+ height: 68px;
+}
+
+.login-main {
+ padding: 35px 14px;
+ align-items: flex-start;
+}
+
+.login-card {
+ margin-top: 15px;
+ padding: 30px 22px;
+ border-radius: 21px;
+}
+
+.login-title {
+ margin-bottom: 27px;
+}
+
+.login-title h1 {
+ font-size: 1.7rem;
+}
+
+.login-footer {
+ padding: 18px 14px;
+}
+```
+
+}
+
+
+/*
+
+* =========================================================
+* TELA DE LOGIN
+* =========================================================
+ */
+
+.login-page {
+min-height: 100vh;
+display: grid;
+place-items: center;
+padding: 40px 20px;
+background: var(--bg);
+}
+
+
+
+.login-card {
+width: min(430px, 100%);
+padding: 42px;
+background: var(--white);
+border: 1px solid var(--border);
+border-radius: var(--radius);
+box-shadow: var(--shadow);
+}
+
+
+
+.login-brand {
+justify-content: center;
+font-size: 1.15rem;
+}
+
+.login-heading {
+margin-top: 38px;
+margin-bottom: 32px;
+text-align: center;
+}
+
+.login-heading h1 {
+font-size: 1.85rem;
+line-height: 1.2;
+letter-spacing: -0.03em;
+color: var(--text);
+}
+
+.login-heading h1 span {
+color: var(--blue);
+}
+
+.login-heading p {
+margin-top: 10px;
+color: var(--muted);
+font-size: .85rem;
+}
+
+.login-field {
+margin-bottom: 19px;
+}
+
+.login-field label {
+display: block;
+margin-bottom: 7px;
+color: var(--text);
+font-size: .78rem;
+font-weight: 700;
+}
+
+.login-field input {
+width: 100%;
+padding: 13px 14px;
+border: 1px solid #dce3ec;
+border-radius: 11px;
+outline: none;
+background: #fbfcfe;
+color: var(--text);
+font: inherit;
+transition: .2s;
+}
+
+.login-field input:focus {
+border-color: var(--blue);
+box-shadow: 0 0 0 3px #2563eb18;
+}
+
+
+.login-back {
+display: block;
+margin-top: 25px;
+color: var(--muted);
+text-align: center;
+font-size: .78rem;
+font-weight: 600;
+}
+
+.login-back:hover {
+color: var(--blue);
+}
+
+@media (max-width: 560px) {
+
+}
diff --git a/dashboard.php b/dashboard.php
new file mode 100644
index 0000000..6d4251b
--- /dev/null
+++ b/dashboard.php
@@ -0,0 +1,10 @@
+index();
diff --git a/includes/auth.php b/includes/auth.php
new file mode 100644
index 0000000..4be6b67
--- /dev/null
+++ b/includes/auth.php
@@ -0,0 +1,21 @@
+
+Finanças+ | Educação Financeira
-
-EDUCAÇÃO FINANCEIRASeu dinheiro deve trabalhar por você.
Aprenda a organizar suas finanças, tomar decisões melhores e construir um futuro financeiro mais tranquilo.
✓ Conteúdo educativo ✓ Simulação gratuita ✓ Sem cadastro
Patrimônio projetado●
R$ 84.750,00+18,4%12 meses
-PRIMEIROS PASSOSUma vida financeira melhor começa com três hábitos.
Conhecimento, organização e constância fazem mais diferença do que procurar uma fórmula mágica.
💰
01Organize
Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.
📊
02Planeje
Transforme objetivos em metas financeiras claras e mensuráveis.
📈
03Invista
Conheça as opções de investimento e o efeito dos juros compostos.
-SIMULADORVeja o poder dos juros compostos.
Faça uma projeção educativa do crescimento do seu patrimônio. Não é recomendação de investimento.
💡
Uma regra importanteTempo e aportes consistentes podem fazer grande diferença no longo prazo.
Total investidoR$ 0,00
RendimentosR$ 0,00
Valor projetadoR$ 0,00
-“Educação financeira não é sobre ficar rico rapidamente. É sobre fazer escolhas conscientes com o dinheiro que você tem.
-
+
+
+
+
+
+
+
+ Finanças+ | Educação Financeira
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
EDUCAÇÃO FINANCEIRA
+
+
+ Seu dinheiro deve trabalhar por você.
+
+
+
+ Aprenda a organizar suas finanças, tomar decisões melhores
+ e construir um futuro financeiro mais tranquilo.
+
+
+
+
+
+ ✓ Conteúdo educativo
+ ✓ Simulação gratuita
+ ✓ Área do aluno
+
+
+
+
+
+
+
+
+
+ Patrimônio projetado
+ ●
+
+
+
R$ 84.750,00
+
+
+
+
+ +18,4%
+ 12 meses
+
+
+
+
+
+ 🎯
+
+ Meta mensal
+ R$ 500
+
+
+
+
+ 💰
+
+ Economizado
+ R$ 7.280
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
PRIMEIROS PASSOS
+
+
+ Uma vida financeira melhor começa com três hábitos.
+
+
+
+ Conhecimento, organização e constância fazem mais diferença
+ do que procurar uma fórmula mágica.
+
+
+
+
+
+
+
+
+ 💰
+
+ 01
+
+ Organize
+
+
+ Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.
+
+
+
+
+
+
+ 📊
+
+ 02
+
+ Planeje
+
+
+ Transforme objetivos em metas financeiras claras e mensuráveis.
+
+
+
+
+
+
+ 📈
+
+ 03
+
+ Invista
+
+
+ Conheça as opções de investimento e o efeito dos juros compostos.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SIMULADOR
+
+
+ Veja o poder dos juros compostos.
+
+
+
+ Faça uma projeção educativa do crescimento do seu patrimônio.
+ Não é recomendação de investimento.
+
+
+
+
+ 💡
+
+
+
Uma regra importante
+
+
+ Tempo e aportes consistentes podem fazer grande diferença
+ no longo prazo.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total investido
+ R$ 0,00
+
+
+
+ Rendimentos
+ R$ 0,00
+
+
+
+ Valor projetado
+ R$ 0,00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
“
+
+
+ Educação financeira não é sobre ficar rico rapidamente.
+ É sobre fazer escolhas conscientes com o dinheiro que você tem.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..81872a6
--- /dev/null
+++ b/index.php
@@ -0,0 +1,472 @@
+
+
+
+
+
+
+
+
+
+ Finanças+ | Educação Financeira
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
EDUCAÇÃO FINANCEIRA
+
+
+ Seu dinheiro deve trabalhar por você.
+
+
+
+ Aprenda a organizar suas finanças, tomar decisões melhores
+ e construir um futuro financeiro mais tranquilo.
+
+
+
+
+
+ ✓ Conteúdo educativo
+ ✓ Simulação gratuita
+ ✓ Área do aluno
+
+
+
+
+
+
+
+
+
+ Patrimônio projetado
+ ●
+
+
+
R$ 84.750,00
+
+
+
+
+ +18,4%
+ 12 meses
+
+
+
+
+
+ 🎯
+
+ Meta mensal
+ R$ 500
+
+
+
+
+ 💰
+
+ Economizado
+ R$ 7.280
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
PRIMEIROS PASSOS
+
+
+ Uma vida financeira melhor começa com três hábitos.
+
+
+
+ Conhecimento, organização e constância fazem mais diferença
+ do que procurar uma fórmula mágica.
+
+
+
+
+
+
+
+
+ 💰
+
+ 01
+
+ Organize
+
+
+ Saiba quanto entra, quanto sai e para onde seu dinheiro está indo.
+
+
+
+
+
+
+ 📊
+
+ 02
+
+ Planeje
+
+
+ Transforme objetivos em metas financeiras claras e mensuráveis.
+
+
+
+
+
+
+ 📈
+
+ 03
+
+ Invista
+
+
+ Conheça as opções de investimento e o efeito dos juros compostos.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SIMULADOR
+
+
+ Veja o poder dos juros compostos.
+
+
+
+ Faça uma projeção educativa do crescimento do seu patrimônio.
+ Não é recomendação de investimento.
+
+
+
+
+ 💡
+
+
+
Uma regra importante
+
+
+ Tempo e aportes consistentes podem fazer grande diferença
+ no longo prazo.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total investido
+ R$ 0,00
+
+
+
+ Rendimentos
+ R$ 0,00
+
+
+
+ Valor projetado
+ R$ 0,00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
“
+
+
+ Educação financeira não é sobre ficar rico rapidamente.
+ É sobre fazer escolhas conscientes com o dinheiro que você tem.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..7576e03
--- /dev/null
+++ b/login.php
@@ -0,0 +1,45 @@
+login();
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..f2cd26b
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,42 @@
+logout();
diff --git a/storage/cache/.gitkeep b/storage/cache/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/storage/logs/.gitkeep b/storage/logs/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/test-ip.php b/test-ip.php
new file mode 100644
index 0000000..3af43c3
--- /dev/null
+++ b/test-ip.php
@@ -0,0 +1,10 @@
+
diff --git a/teste_de_push.txt b/teste_de_push.txt
deleted file mode 100644
index 56a68b5..0000000
--- a/teste_de_push.txt
+++ /dev/null
@@ -1 +0,0 @@
-testando o push deste arquivo para a conta romeritomelo no gitea na OCI.
\ No newline at end of file