commit 2df14d57dd6bd38a586b82bdf4b79e5d41a0560f Author: gustavooth Date: Thu Jul 23 20:32:08 2026 -0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bb2d9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Environment +.env + +# WordPress core +wp-includes/ +wp-admin/ +wp-content/languages/ +wp-content/upgrade/ +wp-content/backup-*/ + +# Default WordPress themes (not part of project) +wp-content/themes/twentytwentyfive/ +wp-content/themes/twentytwentyfour/ +wp-content/themes/twentytwentythree/ + +# Default WordPress plugins (not part of project) +wp-content/plugins/fluentform/ +wp-content/plugins/hello.php +wp-content/plugins/index.php + +# Project plugins/themes vendor deps +wp-content/plugins/*/vendor/ +wp-content/themes/*/node_modules/ +wp-content/themes/*/vendor/ + +# Logs +*.log +wp-content/debug.log + +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Docker +docker-compose.override.yml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..10d5736 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Gustavo Portfolio — Tema WordPress + +Tema WordPress clássico para o portfólio e blog de **Gustavo Oliveira** — [gustavoo.me](https://gustavoo.me) + +## Screenshot + +![Theme Screenshot](wp-content/themes/gustavoo-portfolio/screenshot.png) + +## Sobre + +Tema WordPress clássico desenvolvido para o portfólio pessoal e blog de Gustavo Oliveira, desenvolvedor Full Stack Sênior. O tema foi construído com foco em performance, acessibilidade e experiência do usuário, utilizando as melhores práticas do WordPress moderno. + +## Características + +- **Tema Clássico** — Template hierarchy tradicional do WordPress +- **Theme.json** — Configuração completa de estilos via theme.json (cores, tipografia, espaçamento, bordas) +- **Blocos Nativos** — Suporte completo a blocos do Gutenberg +- **Fluent Forms + FluentCRM** — Integração nativa para formulários e newsletter +- **Portfólio de Projetos** — CPT customizado para exibição de projetos +- **Newsletter Widget** — Widget personalizado para FluentCRM +- **SEO Ready** — Estrutura semântica e meta tags otimizadas +- **Performance** — Assets otimizados, fontes variáveis (Inter Variable), lazy loading nativo + +## Stack + +- **WordPress** 6.5+ +- **PHP** 8.1+ +- **Theme.json** v3 +- **Inter Variable Font** (local, self-hosted) +- **Fluent Forms** + **FluentCRM** (integração via plugin complementar) +- **Docker Compose** para desenvolvimento local + +## Estrutura do Tema + +``` +gustavoo-portfolio/ +├── assets/ +│ └── fonts/ # Inter Variable Font (woff2) +├── inc/ +│ ├── customizer.php # Personalizador do tema +│ ├── defaults.php # Configurações padrão (theme.json fallback) +│ ├── enqueue.php # Assets e estilos +│ ├── forms.php # Integração Fluent Forms +│ ├── setup.php # Configuração do tema +│ ├── template-tags.php # Template tags auxiliares +│ └── widgets.php # Widget Newsletter (FluentCRM) +├── template-parts/ # Partes de template reutilizáveis +├── 404.php +├── archive.php +├── category.php +├── comments.php +├── footer.php +├── front-page.php # Página inicial customizada +├── functions.php # Bootstrap do tema +├── header.php +├── home.php +├── index.php +├── page.php +├── search.php +├── searchform.php +├── sidebar.php +├── single.php +├── screenshot.png # Screenshot do tema (theme.png) +├── style.css # Stylesheet principal (metadados do tema) +└── theme.json # Configuração global de estilos +``` + +## Plugin Complementar + +O tema funciona em conjunto com o plugin **Gustavo Portfolio Core** (`wp-content/plugins/gustavoo-portfolio-core`), que provê: + +- **CPT Projetos** — Post type customizado para portfólio +- **Configurações Globais** — Opções de hero, sobre, projetos, blog, contato, newsletter e redes sociais +- **Seeder** — Criação automática de páginas, formulários Fluent Forms e feeds FluentCRM +- **Widget Newsletter** — Widget para exibir formulário de newsletter no sidebar + +## Desenvolvimento Local + +### Pré-requisitos + +O tema e plugin dependem dos seguintes plugins WordPress (instalados e ativados): + +- **Fluent Forms** — Formulários de contato e newsletter +- **FluentCRM** — Automação de marketing e newsletter + +```bash +# Subir ambiente Docker +docker compose up -d + +# Acessar WordPress +# http://localhost:8080 +# Usuário: admin | Senha: admin (definido no .env) + +# Instalar dependências (plugins obrigatórios) +# 1. Acesse http://localhost:8080/wp-admin +# 2. Plugins → Adicionar novo → Busque e instale: +# - Fluent Forms +# - FluentCRM +# 3. Ative ambos os plugins + +# Logs +docker compose logs -f wordpress +``` + +## Deploy + +O tema está preparado para deploy em ambientes WordPress padrão (VPS, shared hosting, WP Engine, Kinsta, etc.). + +1. Faça o build dos assets se necessário +2. Faça deploy da pasta `wp-content/themes/gustavoo-portfolio/` +3. Ative o tema no painel WordPress +4. Ative o plugin **Gustavo Portfolio Core** +5. Execute o seeder (Configurações → Portfólio → "Executar Seeder") para criar páginas e formulários padrão + +## Licença + +GNU General Public License v2 ou posterior — mesmo licença do WordPress. + +--- + +**Desenvolvido por [Gustavo Oliveira](https://gustavoo.me)** — Desenvolvedor Full Stack Sênior \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..2ceda7a --- /dev/null +++ b/compose.yaml @@ -0,0 +1,46 @@ +services: + db: + image: mariadb:11.8 + environment: + MARIADB_DATABASE: ${WORDPRESS_DB_NAME:-wordpress} + MARIADB_USER: ${WORDPRESS_DB_USER:-wordpress} + MARIADB_PASSWORD: ${WORDPRESS_DB_PASSWORD:?Defina WORDPRESS_DB_PASSWORD no arquivo .env} + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:?Defina MARIADB_ROOT_PASSWORD no arquivo .env} + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + wordpress: + container_name: gustavoo-me + image: wordpress:7-php8.3-apache + depends_on: + db: + condition: service_healthy + ports: + - "${WORDPRESS_PORT:-8080}:80" + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME:-wordpress} + WORDPRESS_DB_USER: ${WORDPRESS_DB_USER:-wordpress} + WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD} + volumes: + - wordpress_data:/var/www/html + - type: bind + source: ./wp-content/themes + target: /var/www/html/wp-content/themes + bind: + create_host_path: false + - type: bind + source: ./wp-content/plugins + target: /var/www/html/wp-content/plugins + bind: + create_host_path: false + +volumes: + db_data: + wordpress_data: diff --git a/wp-content/plugins/.gitkeep b/wp-content/plugins/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/wp-content/plugins/.gitkeep @@ -0,0 +1 @@ + diff --git a/wp-content/plugins/gustavoo-portfolio-core/gustavoo-portfolio-core.php b/wp-content/plugins/gustavoo-portfolio-core/gustavoo-portfolio-core.php new file mode 100644 index 0000000..a33b912 --- /dev/null +++ b/wp-content/plugins/gustavoo-portfolio-core/gustavoo-portfolio-core.php @@ -0,0 +1,90 @@ + + */ + function gso_portfolio_get_settings() { + return GSO_Portfolio_Settings::get_settings(); + } +} + +if ( ! function_exists( 'gso_portfolio_get_setting' ) ) { + /** + * Return one portfolio setting. + * + * @param string $key Setting key. + * @param mixed $default Fallback value. + * @return mixed + */ + function gso_portfolio_get_setting( $key, $default = null ) { + $settings = gso_portfolio_get_settings(); + + return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default; + } +} + +if ( ! function_exists( 'gso_render_fluent_form' ) ) { + /** + * Render a Fluent Form using a trusted numeric ID. + * + * @param int $form_id Fluent Forms form ID. + * @return string + */ + function gso_render_fluent_form( $form_id ) { + $form_id = absint( $form_id ); + + if ( ! $form_id || ! shortcode_exists( 'fluentform' ) ) { + return ''; + } + + return do_shortcode( sprintf( '[fluentform id="%d"]', $form_id ) ); + } +} + +if ( ! function_exists( 'gso_portfolio_render_fluent_form' ) ) { + /** + * Backwards-compatible descriptive alias for the form helper. + * + * @param int $form_id Fluent Forms form ID. + * @return string + */ + function gso_portfolio_render_fluent_form( $form_id ) { + return gso_render_fluent_form( $form_id ); + } +} diff --git a/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-newsletter-widget.php b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-newsletter-widget.php new file mode 100644 index 0000000..d071007 --- /dev/null +++ b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-newsletter-widget.php @@ -0,0 +1,110 @@ + 'widget_gso_newsletter', + 'description' => __( 'Exibe o formulário global de newsletter configurado no Portfólio.', 'gustavoo-portfolio-core' ), + 'customize_selective_refresh' => true, + ) + ); + } + + /** + * Render the widget. + * + * @param array $args Sidebar wrappers. + * @param array $instance Saved settings. + * @return void + */ + public function widget( $args, $instance ) { + $settings = GSO_Portfolio_Settings::get_settings(); + $title = ! empty( $instance['title'] ) ? (string) $instance['title'] : (string) $settings['newsletter_heading']; + $text = ! empty( $instance['text'] ) ? (string) $instance['text'] : (string) $settings['newsletter_text']; + $form_id = ! empty( $instance['form_id'] ) ? absint( $instance['form_id'] ) : absint( $settings['newsletter_form_id'] ); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + + if ( $title ) { + echo $args['before_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + echo esc_html( $title ); + echo $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + } + + if ( $text ) { + printf( '

%s

', esc_html( $text ) ); + } + + $form = gso_render_fluent_form( $form_id ); + if ( $form ) { + echo $form; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is generated by Fluent Forms. + } elseif ( current_user_can( 'manage_options' ) ) { + printf( + '

%2$s

', + esc_url( admin_url( 'edit.php?post_type=' . GSO_Projects::POST_TYPE . '&page=' . GSO_Portfolio_Settings::PAGE_SLUG ) ), + esc_html__( 'Configure o formulário de newsletter.', 'gustavoo-portfolio-core' ) + ); + } + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + } + + /** + * Render widget settings. + * + * @param array $instance Saved settings. + * @return void + */ + public function form( $instance ) { + $title = isset( $instance['title'] ) ? (string) $instance['title'] : ''; + $text = isset( $instance['text'] ) ? (string) $instance['text'] : ''; + $form_id = isset( $instance['form_id'] ) ? absint( $instance['form_id'] ) : 0; + ?> +

+ + +

+

+ + +

+

+ + +

+ $new_instance New values. + * @param array $old_instance Previous values. + * @return array + */ + public function update( $new_instance, $old_instance ) { + unset( $old_instance ); + + return array( + 'title' => isset( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '', + 'text' => isset( $new_instance['text'] ) ? sanitize_textarea_field( $new_instance['text'] ) : '', + 'form_id' => isset( $new_instance['form_id'] ) ? absint( $new_instance['form_id'] ) : 0, + ); + } +} diff --git a/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-portfolio-core.php b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-portfolio-core.php new file mode 100644 index 0000000..c5f330f --- /dev/null +++ b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-portfolio-core.php @@ -0,0 +1,89 @@ + + */ + public static function defaults() { + return array( + 'hero_eyebrow' => __( 'Desenvolvimento de ponta a ponta', 'gustavoo-portfolio-core' ), + 'hero_title' => __( 'Transformo ideias em produtos digitais sólidos.', 'gustavoo-portfolio-core' ), + 'hero_description' => __( 'Gustavo, Desenvolvedor Full Stack Sênior. Web, mobile, infraestrutura e automação para tirar projetos do papel e fazê-los crescer.', 'gustavoo-portfolio-core' ), + 'hero_primary_label' => __( 'Ver projetos', 'gustavoo-portfolio-core' ), + 'hero_primary_url' => '#projetos', + 'hero_secondary_label' => __( 'Fale comigo', 'gustavoo-portfolio-core' ), + 'hero_secondary_url' => '#contato', + 'about_title' => __( 'Responsabilidade técnica do planejamento à produção.', 'gustavoo-portfolio-core' ), + 'about_text' => __( 'Mais do que escrever código, meu objetivo é garantir que seu projeto saia do papel e funcione exatamente como planejado. Assumo a responsabilidade técnica de ponta a ponta para que você foque no que importa: crescer.', 'gustavoo-portfolio-core' ), + 'about_secondary_text' => __( 'Já participei do desenvolvimento e lançamento de múltiplos projetos digitais, atuando da arquitetura técnica à divulgação e ao crescimento das plataformas. Essa combinação de visão técnica e entendimento de mercado orienta cada entrega.', 'gustavoo-portfolio-core' ), + 'contact_heading' => __( 'Tem um projeto em mente?', 'gustavoo-portfolio-core' ), + 'contact_text' => __( 'Conte o que você precisa construir ou melhorar. Responderei com os próximos passos para transformar a ideia em uma entrega concreta.', 'gustavoo-portfolio-core' ), + 'contact_form_id' => 0, + 'newsletter_heading' => __( 'Ideias úteis, direto na sua caixa de entrada.', 'gustavoo-portfolio-core' ), + 'newsletter_text' => __( 'Receba conteúdos sobre desenvolvimento, produto, infraestrutura e automação. Sem ruído e sem spam.', 'gustavoo-portfolio-core' ), + 'newsletter_form_id' => 0, + 'projects_title' => __( 'Projetos selecionados', 'gustavoo-portfolio-core' ), + 'projects_text' => __( 'Uma seleção de produtos, experiências e soluções que ajudei a colocar no mundo.', 'gustavoo-portfolio-core' ), + 'projects_limit' => 6, + 'blog_title' => __( 'Código, produto e bastidores', 'gustavoo-portfolio-core' ), + 'blog_text' => __( 'Análises práticas sobre desenvolvimento, infraestrutura, automação e crescimento de produtos digitais.', 'gustavoo-portfolio-core' ), + 'blog_limit' => 4, + 'email' => 'contato@gustavoo.me', + 'whatsapp' => '+55 31 99516-8069', + 'whatsapp_message' => __( 'Olá! Gostaria de falar sobre um projeto.', 'gustavoo-portfolio-core' ), + 'github_url' => 'https://github.com/gustavooth', + 'linkedin_url' => '', + 'instagram_url' => '', + 'availability_label' => __( 'Disponível para novos projetos', 'gustavoo-portfolio-core' ), + ); + } + + /** + * Add missing defaults without overwriting existing values. + * + * @return void + */ + public static function ensure_defaults() { + $existing = get_option( self::OPTION_NAME, null ); + + if ( null === $existing || ! is_array( $existing ) ) { + add_option( self::OPTION_NAME, self::defaults(), '', false ); + return; + } + + $merged = array_merge( self::defaults(), $existing ); + if ( $merged !== $existing ) { + update_option( self::OPTION_NAME, $merged, false ); + } + } + + /** + * Return settings merged with defaults. + * + * @return array + */ + public static function get_settings() { + $settings = get_option( self::OPTION_NAME, array() ); + + return array_merge( self::defaults(), is_array( $settings ) ? $settings : array() ); + } + + /** + * Add the Settings submenu below Portfolio. + * + * @return void + */ + public static function add_settings_page() { + add_submenu_page( + 'edit.php?post_type=' . GSO_Projects::POST_TYPE, + __( 'Configurações do portfólio', 'gustavoo-portfolio-core' ), + __( 'Configurações', 'gustavoo-portfolio-core' ), + 'manage_options', + self::PAGE_SLUG, + array( __CLASS__, 'render_settings_page' ) + ); + } + + /** + * Register the option, sections and fields. + * + * @return void + */ + public static function register_settings() { + register_setting( + self::OPTION_GROUP, + self::OPTION_NAME, + array( + 'type' => 'array', + 'description' => __( 'Configurações compartilhadas pelo tema Gustavo Portfolio.', 'gustavoo-portfolio-core' ), + 'sanitize_callback' => array( __CLASS__, 'sanitize_settings' ), + 'default' => self::defaults(), + ) + ); + + $sections = self::sections(); + $fields = self::fields(); + + foreach ( $sections as $section_id => $section ) { + add_settings_section( + $section_id, + $section['title'], + array( __CLASS__, 'render_section_description' ), + self::PAGE_SLUG, + array( 'description' => $section['description'] ) + ); + } + + foreach ( $fields as $key => $field ) { + add_settings_field( + 'gso-setting-' . str_replace( '_', '-', $key ), + $field['label'], + array( __CLASS__, 'render_field' ), + self::PAGE_SLUG, + $field['section'], + array_merge( $field, array( 'key' => $key ) ) + ); + } + } + + /** + * Settings page section definitions. + * + * @return array> + */ + private static function sections() { + return array( + 'gso-settings-hero' => array( + 'title' => __( 'Hero', 'gustavoo-portfolio-core' ), + 'description' => __( 'Mensagem principal e chamadas para ação da página inicial.', 'gustavoo-portfolio-core' ), + ), + 'gso-settings-about' => array( + 'title' => __( 'Sobre', 'gustavoo-portfolio-core' ), + 'description' => __( 'Apresentação profissional exibida na seção Sobre.', 'gustavoo-portfolio-core' ), + ), + 'gso-settings-content' => array( + 'title' => __( 'Projetos e blog', 'gustavoo-portfolio-core' ), + 'description' => __( 'Títulos, textos de apoio e limites das listagens da página inicial.', 'gustavoo-portfolio-core' ), + ), + 'gso-settings-contact' => array( + 'title' => __( 'Contato', 'gustavoo-portfolio-core' ), + 'description' => __( 'Canais diretos e formulário principal do Fluent Forms.', 'gustavoo-portfolio-core' ), + ), + 'gso-settings-newsletter' => array( + 'title' => __( 'Newsletter', 'gustavoo-portfolio-core' ), + 'description' => __( 'Conteúdo e formulário usados pelo CTA e pelo widget de newsletter.', 'gustavoo-portfolio-core' ), + ), + 'gso-settings-social' => array( + 'title' => __( 'Redes sociais', 'gustavoo-portfolio-core' ), + 'description' => __( 'Perfis públicos exibidos no tema.', 'gustavoo-portfolio-core' ), + ), + ); + } + + /** + * Settings field schema. + * + * @return array> + */ + private static function fields() { + return array( + 'hero_eyebrow' => self::field( 'gso-settings-hero', __( 'Sobretítulo', 'gustavoo-portfolio-core' ), 'text' ), + 'hero_title' => self::field( 'gso-settings-hero', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ), + 'hero_description' => self::field( 'gso-settings-hero', __( 'Descrição', 'gustavoo-portfolio-core' ), 'textarea' ), + 'hero_primary_label' => self::field( 'gso-settings-hero', __( 'CTA principal — texto', 'gustavoo-portfolio-core' ), 'text' ), + 'hero_primary_url' => self::field( 'gso-settings-hero', __( 'CTA principal — destino', 'gustavoo-portfolio-core' ), 'link', __( 'Aceita URL HTTP(S), mailto:, tel:, caminho relativo ou âncora como #projetos.', 'gustavoo-portfolio-core' ) ), + 'hero_secondary_label' => self::field( 'gso-settings-hero', __( 'CTA secundário — texto', 'gustavoo-portfolio-core' ), 'text' ), + 'hero_secondary_url' => self::field( 'gso-settings-hero', __( 'CTA secundário — destino', 'gustavoo-portfolio-core' ), 'link', __( 'Aceita URL HTTP(S), mailto:, tel:, caminho relativo ou âncora.', 'gustavoo-portfolio-core' ) ), + 'about_title' => self::field( 'gso-settings-about', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ), + 'about_text' => self::field( 'gso-settings-about', __( 'Texto', 'gustavoo-portfolio-core' ), 'textarea' ), + 'projects_title' => self::field( 'gso-settings-content', __( 'Projetos — título', 'gustavoo-portfolio-core' ), 'text' ), + 'projects_text' => self::field( 'gso-settings-content', __( 'Projetos — texto', 'gustavoo-portfolio-core' ), 'textarea' ), + 'projects_limit' => self::field( 'gso-settings-content', __( 'Projetos — quantidade', 'gustavoo-portfolio-core' ), 'number', __( 'Entre 1 e 24.', 'gustavoo-portfolio-core' ), array( 'min' => 1, 'max' => 24 ) ), + 'blog_title' => self::field( 'gso-settings-content', __( 'Blog — título', 'gustavoo-portfolio-core' ), 'text' ), + 'blog_text' => self::field( 'gso-settings-content', __( 'Blog — texto', 'gustavoo-portfolio-core' ), 'textarea' ), + 'blog_limit' => self::field( 'gso-settings-content', __( 'Blog — quantidade', 'gustavoo-portfolio-core' ), 'number', __( 'Entre 1 e 24.', 'gustavoo-portfolio-core' ), array( 'min' => 1, 'max' => 24 ) ), + 'contact_heading' => self::field( 'gso-settings-contact', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ), + 'contact_text' => self::field( 'gso-settings-contact', __( 'Texto', 'gustavoo-portfolio-core' ), 'textarea' ), + 'contact_form_id' => self::field( 'gso-settings-contact', __( 'ID do formulário de contato', 'gustavoo-portfolio-core' ), 'number', __( 'ID numérico do Fluent Forms. O seed preenche este campo quando possível.', 'gustavoo-portfolio-core' ), array( 'min' => 0 ) ), + 'email' => self::field( 'gso-settings-contact', __( 'E-mail', 'gustavoo-portfolio-core' ), 'email' ), + 'whatsapp' => self::field( 'gso-settings-contact', __( 'WhatsApp', 'gustavoo-portfolio-core' ), 'tel', __( 'Inclua o DDI, por exemplo +55 31 99999-9999.', 'gustavoo-portfolio-core' ) ), + 'whatsapp_message' => self::field( 'gso-settings-contact', __( 'WhatsApp — mensagem inicial', 'gustavoo-portfolio-core' ), 'textarea', __( 'Texto que será preenchido automaticamente ao abrir o botão flutuante.', 'gustavoo-portfolio-core' ) ), + 'newsletter_heading' => self::field( 'gso-settings-newsletter', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ), + 'newsletter_text' => self::field( 'gso-settings-newsletter', __( 'Texto', 'gustavoo-portfolio-core' ), 'textarea' ), + 'newsletter_form_id' => self::field( 'gso-settings-newsletter', __( 'ID do formulário de newsletter', 'gustavoo-portfolio-core' ), 'number', __( 'ID numérico do Fluent Forms. O seed preenche este campo quando possível.', 'gustavoo-portfolio-core' ), array( 'min' => 0 ) ), + 'github_url' => self::field( 'gso-settings-social', __( 'GitHub', 'gustavoo-portfolio-core' ), 'url' ), + 'linkedin_url' => self::field( 'gso-settings-social', __( 'LinkedIn', 'gustavoo-portfolio-core' ), 'url' ), + 'instagram_url' => self::field( 'gso-settings-social', __( 'Instagram', 'gustavoo-portfolio-core' ), 'url' ), + ); + } + + /** + * Build one field definition. + * + * @param string $section Section ID. + * @param string $label Label. + * @param string $type Field type. + * @param string $description Help text. + * @param array $extra Extra attributes. + * @return array + */ + private static function field( $section, $label, $type, $description = '', $extra = array() ) { + return array_merge( + array( + 'section' => $section, + 'label' => $label, + 'type' => $type, + 'description' => $description, + ), + $extra + ); + } + + /** + * Sanitize every documented setting and retain unknown extension keys. + * + * @param mixed $input Raw submitted option. + * @return array + */ + public static function sanitize_settings( $input ) { + $current = get_option( self::OPTION_NAME, array() ); + $output = array_merge( self::defaults(), is_array( $current ) ? $current : array() ); + $input = is_array( $input ) ? $input : array(); + + foreach ( self::fields() as $key => $field ) { + $value = $input[ $key ] ?? ''; + + switch ( $field['type'] ) { + case 'textarea': + $output[ $key ] = sanitize_textarea_field( $value ); + break; + case 'email': + $output[ $key ] = sanitize_email( $value ); + break; + case 'tel': + $output[ $key ] = self::sanitize_phone( $value ); + break; + case 'url': + $output[ $key ] = self::sanitize_public_url( $value ); + break; + case 'link': + $output[ $key ] = self::sanitize_link( $value ); + break; + case 'number': + $number = absint( $value ); + $min = isset( $field['min'] ) ? absint( $field['min'] ) : 0; + $max = isset( $field['max'] ) ? absint( $field['max'] ) : PHP_INT_MAX; + $output[ $key ] = min( $max, max( $min, $number ) ); + break; + default: + $output[ $key ] = sanitize_text_field( $value ); + } + } + + return $output; + } + + /** + * Sanitize a public social URL. + * + * @param mixed $value Raw value. + * @return string + */ + private static function sanitize_public_url( $value ) { + $url = esc_url_raw( trim( (string) $value ), array( 'http', 'https' ) ); + + return $url && wp_http_validate_url( $url ) ? $url : ''; + } + + /** + * Sanitize a CTA link, including safe same-page anchors. + * + * @param mixed $value Raw value. + * @return string + */ + private static function sanitize_link( $value ) { + $value = trim( sanitize_text_field( $value ) ); + + if ( preg_match( '/^#[A-Za-z][A-Za-z0-9_:.\-]*$/', $value ) ) { + return $value; + } + + return esc_url_raw( $value, array( 'http', 'https', 'mailto', 'tel' ) ); + } + + /** + * Keep common telephone punctuation and discard everything else. + * + * @param mixed $value Raw phone value. + * @return string + */ + private static function sanitize_phone( $value ) { + $value = sanitize_text_field( $value ); + $value = preg_replace( '/[^0-9+()\-\s]/', '', $value ); + + return is_string( $value ) ? trim( $value ) : ''; + } + + /** + * Render a section introduction. + * + * @param array $args Section arguments. + * @return void + */ + public static function render_section_description( $args ) { + if ( ! empty( $args['description'] ) ) { + echo '

' . esc_html( $args['description'] ) . '

'; + } + } + + /** + * Render a settings field from its schema. + * + * @param array $args Field arguments. + * @return void + */ + public static function render_field( $args ) { + $settings = self::get_settings(); + $key = $args['key']; + $value = $settings[ $key ] ?? ''; + $id = 'gso-setting-' . str_replace( '_', '-', $key ); + $name = self::OPTION_NAME . '[' . $key . ']'; + + if ( 'textarea' === $args['type'] ) { + printf( + '', + esc_attr( $id ), + esc_attr( $name ), + esc_textarea( (string) $value ) + ); + } else { + $html_type = in_array( $args['type'], array( 'email', 'number', 'tel', 'url' ), true ) ? $args['type'] : 'text'; + $attrs = ''; + + if ( isset( $args['min'] ) ) { + $attrs .= ' min="' . esc_attr( (string) $args['min'] ) . '"'; + } + if ( isset( $args['max'] ) ) { + $attrs .= ' max="' . esc_attr( (string) $args['max'] ) . '"'; + } + + printf( + '', + 'number' === $html_type ? 'small-text' : 'regular-text', + esc_attr( $id ), + esc_attr( $name ), + esc_attr( $html_type ), + esc_attr( (string) $value ), + $attrs // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes are assembled from escaped integers. + ); + } + + if ( ! empty( $args['description'] ) ) { + echo '

' . esc_html( $args['description'] ) . '

'; + } + } + + /** + * Render the admin settings page. + * + * @return void + */ + public static function render_settings_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + ?> +
+

+

+ +
+ +
+
+ +
+

+
    +
  • +
  • +
  • +
  • +
+
+ $links Existing links. + * @return array + */ + public static function plugin_action_links( $links ) { + $url = admin_url( 'edit.php?post_type=' . GSO_Projects::POST_TYPE . '&page=' . self::PAGE_SLUG ); + + array_unshift( + $links, + '' . esc_html__( 'Configurações', 'gustavoo-portfolio-core' ) . '' + ); + + return $links; + } +} diff --git a/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-projects.php b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-projects.php new file mode 100644 index 0000000..f836170 --- /dev/null +++ b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-projects.php @@ -0,0 +1,563 @@ + array( + 'name' => __( 'Projetos', 'gustavoo-portfolio-core' ), + 'singular_name' => __( 'Projeto', 'gustavoo-portfolio-core' ), + 'menu_name' => __( 'Portfólio', 'gustavoo-portfolio-core' ), + 'name_admin_bar' => __( 'Projeto', 'gustavoo-portfolio-core' ), + 'add_new' => __( 'Adicionar projeto', 'gustavoo-portfolio-core' ), + 'add_new_item' => __( 'Adicionar novo projeto', 'gustavoo-portfolio-core' ), + 'edit_item' => __( 'Editar projeto', 'gustavoo-portfolio-core' ), + 'new_item' => __( 'Novo projeto', 'gustavoo-portfolio-core' ), + 'view_item' => __( 'Ver projeto', 'gustavoo-portfolio-core' ), + 'search_items' => __( 'Buscar projetos', 'gustavoo-portfolio-core' ), + 'not_found' => __( 'Nenhum projeto encontrado.', 'gustavoo-portfolio-core' ), + 'not_found_in_trash' => __( 'Nenhum projeto encontrado na lixeira.', 'gustavoo-portfolio-core' ), + 'all_items' => __( 'Projetos', 'gustavoo-portfolio-core' ), + 'featured_image' => __( 'Imagem do projeto', 'gustavoo-portfolio-core' ), + 'set_featured_image' => __( 'Definir imagem do projeto', 'gustavoo-portfolio-core' ), + 'remove_featured_image' => __( 'Remover imagem do projeto', 'gustavoo-portfolio-core' ), + ), + 'public' => false, + 'publicly_queryable' => false, + 'exclude_from_search' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_admin_bar' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => true, + 'menu_position' => 25, + 'menu_icon' => 'dashicons-portfolio', + 'capability_type' => 'post', + 'map_meta_cap' => true, + 'hierarchical' => false, + 'has_archive' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes', 'revisions', 'custom-fields' ), + ) + ); + + register_taxonomy( + self::TAX_TYPE, + self::POST_TYPE, + array( + 'labels' => array( + 'name' => __( 'Tipos de projeto', 'gustavoo-portfolio-core' ), + 'singular_name' => __( 'Tipo de projeto', 'gustavoo-portfolio-core' ), + 'menu_name' => __( 'Tipos', 'gustavoo-portfolio-core' ), + 'all_items' => __( 'Todos os tipos', 'gustavoo-portfolio-core' ), + 'edit_item' => __( 'Editar tipo', 'gustavoo-portfolio-core' ), + 'add_new_item' => __( 'Adicionar tipo', 'gustavoo-portfolio-core' ), + 'search_items' => __( 'Buscar tipos', 'gustavoo-portfolio-core' ), + ), + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => true, + 'show_admin_column' => false, + 'show_in_rest' => true, + 'hierarchical' => true, + 'rewrite' => false, + ) + ); + + register_taxonomy( + self::TAX_TECH, + self::POST_TYPE, + array( + 'labels' => array( + 'name' => __( 'Tecnologias', 'gustavoo-portfolio-core' ), + 'singular_name' => __( 'Tecnologia', 'gustavoo-portfolio-core' ), + 'menu_name' => __( 'Tecnologias', 'gustavoo-portfolio-core' ), + 'all_items' => __( 'Todas as tecnologias', 'gustavoo-portfolio-core' ), + 'edit_item' => __( 'Editar tecnologia', 'gustavoo-portfolio-core' ), + 'add_new_item' => __( 'Adicionar tecnologia', 'gustavoo-portfolio-core' ), + 'search_items' => __( 'Buscar tecnologias', 'gustavoo-portfolio-core' ), + 'separate_items_with_commas' => __( 'Separe tecnologias com vírgulas', 'gustavoo-portfolio-core' ), + ), + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => true, + 'show_admin_column' => false, + 'show_in_rest' => true, + 'hierarchical' => false, + 'rewrite' => false, + ) + ); + + self::register_meta(); + } + + /** + * Register project metadata. + * + * @return void + */ + private static function register_meta() { + $common = array( + 'single' => true, + 'show_in_rest' => true, + 'auth_callback' => array( __CLASS__, 'authorize_meta' ), + ); + + register_post_meta( + self::POST_TYPE, + self::META_URL, + array_merge( + $common, + array( + 'type' => 'string', + 'sanitize_callback' => array( __CLASS__, 'sanitize_external_url' ), + ) + ) + ); + + foreach ( array( self::META_CLIENT, self::META_LINK_LABEL ) as $meta_key ) { + register_post_meta( + self::POST_TYPE, + $meta_key, + array_merge( + $common, + array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ) + ) + ); + } + + register_post_meta( + self::POST_TYPE, + self::META_YEAR, + array_merge( + $common, + array( + 'type' => 'integer', + 'sanitize_callback' => array( __CLASS__, 'sanitize_year' ), + ) + ) + ); + + register_post_meta( + self::POST_TYPE, + self::META_FEATURED, + array_merge( + $common, + array( + 'type' => 'boolean', + 'sanitize_callback' => 'rest_sanitize_boolean', + ) + ) + ); + + register_post_meta( + self::POST_TYPE, + self::META_ACCENT, + array_merge( + $common, + array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_hex_color', + ) + ) + ); + } + + /** + * Restrict meta updates to users who can edit the project. + * + * @param bool $allowed Current authorization result. + * @param string $meta_key Meta key. + * @param int $post_id Project ID. + * @return bool + */ + public static function authorize_meta( $allowed, $meta_key, $post_id ) { + unset( $allowed, $meta_key ); + + return current_user_can( 'edit_post', (int) $post_id ); + } + + /** + * Allow only valid external HTTP(S) URLs. + * + * @param mixed $value Raw URL. + * @return string + */ + public static function sanitize_external_url( $value ) { + $url = esc_url_raw( trim( (string) $value ), array( 'http', 'https' ) ); + + return $url && wp_http_validate_url( $url ) ? $url : ''; + } + + /** + * Validate a four-digit project year. + * + * @param mixed $value Raw year. + * @return int + */ + public static function sanitize_year( $value ) { + $year = absint( $value ); + $max_year = (int) gmdate( 'Y' ) + 5; + + return $year >= 1900 && $year <= $max_year ? $year : 0; + } + + /** + * Register the project details metabox. + * + * @return void + */ + public static function add_meta_boxes() { + add_meta_box( + 'gso-project-details', + __( 'Detalhes do projeto', 'gustavoo-portfolio-core' ), + array( __CLASS__, 'render_meta_box' ), + self::POST_TYPE, + 'normal', + 'high' + ); + } + + /** + * Render project fields. + * + * @param WP_Post $post Current project. + * @return void + */ + public static function render_meta_box( $post ) { + $url = (string) get_post_meta( $post->ID, self::META_URL, true ); + $client = (string) get_post_meta( $post->ID, self::META_CLIENT, true ); + $year = absint( get_post_meta( $post->ID, self::META_YEAR, true ) ); + $featured = (bool) get_post_meta( $post->ID, self::META_FEATURED, true ); + $link_label = (string) get_post_meta( $post->ID, self::META_LINK_LABEL, true ); + $accent = sanitize_hex_color( get_post_meta( $post->ID, self::META_ACCENT, true ) ); + + if ( ! $accent ) { + $accent = '#7cf3da'; + } + + wp_nonce_field( 'gso_save_project_details', 'gso_project_details_nonce' ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $columns Existing columns. + * @return array + */ + public static function admin_columns( $columns ) { + return array( + 'cb' => $columns['cb'] ?? '', + 'gso_thumbnail' => __( 'Imagem', 'gustavoo-portfolio-core' ), + 'title' => __( 'Projeto', 'gustavoo-portfolio-core' ), + 'gso_client' => __( 'Cliente', 'gustavoo-portfolio-core' ), + 'gso_type' => __( 'Tipo', 'gustavoo-portfolio-core' ), + 'gso_tech' => __( 'Tecnologias', 'gustavoo-portfolio-core' ), + 'gso_year' => __( 'Ano', 'gustavoo-portfolio-core' ), + 'gso_featured' => __( 'Destaque', 'gustavoo-portfolio-core' ), + 'gso_url' => __( 'URL', 'gustavoo-portfolio-core' ), + 'gso_menu_order' => __( 'Ordem', 'gustavoo-portfolio-core' ), + 'date' => $columns['date'] ?? __( 'Data', 'gustavoo-portfolio-core' ), + ); + } + + /** + * Render one project list cell. + * + * @param string $column Column ID. + * @param int $post_id Project ID. + * @return void + */ + public static function render_admin_column( $column, $post_id ) { + switch ( $column ) { + case 'gso_thumbnail': + if ( has_post_thumbnail( $post_id ) ) { + echo wp_kses_post( get_the_post_thumbnail( $post_id, array( 64, 48 ), array( 'style' => 'width:64px;height:48px;object-fit:cover;border-radius:4px;' ) ) ); + } else { + echo '' . esc_html__( 'Sem imagem', 'gustavoo-portfolio-core' ) . ''; + } + break; + case 'gso_client': + echo esc_html( (string) get_post_meta( $post_id, self::META_CLIENT, true ) ?: '—' ); + break; + case 'gso_type': + self::render_terms_column( $post_id, self::TAX_TYPE ); + break; + case 'gso_tech': + self::render_terms_column( $post_id, self::TAX_TECH ); + break; + case 'gso_year': + $year = absint( get_post_meta( $post_id, self::META_YEAR, true ) ); + echo $year ? esc_html( (string) $year ) : '—'; + break; + case 'gso_featured': + if ( get_post_meta( $post_id, self::META_FEATURED, true ) ) { + echo '' . esc_html__( 'Sim', 'gustavoo-portfolio-core' ) . ''; + } else { + echo '' . esc_html__( 'Não', 'gustavoo-portfolio-core' ) . ''; + } + break; + case 'gso_url': + $url = self::sanitize_external_url( get_post_meta( $post_id, self::META_URL, true ) ); + if ( $url ) { + $host = wp_parse_url( $url, PHP_URL_HOST ); + printf( + '%2$s %3$s', + esc_url( $url ), + esc_html( $host ?: $url ), + esc_html__( '(abre em nova aba)', 'gustavoo-portfolio-core' ) + ); + } else { + echo '—'; + } + break; + case 'gso_menu_order': + echo esc_html( (string) get_post_field( 'menu_order', $post_id ) ); + break; + } + } + + /** + * Render taxonomy terms in an admin column. + * + * @param int $post_id Project ID. + * @param string $taxonomy Taxonomy name. + * @return void + */ + private static function render_terms_column( $post_id, $taxonomy ) { + $terms = get_the_terms( $post_id, $taxonomy ); + + if ( ! $terms || is_wp_error( $terms ) ) { + echo '—'; + return; + } + + echo esc_html( implode( ', ', wp_list_pluck( $terms, 'name' ) ) ); + } + + /** + * Make selected columns sortable. + * + * @param array $columns Existing sortable columns. + * @return array + */ + public static function sortable_columns( $columns ) { + $columns['gso_client'] = 'gso_client'; + $columns['gso_year'] = 'gso_year'; + $columns['gso_featured'] = 'gso_featured'; + $columns['gso_menu_order'] = 'menu_order'; + + return $columns; + } + + /** + * Apply safe sorting to the main project admin query. + * + * @param WP_Query $query Current query. + * @return void + */ + public static function apply_admin_sorting( $query ) { + if ( ! is_admin() || ! $query->is_main_query() || self::POST_TYPE !== $query->get( 'post_type' ) ) { + return; + } + + switch ( $query->get( 'orderby' ) ) { + case 'gso_client': + $query->set( 'meta_key', self::META_CLIENT ); + $query->set( 'orderby', 'meta_value' ); + break; + case 'gso_year': + $query->set( 'meta_key', self::META_YEAR ); + $query->set( 'orderby', 'meta_value_num' ); + break; + case 'gso_featured': + $query->set( 'meta_key', self::META_FEATURED ); + $query->set( 'orderby', 'meta_value_num' ); + break; + case 'menu_order': + $query->set( 'orderby', 'menu_order title' ); + break; + } + } + + /** + * Add type and technology filters above the project list. + * + * @param string $post_type Current list post type. + * @return void + */ + public static function taxonomy_filters( $post_type ) { + if ( self::POST_TYPE !== $post_type ) { + return; + } + + foreach ( array( self::TAX_TYPE, self::TAX_TECH ) as $taxonomy ) { + $tax_object = get_taxonomy( $taxonomy ); + if ( ! $tax_object ) { + continue; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only list filter. + $selected = isset( $_GET[ $taxonomy ] ) ? sanitize_title( wp_unslash( $_GET[ $taxonomy ] ) ) : ''; + + wp_dropdown_categories( + array( + 'show_option_all' => sprintf( + /* translators: %s: taxonomy label. */ + __( 'Todos: %s', 'gustavoo-portfolio-core' ), + $tax_object->labels->name + ), + 'taxonomy' => $taxonomy, + 'name' => $taxonomy, + 'orderby' => 'name', + 'selected' => $selected, + 'hide_empty' => false, + 'hierarchical' => $tax_object->hierarchical, + 'value_field' => 'slug', + ) + ); + } + } +} diff --git a/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-seeder.php b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-seeder.php new file mode 100644 index 0000000..2cc110f --- /dev/null +++ b/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-seeder.php @@ -0,0 +1,619 @@ + + */ + public static function get_state() { + $state = get_option( self::STATE_OPTION, array() ); + + return wp_parse_args( + is_array( $state ) ? $state : array(), + array( + 'version' => '', + 'status' => 'pending', + 'projects_seeded' => false, + 'contact_form_id' => 0, + 'newsletter_form_id' => 0, + 'crm_feeds' => false, + 'message' => '', + ) + ); + } + + /** + * Complete integration setup when Fluent Forms and FluentCRM are ready. + * + * @return void + */ + public static function maybe_seed() { + self::seed_site_configuration(); + + $state = self::get_state(); + + if ( self::VERSION === $state['version'] && 'complete' === $state['status'] ) { + self::maybe_migrate_contact_form(); + return; + } + + if ( ! post_type_exists( GSO_Projects::POST_TYPE ) ) { + GSO_Projects::register_content_types(); + } + + self::seed_projects(); + + if ( ! self::fluent_is_ready() ) { + $state['projects_seeded'] = true; + $state['status'] = 'waiting'; + $state['message'] = __( 'Aguardando Fluent Forms e FluentCRM ativos.', 'gustavoo-portfolio-core' ); + update_option( self::STATE_OPTION, $state, false ); + return; + } + + if ( ! add_option( self::LOCK_OPTION, time(), '', false ) ) { + return; + } + + try { + $contact_id = self::upsert_form( 'contact', __( 'Site — Contato e orçamento', 'gustavoo-portfolio-core' ), true ); + $newsletter_id = self::upsert_form( 'newsletter', __( 'Site — Newsletter', 'gustavoo-portfolio-core' ), false ); + + self::upsert_crm_feed( $contact_id, 'contact' ); + self::upsert_crm_feed( $newsletter_id, 'newsletter' ); + self::save_form_ids( $contact_id, $newsletter_id ); + + update_option( + self::STATE_OPTION, + array( + 'version' => self::VERSION, + 'status' => 'complete', + 'projects_seeded' => true, + 'contact_form_id' => $contact_id, + 'newsletter_form_id' => $newsletter_id, + 'crm_feeds' => true, + 'message' => __( 'Formulários e feeds do FluentCRM configurados.', 'gustavoo-portfolio-core' ), + ), + false + ); + } catch ( Throwable $error ) { + $state['status'] = 'error'; + $state['message'] = sanitize_text_field( $error->getMessage() ); + update_option( self::STATE_OPTION, $state, false ); + } finally { + delete_option( self::LOCK_OPTION ); + } + } + + /** + * Configure the WordPress pieces that make the bundled theme work on a + * fresh installation. + * + * Existing sites are left untouched: each value is written only when the + * corresponding WordPress setting still has its fresh-install value. + * + * @return void + */ + private static function seed_site_configuration() { + $state = get_option( self::SITE_OPTION, array() ); + + if ( ! is_array( $state ) || empty( $state['pages'] ) ) { + $home_id = self::ensure_seed_page( 'inicio', __( 'Início', 'gustavoo-portfolio-core' ) ); + $blog_id = self::ensure_seed_page( 'blog', __( 'Blog', 'gustavoo-portfolio-core' ) ); + + if ( $home_id && $blog_id && 'posts' === get_option( 'show_on_front' ) ) { + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $home_id ); + update_option( 'page_for_posts', $blog_id ); + } + + if ( $home_id && $blog_id ) { + $state = is_array( $state ) ? $state : array(); + $state['pages'] = true; + update_option( self::SITE_OPTION, $state, false ); + } + } + + if ( empty( $state['widgets'] ) ) { + self::seed_theme_widgets(); + $state = is_array( $state ) ? $state : array(); + $state['widgets'] = true; + update_option( self::SITE_OPTION, $state, false ); + } + } + + /** + * Create a required empty page once and identify it by a private marker. + * + * @param string $slug Page slug. + * @param string $title Page title. + * @return int + */ + private static function ensure_seed_page( $slug, $title ) { + $existing = get_posts( + array( + 'post_type' => 'page', + 'post_status' => 'any', + 'posts_per_page' => 1, + 'fields' => 'ids', + 'meta_key' => '_gso_seed_page', + 'meta_value' => $slug, + ) + ); + + if ( $existing ) { + return absint( $existing[0] ); + } + + $page = get_page_by_path( $slug, OBJECT, 'page' ); + if ( $page ) { + update_post_meta( $page->ID, '_gso_seed_page', $slug ); + return absint( $page->ID ); + } + + $page_id = wp_insert_post( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_title' => $title, + 'post_name' => $slug, + ), + true + ); + + if ( is_wp_error( $page_id ) ) { + return 0; + } + + update_post_meta( $page_id, '_gso_seed_page', $slug ); + return absint( $page_id ); + } + + /** + * Reproduce the sidebar and footer widget arrangement from this site. + * Widgets are added only to empty areas, so a user's arrangement wins. + * + * @return void + */ + private static function seed_theme_widgets() { + $sidebars = wp_get_sidebars_widgets(); + $sidebars = is_array( $sidebars ) ? $sidebars : array(); + + $blog_widgets = isset( $sidebars['sidebar-blog'] ) ? (array) $sidebars['sidebar-blog'] : array(); + $can_seed_blog = empty( $blog_widgets ) || array( 'gustavoo_portfolio_newsletter-1' ) === $blog_widgets; + + if ( $can_seed_blog ) { + $newsletter = get_option( 'widget_gustavoo_portfolio_newsletter', array() ); + $newsletter = is_array( $newsletter ) ? $newsletter : array(); + $newsletter[1] = array( + 'title' => __( 'Newsletter', 'gustavoo-portfolio-core' ), + 'text' => __( 'Receba novos artigos sobre desenvolvimento, infraestrutura e automação.', 'gustavoo-portfolio-core' ), + 'form_id' => 0, + ); + $newsletter['_multiwidget'] = 1; + update_option( 'widget_gustavoo_portfolio_newsletter', $newsletter, false ); + + $blocks = get_option( 'widget_block', array() ); + $blocks = is_array( $blocks ) ? $blocks : array(); + $blocks[2] = array( 'content' => '' ); + $blocks[3] = array( 'content' => '

Posts recentes

' ); + $blocks[4] = array( 'content' => '

Comentários

' ); + $blocks[5] = array( 'content' => '

Arquivos

' ); + $blocks[6] = array( 'content' => '

Categorias

' ); + $blocks['_multiwidget'] = 1; + update_option( 'widget_block', $blocks, false ); + + $sidebars['sidebar-blog'] = array( 'gustavoo_portfolio_newsletter-1', 'block-2', 'block-3', 'block-4' ); + } + + if ( empty( $sidebars['footer-1'] ) ) { + $sidebars['footer-1'] = array( 'block-5', 'block-6' ); + } + + update_option( 'sidebars_widgets', $sidebars, false ); + } + + /** + * Repair the seeded contact form when an earlier migration did not persist. + * + * @return void + */ + private static function maybe_migrate_contact_form() { + if ( ! self::fluent_is_ready() ) { + return; + } + + $form = self::find_seeded_form( 'contact' ); + + if ( ! $form ) { + return; + } + + $fields = json_decode( (string) $form->form_fields, true ); + $has_message = false; + + foreach ( (array) ( $fields['fields'] ?? array() ) as $field ) { + if ( 'message' === (string) ( $field['attributes']['name'] ?? '' ) && 'textarea' === (string) ( $field['element'] ?? '' ) ) { + $has_message = true; + break; + } + } + + if ( ! $has_message ) { + try { + self::upsert_form( 'contact', __( 'Site — Contato e orçamento', 'gustavoo-portfolio-core' ), true ); + } catch ( Throwable $error ) { + // Keep the front end available if Fluent Forms is temporarily unavailable. + } + } + } + + /** + * Check the optional integrations without triggering autoload errors. + * + * @return bool + */ + private static function fluent_is_ready() { + return defined( 'FLUENTFORM' ) + && defined( 'FLUENTCRM' ) + && function_exists( 'fluentformLoadFile' ) + && function_exists( 'FluentCrmApi' ) + && class_exists( '\\FluentForm\\App\\Models\\Form' ) + && class_exists( '\\FluentForm\\App\\Models\\FormMeta' ) + && class_exists( '\\FluentForm\\App\\Services\\Form\\FormService' ) + && class_exists( '\\FluentForm\\App\\Services\\Integrations\\FormIntegrationService' ); + } + + /** + * Create or reuse a seed-owned Fluent Form. + * + * @param string $key Deterministic form key. + * @param string $title Form title. + * @param bool $include_whatsapp Include the contact message field. + * @return int + * @throws Exception When Fluent Forms cannot create the form. + */ + private static function upsert_form( $key, $title, $include_whatsapp ) { + $form = self::find_seeded_form( $key ); + + $defaults = fluentformLoadFile( 'Services/FormBuilder/DefaultElements.php' ); + $blank = \FluentForm\App\Models\Form::resolvePredefinedForm( + array( + 'predefined' => 'blank_form', + 'type' => 'form', + ) + ); + $structure = json_decode( (string) $blank['form_fields'], true ); + + if ( ! is_array( $defaults ) || ! is_array( $structure ) ) { + throw new Exception( 'Não foi possível carregar a estrutura padrão do Fluent Forms.' ); + } + + $email = $defaults['general']['input_email']; + $email['uniqElKey'] = 'el_gso_' . $key . '_email'; + $email['attributes']['name'] = 'email'; + $email['attributes']['placeholder'] = __( 'Seu melhor e-mail', 'gustavoo-portfolio-core' ); + $email['settings']['label'] = __( 'E-mail', 'gustavoo-portfolio-core' ); + $email['settings']['admin_field_label'] = __( 'E-mail', 'gustavoo-portfolio-core' ); + $email['settings']['validation_rules']['required']['value'] = true; + + $fields = array( $email ); + + if ( $include_whatsapp ) { + $whatsapp = $defaults['general']['input_mask']; + $whatsapp['uniqElKey'] = 'el_gso_' . $key . '_whatsapp'; + $whatsapp['attributes']['name'] = 'whatsapp'; + $whatsapp['attributes']['placeholder'] = '(31) 99999-9999'; + $whatsapp['attributes']['data-mask'] = '(00) 00000-0000'; + $whatsapp['settings']['label'] = __( 'WhatsApp', 'gustavoo-portfolio-core' ); + $whatsapp['settings']['admin_field_label'] = __( 'WhatsApp', 'gustavoo-portfolio-core' ); + $whatsapp['settings']['mobile_keyboard_type'] = 'tel'; + $whatsapp['settings']['temp_mask'] = 'custom'; + $whatsapp['settings']['data-mask-reverse'] = 'no'; + $whatsapp['settings']['validation_rules']['required']['value'] = true; + $fields[] = $whatsapp; + + $message = $defaults['general']['textarea']; + $message['uniqElKey'] = 'el_gso_' . $key . '_message'; + $message['attributes']['name'] = 'message'; + $message['attributes']['placeholder'] = __( 'Descreva o motivo do contato e o que você quer construir.', 'gustavoo-portfolio-core' ); + $message['attributes']['rows'] = 5; + $message['settings']['label'] = __( 'Como posso ajudar?', 'gustavoo-portfolio-core' ); + $message['settings']['admin_field_label'] = __( 'Motivo do contato', 'gustavoo-portfolio-core' ); + $message['settings']['validation_rules']['required']['value'] = true; + $fields[] = $message; + } + + $structure['fields'] = $fields; + $structure['submitButton']['settings']['button_ui']['text'] = $include_whatsapp + ? __( 'Solicitar contato', 'gustavoo-portfolio-core' ) + : __( 'Quero receber', 'gustavoo-portfolio-core' ); + + $service = new \FluentForm\App\Services\Form\FormService(); + if ( $form ) { + $service->update( + array( + 'form_id' => $form->id, + 'title' => $title, + 'status' => 'published', + 'formFields' => wp_json_encode( $structure, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), + ) + ); + self::localize_form_confirmation( $form->id, $include_whatsapp ); + return absint( $form->id ); + } + + $form = $service->store( + array( + 'predefined' => 'blank_form', + 'type' => 'form', + ) + ); + $form = $service->update( + array( + 'form_id' => $form->id, + 'title' => $title, + 'status' => 'published', + 'formFields' => wp_json_encode( $structure, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), + ) + ); + + \FluentForm\App\Models\FormMeta::persist( $form->id, self::FORM_META, $key ); + self::localize_form_confirmation( $form->id, $include_whatsapp ); + + return absint( $form->id ); + } + + /** + * Locate a form owned by this seed. + * + * @param string $key Seed key. + * @return object|null + */ + private static function find_seeded_form( $key ) { + $markers = \FluentForm\App\Models\FormMeta::where( 'meta_key', self::FORM_META ) + ->where( 'value', $key ) + ->get(); + + foreach ( $markers as $marker ) { + $form = \FluentForm\App\Models\Form::find( absint( $marker->form_id ) ); + if ( $form ) { + return $form; + } + } + + return null; + } + + /** + * Use a concise Portuguese success message. + * + * @param int $form_id Form ID. + * @param bool $is_contact_form Whether this is the lead form. + * @return void + */ + private static function localize_form_confirmation( $form_id, $is_contact_form ) { + $settings = \FluentForm\App\Models\FormMeta::retrieve( 'formSettings', $form_id, array() ); + $settings = is_array( $settings ) ? $settings : array(); + + $settings['confirmation'] = wp_parse_args( + array( + 'redirectTo' => 'samePage', + 'messageToShow' => $is_contact_form + ? __( 'Recebi seus dados. Entrarei em contato em breve.', 'gustavoo-portfolio-core' ) + : __( 'Inscrição recebida. Confira seu e-mail para confirmar.', 'gustavoo-portfolio-core' ), + 'samePageFormBehavior' => 'hide_form', + ), + isset( $settings['confirmation'] ) && is_array( $settings['confirmation'] ) ? $settings['confirmation'] : array() + ); + + \FluentForm\App\Models\FormMeta::persist( $form_id, 'formSettings', $settings ); + } + + /** + * Create the FluentCRM feed for one seed-owned form. + * + * @param int $form_id Form ID. + * @param string $context Contact or newsletter. + * @return void + */ + private static function upsert_crm_feed( $form_id, $context ) { + $existing_id = absint( \FluentForm\App\Models\FormMeta::retrieve( self::FEED_META, $form_id, 0 ) ); + $defaults = apply_filters( 'fluentform/get_integration_defaults_fluentcrm', array(), $form_id ); + $defaults = is_array( $defaults ) ? $defaults : array(); + $is_contact = 'contact' === $context; + $feed = array_replace( + $defaults, + array( + 'name' => $is_contact ? 'Site / Contato' : 'Site / Newsletter', + 'email' => 'email', + 'enabled' => true, + 'double_opt_in' => ! $is_contact, + 'other_fields' => $is_contact + ? array( + array( + 'label' => 'phone', + 'item_value' => '{inputs.whatsapp}', + ), + ) + : array(), + 'conditionals' => array( + 'status' => false, + 'type' => 'all', + 'conditions' => array(), + ), + ) + ); + + $result = ( new \FluentForm\App\Services\Integrations\FormIntegrationService() )->update( + array( + 'form_id' => $form_id, + 'integration_id' => $existing_id, + 'integration_name' => 'fluentcrm', + 'data_type' => 'array', + 'status' => true, + 'integration' => $feed, + ) + ); + + if ( ! empty( $result['integration_id'] ) ) { + \FluentForm\App\Models\FormMeta::persist( $form_id, self::FEED_META, absint( $result['integration_id'] ) ); + } + } + + /** + * Share form IDs with the theme settings option. + * + * @param int $contact_id Contact form ID. + * @param int $newsletter_id Newsletter form ID. + * @return void + */ + private static function save_form_ids( $contact_id, $newsletter_id ) { + $settings = GSO_Portfolio_Settings::get_settings(); + $settings['contact_form_id'] = absint( $contact_id ); + $settings['newsletter_form_id'] = absint( $newsletter_id ); + update_option( GSO_Portfolio_Settings::OPTION_NAME, $settings, false ); + } + + /** + * Add starter projects only when their deterministic markers are absent. + * + * @return void + */ + private static function seed_projects() { + $projects = array( + 'aurelia-online' => array( + 'title' => 'Aurelia Online', + 'excerpt' => __( 'Plataforma online desenvolvida com foco em performance, experiência do usuário e evolução contínua.', 'gustavoo-portfolio-core' ), + 'type' => 'Plataforma web', + 'technologies' => array( 'Web', 'APIs', 'Infraestrutura' ), + 'featured' => true, + ), + 'lumenix-engine' => array( + 'title' => 'Lumenix Engine', + 'excerpt' => __( 'Engine e ecossistema técnico para experiências interativas e produtos digitais.', 'gustavoo-portfolio-core' ), + 'type' => 'Games', + 'technologies' => array( 'C++', 'Games', 'Performance' ), + 'featured' => true, + ), + 'aurabet' => array( + 'title' => 'AuraBet', + 'excerpt' => __( 'Produto digital com arquitetura, integrações e infraestrutura preparadas para escala.', 'gustavoo-portfolio-core' ), + 'type' => 'Produto digital', + 'technologies' => array( 'Full Stack', 'Docker', 'APIs' ), + 'featured' => true, + ), + 'visionforge' => array( + 'title' => 'VisionForge', + 'excerpt' => __( 'Solução criada para transformar processos complexos em uma experiência clara e eficiente.', 'gustavoo-portfolio-core' ), + 'type' => 'Software', + 'technologies' => array( 'Automação', 'Dados', 'Web' ), + 'featured' => true, + ), + 'papel-e-cor' => array( + 'title' => 'Papel & Cor', + 'excerpt' => __( 'Experiência de e-commerce otimizada para catálogo, compra e conversão.', 'gustavoo-portfolio-core' ), + 'type' => 'E-commerce', + 'technologies' => array( 'WordPress', 'WooCommerce', 'UX' ), + 'featured' => false, + ), + 'lumina-hub' => array( + 'title' => 'Lumina Hub', + 'excerpt' => __( 'Hub digital que reúne conteúdo, serviços e integrações em uma única plataforma.', 'gustavoo-portfolio-core' ), + 'type' => 'Plataforma web', + 'technologies' => array( 'WordPress', 'APIs', 'Automação' ), + 'featured' => false, + ), + ); + + $order = 0; + foreach ( $projects as $key => $project ) { + $existing = get_posts( + array( + 'post_type' => GSO_Projects::POST_TYPE, + 'post_status' => 'any', + 'posts_per_page' => 1, + 'fields' => 'ids', + 'meta_key' => self::PROJECT_META, + 'meta_value' => $key, + ) + ); + + if ( $existing ) { + ++$order; + continue; + } + + $post_id = wp_insert_post( + array( + 'post_type' => GSO_Projects::POST_TYPE, + 'post_status' => 'publish', + 'post_title' => $project['title'], + 'post_excerpt' => $project['excerpt'], + 'menu_order' => $order, + ), + true + ); + + if ( is_wp_error( $post_id ) ) { + ++$order; + continue; + } + + update_post_meta( $post_id, self::PROJECT_META, $key ); + update_post_meta( $post_id, GSO_Projects::META_URL, 'https://gustavoo.me/#projetos' ); + update_post_meta( $post_id, GSO_Projects::META_CLIENT, __( 'Projeto selecionado', 'gustavoo-portfolio-core' ) ); + update_post_meta( $post_id, GSO_Projects::META_YEAR, (int) gmdate( 'Y' ) ); + update_post_meta( $post_id, GSO_Projects::META_LINK_LABEL, __( 'Conhecer projeto', 'gustavoo-portfolio-core' ) ); + update_post_meta( $post_id, GSO_Projects::META_FEATURED, $project['featured'] ? '1' : '' ); + wp_set_object_terms( $post_id, $project['type'], GSO_Projects::TAX_TYPE ); + wp_set_object_terms( $post_id, $project['technologies'], GSO_Projects::TAX_TECH ); + ++$order; + } + } + +} diff --git a/wp-content/themes/.gitkeep b/wp-content/themes/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/wp-content/themes/.gitkeep @@ -0,0 +1 @@ + diff --git a/wp-content/themes/gustavoo-portfolio/404.php b/wp-content/themes/gustavoo-portfolio/404.php new file mode 100644 index 0000000..42dc3ca --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/404.php @@ -0,0 +1,57 @@ + 'post', + 'post_status' => 'publish', + 'posts_per_page' => 3, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ) +); +?> + +
+
+
+

404

+

+

+
+ +
+ +
+
+ + +
+
+
+

+

+
+
+ +
+
+
+ +
+ + + +
+
+
+

+

+ +
+ +
+
+ +
+
+ +
+ +
+ + + + +
+ + +
+
+ + a, +.site-header .menu a:hover, +.site-header .current-menu-item > a { + color: var(--color-text); +} + +.primary-nav a:hover::after, +.primary-nav .current-menu-item > a::after, +.site-header .menu a:hover::after, +.site-header .current-menu-item > a::after { + transform: scaleX(1); + transform-origin: left; +} + +.header-cta { + min-height: 42px; + padding: 10px 16px; +} + +.nav-toggle { + position: relative; + display: none; + width: 46px; + height: 46px; + padding: 0; + color: var(--color-text); + background: rgba(255, 255, 255, 0.035); + border: 1px solid var(--color-line-soft); + border-radius: 12px; +} + +.nav-toggle__line, +.nav-toggle span:not(.screen-reader-text) { + position: absolute; + left: 12px; + width: 20px; + height: 2px; + background: currentColor; + border-radius: 999px; + transition: transform 200ms ease, top 200ms ease, opacity 200ms ease; +} + +.nav-toggle__line:nth-of-type(1), +.nav-toggle span:nth-last-child(3) { + top: 15px; +} + +.nav-toggle__line:nth-of-type(2), +.nav-toggle span:nth-last-child(2) { + top: 21px; +} + +.nav-toggle__line:nth-of-type(3), +.nav-toggle span:nth-last-child(1) { + top: 27px; +} + +.nav-toggle[aria-expanded="true"] .nav-toggle__line:nth-of-type(1) { + top: 21px; + transform: rotate(45deg); +} + +.nav-toggle[aria-expanded="true"] .nav-toggle__line:nth-of-type(2) { + opacity: 0; +} + +.nav-toggle[aria-expanded="true"] .nav-toggle__line:nth-of-type(3) { + top: 21px; + transform: rotate(-45deg); +} + +/* Hero */ +.hero-stage { + position: relative; + display: grid; + min-height: calc(100vh - var(--hero-peek)); + min-height: calc(100svh - var(--hero-peek)); + overflow: clip; + padding-top: var(--header-height); + isolation: isolate; + background: + radial-gradient(circle at 77% 46%, rgba(116, 213, 199, 0.16), transparent 28%), + radial-gradient(circle at 13% 72%, rgba(68, 111, 187, 0.1), transparent 33%), + linear-gradient(135deg, #071624 0%, #0a1c2d 62%, #0b2235 100%); +} + +.hero-stage::before { + position: absolute; + z-index: -2; + inset: 0; + content: ""; + opacity: 0.45; + background-image: + linear-gradient(rgba(142, 190, 207, 0.045) 1px, transparent 1px), + linear-gradient(90deg, rgba(142, 190, 207, 0.045) 1px, transparent 1px); + background-size: 52px 52px; + mask-image: linear-gradient(to bottom, black, transparent 92%); +} + +.hero-stage::after { + position: absolute; + z-index: -1; + width: min(55vw, 780px); + aspect-ratio: 1; + right: -12%; + bottom: -40%; + content: ""; + border: 1px solid rgba(116, 213, 199, 0.09); + border-radius: 50%; + box-shadow: + 0 0 0 100px rgba(116, 213, 199, 0.022), + 0 0 0 200px rgba(116, 213, 199, 0.016); +} + +.hero { + display: grid; + width: 100%; + align-items: center; + padding-block: clamp(54px, 7vh, 96px) clamp(88px, 11vh, 126px); +} + +.hero__inner { + display: grid; + width: min(var(--container), calc(100% - (2 * var(--gutter)))); + margin-inline: auto; + align-items: center; + gap: clamp(38px, 6vw, 88px); + grid-template-columns: minmax(0, 7fr) minmax(320px, 5fr); +} + +.hero__content { + position: relative; + z-index: 2; + max-width: 790px; +} + +.hero__title { + max-width: 13ch; + margin-bottom: 26px; + font-size: clamp(2.8rem, 5.25vw, 5.15rem); + font-weight: 790; + line-height: 0.99; + letter-spacing: -0.054em; +} + +.hero__description, +.hero__lead { + max-width: 65ch; + margin-bottom: 30px; + color: var(--color-text-soft); + font-size: clamp(1.04rem, 1.4vw, 1.27rem); + line-height: 1.62; +} + +.hero__actions { + display: flex; + margin-bottom: 34px; + flex-wrap: wrap; + gap: 12px; +} + +.hero__scope, +.scope-list { + display: flex; + margin: 0; + padding: 0; + flex-wrap: wrap; + gap: 8px; + list-style: none; +} + +.hero__scope li, +.scope-list li, +.chip, +.project-card__tag, +.post-card__category, +.entry-taxonomy a { + display: inline-flex; + min-height: 30px; + align-items: center; + padding: 6px 10px; + color: var(--color-text-soft); + background: rgba(116, 213, 199, 0.07); + border: 1px solid rgba(116, 213, 199, 0.18); + border-radius: 999px; + font-family: var(--font-mono); + font-size: 0.7rem; + line-height: 1.2; + letter-spacing: 0.02em; +} + +.hero__media { + position: relative; + z-index: 1; + display: grid; + justify-items: center; + place-items: center; +} + +.hero__media::before { + position: absolute; + z-index: -1; + width: 82%; + aspect-ratio: 1; + content: ""; + background: radial-gradient(circle, rgba(80, 229, 207, 0.14), transparent 68%); + filter: blur(10px); +} + +.hero__image, +.hero__media img { + width: min(44vw, 570px); + max-height: 65vh; + object-fit: contain; + filter: drop-shadow(0 28px 36px rgba(0, 0, 0, 0.28)); + animation: hero-float 7s ease-in-out infinite; +} + +.hero__status, +.availability-badge { + display: inline-flex; + align-items: center; + gap: 8px; + margin-bottom: 18px; + color: var(--color-text-soft); + font-family: var(--font-mono); + font-size: 0.74rem; +} + +.hero__status::before, +.availability-badge::before { + width: 8px; + height: 8px; + content: ""; + background: var(--color-success); + border-radius: 50%; + box-shadow: 0 0 0 5px rgba(123, 224, 161, 0.12); +} + +.scroll-cue { + position: absolute; + z-index: 3; + bottom: 20px; + left: 50%; + display: grid; + justify-items: center; + gap: 8px; + color: var(--color-text-muted); + font-family: var(--font-mono); + font-size: 0.66rem; + letter-spacing: 0.11em; + text-decoration: none; + text-transform: uppercase; + transform: translateX(-50%); +} + +.scroll-cue::after { + width: 1px; + height: 30px; + content: ""; + background: linear-gradient(var(--color-brand), transparent); + animation: scroll-cue 1.7s ease-in-out infinite; + transform-origin: top; +} + +.scroll-cue:hover { + color: var(--color-brand); +} + +@keyframes hero-float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } +} + +@keyframes scroll-cue { + 0%, 100% { opacity: 0.35; transform: scaleY(0.55); } + 50% { opacity: 1; transform: scaleY(1); } +} + +/* Sections and shared headings */ +.section { + position: relative; + padding-block: var(--section-space); +} + +.section[id] { + scroll-margin-top: calc(var(--header-height) + 12px); +} + +.section--surface, +.section-surface { + background: var(--color-bg-alt); + border-block: 1px solid var(--color-line-soft); +} + +.section--raised { + background: var(--color-surface); +} + +.section--intro, +.intro-strip, +.capability-strip { + position: relative; + z-index: 4; + margin-top: 0; + padding-block: clamp(44px, 6vw, 72px); + background: var(--color-surface); + border-top: 1px solid rgba(116, 213, 199, 0.18); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + box-shadow: 0 -22px 70px rgba(0, 0, 0, 0.16); +} + +.section-heading { + display: flex; + margin-bottom: clamp(34px, 5vw, 58px); + align-items: end; + justify-content: space-between; + gap: 36px; +} + +.section-heading__content, +.section-heading > div:first-child { + max-width: 760px; +} + +.section-heading h2 { + margin-bottom: 0; +} + +.section-heading__copy, +.section-heading p { + max-width: 58ch; + margin: 14px 0 0; + color: var(--color-text-soft); + font-size: 1.04rem; +} + +.section-heading__link { + flex: 0 0 auto; +} + +/* About */ +.about-layout { + display: grid; + align-items: start; + gap: clamp(38px, 7vw, 90px); + grid-template-columns: minmax(0, 7fr) minmax(300px, 5fr); +} + +.about-copy, +.about__content { + max-width: 760px; +} + +.about-copy h2, +.about__content h2 { + margin-bottom: 26px; +} + +.about-copy p, +.about__content p { + color: var(--color-text-soft); + font-size: clamp(1rem, 1.3vw, 1.16rem); +} + +.about-copy p + p, +.about__content p + p { + margin-top: 18px; +} + +.about-signature { + display: inline-flex; + margin-top: 22px; + align-items: center; + gap: 14px; + color: var(--color-text); + font-weight: 720; +} + +.about-signature img { + width: 48px; + height: 48px; + object-fit: contain; +} + +.proof-grid, +.about-proof, +.stats-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.proof-card, +.stat-card { + min-height: 138px; + padding: 22px; + background: linear-gradient(145deg, rgba(17, 44, 66, 0.92), rgba(13, 36, 56, 0.86)); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-md); + transition: transform 220ms var(--ease-out), border-color 220ms ease; +} + +.proof-card:hover, +.stat-card:hover { + border-color: rgba(116, 213, 199, 0.38); + transform: translateY(-3px); +} + +.proof-card__icon, +.stat-card__icon { + display: grid; + width: 40px; + height: 40px; + margin-bottom: 20px; + place-items: center; + color: var(--color-brand); + background: rgba(116, 213, 199, 0.08); + border: 1px solid rgba(116, 213, 199, 0.16); + border-radius: 11px; +} + +.proof-card strong, +.stat-card strong { + display: block; + margin-bottom: 5px; + color: var(--color-text); + font-size: 0.98rem; +} + +.proof-card span, +.stat-card span { + color: var(--color-text-muted); + font-family: var(--font-mono); + font-size: 0.68rem; + line-height: 1.45; +} + +/* Services */ +.services-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.service-card { + position: relative; + min-height: 290px; + overflow: hidden; + padding: clamp(24px, 2.4vw, 32px); + background: linear-gradient(150deg, rgba(17, 44, 66, 0.9), rgba(10, 28, 45, 0.96)); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-md); + transition: + transform 240ms var(--ease-out), + border-color 240ms ease, + box-shadow 240ms ease, + background-color 240ms ease; +} + +.service-card::after { + position: absolute; + width: 170px; + height: 170px; + right: -90px; + bottom: -100px; + content: ""; + background: radial-gradient(circle, rgba(116, 213, 199, 0.13), transparent 68%); + transition: transform 350ms var(--ease-out); +} + +.service-card:hover { + background: linear-gradient(150deg, rgba(21, 53, 77, 0.96), rgba(10, 28, 45, 0.98)); + border-color: rgba(116, 213, 199, 0.42); + box-shadow: var(--shadow-card); + transform: translateY(-5px); +} + +.service-card:hover::after { + transform: scale(1.35); +} + +.service-card__top { + display: flex; + margin-bottom: 30px; + align-items: center; + justify-content: space-between; +} + +.service-card__icon { + display: grid; + width: 48px; + height: 48px; + place-items: center; + color: var(--color-brand); + background: rgba(116, 213, 199, 0.08); + border: 1px solid rgba(116, 213, 199, 0.18); + border-radius: 13px; +} + +.service-card__icon svg { + width: 24px; + height: 24px; +} + +.service-card__number { + color: rgba(184, 197, 206, 0.32); + font-family: var(--font-mono); + font-size: 0.72rem; +} + +.service-card h3 { + margin-bottom: 14px; +} + +.service-card p { + color: var(--color-text-soft); + font-size: 0.94rem; + line-height: 1.68; +} + +/* Project cards */ +.projects-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.project-card, +.post-card { + position: relative; + display: flex; + min-width: 0; + overflow: hidden; + flex-direction: column; + background: rgba(13, 36, 56, 0.88); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-md); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.015) inset; + transition: + transform 240ms var(--ease-out), + border-color 240ms ease, + box-shadow 240ms ease; +} + +.project-card:hover, +.project-card:focus-within, +.post-card:hover, +.post-card:focus-within { + border-color: rgba(116, 213, 199, 0.42); + box-shadow: var(--shadow-card); + transform: translateY(-5px); +} + +.project-card__media, +.post-card__media { + position: relative; + aspect-ratio: 16 / 10; + overflow: hidden; + background: + radial-gradient(circle at 70% 30%, rgba(116, 213, 199, 0.2), transparent 28%), + linear-gradient(135deg, #102b42, #071521); + border-bottom: 1px solid var(--color-line-soft); +} + +.project-card__media::after, +.post-card__media::after { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + background: linear-gradient(to top, rgba(4, 16, 27, 0.2), transparent 45%); +} + +.project-card__media img, +.post-card__media img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 450ms var(--ease-out), filter 300ms ease; +} + +.project-card:hover .project-card__media img, +.post-card:hover .post-card__media img { + transform: scale(1.04); +} + +.project-card__placeholder, +.project-placeholder { + position: absolute; + inset: 0; + display: grid; + place-items: center; + overflow: hidden; + color: rgba(245, 248, 250, 0.92); + font-family: var(--font-mono); + font-size: clamp(2rem, 4vw, 3.6rem); + font-weight: 750; + letter-spacing: -0.08em; +} + +.project-card__placeholder::before, +.project-placeholder::before { + position: absolute; + inset: 14%; + content: ""; + opacity: 0.5; + background-image: + linear-gradient(rgba(116, 213, 199, 0.13) 1px, transparent 1px), + linear-gradient(90deg, rgba(116, 213, 199, 0.13) 1px, transparent 1px); + background-size: 26px 26px; + border: 1px solid rgba(116, 213, 199, 0.16); + border-radius: 18px; + transform: rotate(-4deg); +} + +.project-card__body, +.post-card__body { + display: flex; + padding: 24px; + flex: 1; + flex-direction: column; +} + +.project-card__meta, +.post-card__meta, +.entry-meta { + display: flex; + margin-bottom: 13px; + align-items: center; + flex-wrap: wrap; + gap: 8px 12px; + color: var(--color-text-muted); + font-family: var(--font-mono); + font-size: 0.68rem; + line-height: 1.4; +} + +.project-card h3, +.post-card h2, +.post-card h3 { + margin-bottom: 12px; + font-size: clamp(1.18rem, 1.7vw, 1.45rem); + line-height: 1.2; +} + +.project-card h3 a, +.post-card h2 a, +.post-card h3 a { + color: var(--color-text); + text-decoration: none; +} + +.project-card h3 a::after, +.post-card h2 a::after, +.post-card h3 a::after { + position: absolute; + inset: 0; + content: ""; +} + +.project-card__excerpt, +.post-card__excerpt { + margin-bottom: 20px; + color: var(--color-text-soft); + font-size: 0.9rem; + line-height: 1.65; +} + +.project-card__footer, +.post-card__footer { + display: flex; + margin-top: auto; + align-items: center; + justify-content: space-between; + gap: 14px; +} + +.project-card__tags, +.entry-taxonomy { + position: relative; + z-index: 2; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.card-link { + position: relative; + z-index: 2; + display: inline-flex; + min-width: max-content; + min-height: 40px; + align-items: center; + gap: 7px; + color: var(--color-brand); + font-size: 0.78rem; + font-weight: 720; + text-decoration: none; +} + +.project-card--featured { + grid-column: span 2; +} + +.project-card--featured .project-card__media { + aspect-ratio: 2 / 1; +} + +.projects-empty, +.no-results { + padding: clamp(30px, 5vw, 58px); + color: var(--color-text-soft); + background: rgba(13, 36, 56, 0.68); + border: 1px dashed rgba(116, 213, 199, 0.3); + border-radius: var(--radius-md); + text-align: center; + grid-column: 1 / -1; +} + +/* Posts */ +.posts-grid { + display: grid; + gap: 20px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.post-card__media { + aspect-ratio: 16 / 9; +} + +.post-card__category { + color: var(--color-brand); +} + +.post-card time { + color: var(--color-text-muted); +} + +/* Contact and Fluent Forms */ +.contact-panel { + position: relative; + display: grid; + overflow: hidden; + padding: clamp(28px, 5vw, 62px); + align-items: center; + gap: clamp(38px, 7vw, 88px); + background: + radial-gradient(circle at 6% 90%, rgba(80, 229, 207, 0.1), transparent 30%), + linear-gradient(145deg, rgba(17, 44, 66, 0.96), rgba(7, 22, 36, 0.98)); + border: 1px solid rgba(116, 213, 199, 0.2); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + grid-template-columns: minmax(0, 5fr) minmax(340px, 5fr); +} + +.contact-panel::before { + position: absolute; + width: 360px; + height: 360px; + top: -260px; + right: -100px; + content: ""; + border: 1px solid rgba(116, 213, 199, 0.09); + border-radius: 50%; + box-shadow: 0 0 0 70px rgba(116, 213, 199, 0.025); +} + +.contact-panel__content { + position: relative; + z-index: 1; +} + +.contact-panel__content h2 { + margin-bottom: 22px; +} + +.contact-panel__content > p { + max-width: 54ch; + color: var(--color-text-soft); + font-size: 1.04rem; +} + +.contact-links { + display: grid; + margin: 28px 0 0; + padding: 0; + gap: 10px; + list-style: none; +} + +.contact-links a { + display: inline-flex; + min-height: 42px; + align-items: center; + gap: 10px; + color: var(--color-text-soft); + text-decoration: none; +} + +.contact-links a:hover { + color: var(--color-brand); +} + +.contact-panel__form, +.form-card { + position: relative; + z-index: 2; + padding: clamp(22px, 3vw, 34px); + background: rgba(4, 16, 27, 0.52); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-md); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); +} + +.contact-panel .fluentform, +.newsletter-panel .fluentform, +.widget .fluentform { + color: var(--color-text); +} + +.contact-panel .ff-el-group, +.newsletter-panel .ff-el-group, +.widget .ff-el-group { + margin-bottom: 16px; +} + +.contact-panel .ff-el-input--label, +.newsletter-panel .ff-el-input--label, +.widget .ff-el-input--label { + margin-bottom: 7px; +} + +.contact-panel .ff-el-input--label label, +.newsletter-panel .ff-el-input--label label, +.widget .ff-el-input--label label, +.contact-panel .ff-el-form-check-label, +.newsletter-panel .ff-el-form-check-label, +.widget .ff-el-form-check-label { + color: var(--color-text-soft) !important; + font-size: 0.82rem; + font-weight: 620; +} + +.contact-panel .ff-el-form-control, +.newsletter-panel .ff-el-form-control, +.widget .ff-el-form-control { + width: 100%; + min-height: 54px; + padding: 13px 15px; + color: var(--color-text) !important; + background: #071827 !important; + border: 1px solid var(--color-line) !important; + border-radius: 10px !important; + box-shadow: none !important; + transition: border-color 180ms ease, box-shadow 180ms ease; +} + +.contact-panel textarea.ff-el-form-control, +.newsletter-panel textarea.ff-el-form-control, +.widget textarea.ff-el-form-control { + min-height: 130px; + resize: vertical; +} + +.contact-panel .ff-el-form-control::placeholder, +.newsletter-panel .ff-el-form-control::placeholder, +.widget .ff-el-form-control::placeholder { + color: #6f8595 !important; + opacity: 1; +} + +.contact-panel .ff-el-form-control:focus, +.newsletter-panel .ff-el-form-control:focus, +.widget .ff-el-form-control:focus { + border-color: var(--color-brand) !important; + box-shadow: 0 0 0 3px rgba(116, 213, 199, 0.16) !important; + outline: none; +} + +.contact-panel .ff-btn-submit, +.newsletter-panel .ff-btn-submit, +.widget .ff-btn-submit { + min-height: 50px; + padding: 13px 20px !important; + color: var(--color-brand-ink) !important; + background: var(--color-brand) !important; + border: 1px solid var(--color-brand) !important; + border-radius: 10px !important; + font-weight: 760 !important; + opacity: 1 !important; + transition: transform 180ms var(--ease-out), background 180ms ease, box-shadow 180ms ease; +} + +.contact-panel .ff-btn-submit:hover, +.newsletter-panel .ff-btn-submit:hover, +.widget .ff-btn-submit:hover { + background: var(--color-brand-strong) !important; + box-shadow: var(--shadow-brand); + transform: translateY(-2px); +} + +.contact-panel .ff-el-is-error .ff-el-form-control, +.newsletter-panel .ff-el-is-error .ff-el-form-control, +.widget .ff-el-is-error .ff-el-form-control { + border-color: var(--color-danger) !important; +} + +.contact-panel .error.text-danger, +.newsletter-panel .error.text-danger, +.widget .error.text-danger { + margin-top: 6px; + color: var(--color-danger) !important; + font-size: 0.75rem; +} + +.ff-message-success { + padding: 16px 18px !important; + color: #cbffdc !important; + background: rgba(123, 224, 161, 0.09) !important; + border: 1px solid rgba(123, 224, 161, 0.34) !important; + border-radius: 10px !important; + box-shadow: none !important; +} + +.form-fallback, +.plugin-notice { + padding: 18px; + color: var(--color-text-soft); + background: rgba(255, 255, 255, 0.025); + border: 1px dashed var(--color-line); + border-radius: 10px; + font-size: 0.88rem; +} + +/* Newsletter, shown globally */ +.newsletter-section { + padding-block: clamp(54px, 7vw, 90px); + background: var(--color-bg-deep); + border-top: 1px solid var(--color-line-soft); +} + +.newsletter-panel { + position: relative; + display: grid; + overflow: hidden; + padding: clamp(28px, 4vw, 48px); + align-items: center; + gap: 36px; + background: + linear-gradient(115deg, rgba(116, 213, 199, 0.08), transparent 42%), + var(--color-surface); + border: 1px solid rgba(116, 213, 199, 0.19); + border-radius: var(--radius-lg); + grid-template-columns: minmax(0, 1.2fr) minmax(340px, 0.8fr); +} + +.newsletter-panel::after { + position: absolute; + width: 210px; + height: 210px; + right: -100px; + bottom: -130px; + content: ""; + border: 1px solid rgba(116, 213, 199, 0.12); + border-radius: 50%; + box-shadow: 0 0 0 45px rgba(116, 213, 199, 0.026); +} + +.newsletter-panel__content, +.newsletter-panel__form { + position: relative; + z-index: 1; +} + +.newsletter-panel h2, +.newsletter-panel h3 { + margin-bottom: 12px; + font-size: clamp(1.55rem, 2.6vw, 2.45rem); +} + +.newsletter-panel p { + max-width: 55ch; + color: var(--color-text-soft); +} + +.newsletter-panel .frm-fluent-form, +.newsletter-panel form { + display: grid; + align-items: end; + gap: 10px; +} + +.newsletter-panel .ff_submit_btn_wrapper { + margin-bottom: 0; +} + +.newsletter-panel .ff-btn-submit { + width: 100%; +} + +/* Inner page headers */ +.page-hero, +.archive-hero, +.search-hero, +.entry-hero { + position: relative; + overflow: hidden; + padding-block: calc(var(--header-height) + clamp(68px, 8vw, 112px)) clamp(60px, 7vw, 98px); + background: + radial-gradient(circle at 78% 35%, rgba(116, 213, 199, 0.11), transparent 28%), + linear-gradient(145deg, var(--color-bg), var(--color-bg-alt)); + border-bottom: 1px solid var(--color-line-soft); +} + +.page-hero::after, +.archive-hero::after, +.entry-hero::after { + position: absolute; + inset: 0; + pointer-events: none; + content: ""; + opacity: 0.35; + background-image: linear-gradient(90deg, rgba(142, 190, 207, 0.045) 1px, transparent 1px); + background-size: 64px 100%; + mask-image: linear-gradient(90deg, transparent, black 30%, transparent); +} + +.page-hero__inner, +.archive-hero__inner, +.entry-hero__inner { + position: relative; + z-index: 1; + max-width: 920px; +} + +.page-hero h1, +.archive-hero h1, +.entry-hero h1 { + max-width: 17ch; + margin-bottom: 18px; + font-size: clamp(2.4rem, 5vw, 4.7rem); +} + +.page-hero p, +.archive-hero p, +.entry-hero p { + max-width: 65ch; + margin-bottom: 0; + color: var(--color-text-soft); + font-size: clamp(1rem, 1.4vw, 1.2rem); +} + +.content-shell { + padding-block: var(--section-space); +} + +.content-grid, +.archive-layout { + display: grid; + align-items: start; + gap: clamp(34px, 5vw, 64px); + grid-template-columns: minmax(0, 1fr) minmax(260px, 320px); +} + +.archive-posts { + display: grid; + gap: 20px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +/* Article */ +.article-layout { + display: grid; + width: min(1160px, calc(100% - (2 * var(--gutter)))); + margin-inline: auto; + padding-block: var(--section-space); + align-items: start; + gap: clamp(42px, 7vw, 88px); + grid-template-columns: minmax(0, var(--content)) minmax(260px, 310px); +} + +.article-layout--single { + justify-content: center; +} + +.article, +.entry-content { + min-width: 0; +} + +.article__featured, +.entry-featured-image { + width: min(1160px, calc(100% - (2 * var(--gutter)))); + margin: clamp(30px, 5vw, 58px) auto 0; + overflow: hidden; + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); +} + +.article__featured img, +.entry-featured-image img { + width: 100%; + max-height: 660px; + object-fit: cover; +} + +.entry-content { + color: #d4dde3; + font-size: clamp(1.04rem, 1.2vw, 1.13rem); + line-height: 1.78; +} + +.entry-content > * { + max-width: var(--content); + margin-inline: auto; +} + +.entry-content > .alignwide { + max-width: 1040px; +} + +.entry-content > .alignfull { + width: 100vw; + max-width: none; + margin-left: calc(50% - 50vw); +} + +.entry-content h2, +.entry-content h3, +.entry-content h4 { + margin-top: 2.2em; + margin-bottom: 0.75em; + scroll-margin-top: calc(var(--header-height) + 24px); +} + +.entry-content p, +.entry-content ul, +.entry-content ol, +.entry-content blockquote, +.entry-content pre, +.entry-content table, +.entry-content figure { + margin-bottom: 1.45em; +} + +.entry-content a { + color: var(--color-brand); + font-weight: 600; +} + +.entry-content a:hover { + color: var(--color-focus); +} + +.entry-content ul, +.entry-content ol { + padding-left: 1.3em; +} + +.entry-content li + li { + margin-top: 0.45em; +} + +.entry-content blockquote { + padding: 8px 0 8px 24px; + color: var(--color-text-soft); + border-left: 3px solid var(--color-brand); + font-size: 1.08em; + font-style: italic; +} + +.entry-content code, +.comment-content code { + padding: 0.15em 0.36em; + color: #b9fff5; + background: #04101b; + border: 1px solid rgba(116, 213, 199, 0.12); + border-radius: 5px; + font-family: var(--font-mono); + font-size: 0.86em; +} + +.entry-content pre, +.comment-content pre { + max-width: 100%; + padding: 22px; + overflow-x: auto; + color: #d9fdf7; + background: #040d16; + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-sm); + font-family: var(--font-mono); + font-size: 0.86rem; + line-height: 1.65; +} + +.entry-content pre code { + padding: 0; + color: inherit; + background: transparent; + border: 0; +} + +.entry-content table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; +} + +.entry-content th, +.entry-content td { + padding: 12px 14px; + border: 1px solid var(--color-line); + text-align: left; +} + +.entry-content th { + color: var(--color-text); + background: var(--color-surface); +} + +.entry-content figcaption, +.wp-caption-text { + margin-top: 8px; + color: var(--color-text-muted); + font-size: 0.76rem; + text-align: center; +} + +.entry-footer, +.article-footer { + margin-top: 46px; + padding-top: 26px; + border-top: 1px solid var(--color-line-soft); +} + +.post-navigation { + margin-top: 54px; + padding-top: 30px; + border-top: 1px solid var(--color-line-soft); +} + +.post-navigation .nav-links { + display: grid; + gap: 18px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.post-navigation a { + display: grid; + min-height: 112px; + padding: 20px; + align-content: center; + color: var(--color-text); + background: var(--color-surface); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-sm); + text-decoration: none; +} + +.post-navigation a:hover { + color: var(--color-brand); + border-color: rgba(116, 213, 199, 0.35); +} + +.nav-subtitle { + color: var(--color-text-muted); + font-family: var(--font-mono); + font-size: 0.68rem; + text-transform: uppercase; +} + +.nav-title { + margin-top: 6px; + font-weight: 700; +} + +/* Sidebar and widgets */ +.sidebar, +.widget-area { + min-width: 0; +} + +.article-layout .sidebar { + position: sticky; + top: calc(var(--header-height) + 28px); +} + +.widget { + margin-bottom: 18px; + padding: 22px; + color: var(--color-text-soft); + background: rgba(13, 36, 56, 0.75); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-md); +} + +.widget-title, +.widget h2, +.widget h3 { + margin-bottom: 16px; + font-size: 1rem; + letter-spacing: -0.015em; +} + +.widget ul { + display: grid; + margin: 0; + padding: 0; + gap: 9px; + list-style: none; +} + +.widget li { + padding-bottom: 9px; + border-bottom: 1px solid rgba(142, 190, 207, 0.09); +} + +.widget li:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.widget a { + color: var(--color-text-soft); + text-decoration: none; +} + +.widget a:hover { + color: var(--color-brand); +} + +.search-form { + display: flex; + gap: 8px; +} + +.search-form label { + flex: 1; +} + +.search-field, +.comment-form input:not([type="submit"]), +.comment-form textarea { + width: 100%; + min-height: 48px; + padding: 11px 13px; + color: var(--color-text); + background: #071827; + border: 1px solid var(--color-line); + border-radius: 9px; +} + +.search-field:focus, +.comment-form input:not([type="submit"]):focus, +.comment-form textarea:focus { + border-color: var(--color-brand); + box-shadow: 0 0 0 3px rgba(116, 213, 199, 0.15); + outline: none; +} + +.search-submit { + min-width: 48px; + padding-inline: 15px; +} + +/* Pagination */ +.pagination, +.posts-navigation { + margin-top: 48px; +} + +.pagination .nav-links, +.posts-navigation .nav-links { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.page-numbers, +.posts-navigation a { + display: inline-flex; + min-width: 44px; + min-height: 44px; + padding: 10px 13px; + align-items: center; + justify-content: center; + color: var(--color-text-soft); + background: var(--color-surface); + border: 1px solid var(--color-line-soft); + border-radius: 9px; + text-decoration: none; +} + +.page-numbers.current, +.page-numbers:hover, +.posts-navigation a:hover { + color: var(--color-brand-ink); + background: var(--color-brand); + border-color: var(--color-brand); +} + +/* Comments */ +.comments-area { + margin-top: 64px; + padding-top: 44px; + border-top: 1px solid var(--color-line-soft); +} + +.comments-title, +.comment-reply-title { + margin-bottom: 28px; + font-size: clamp(1.45rem, 2vw, 2rem); +} + +#cancel-comment-reply-link { + display: inline-flex; + margin-left: 12px; + vertical-align: middle; + font-size: 0.62em; + font-weight: 650; + white-space: nowrap; +} + +.comment-list { + margin: 0 0 44px; + padding: 0; + list-style: none; +} + +.comment-list .children { + margin-left: 30px; + list-style: none; +} + +.comment-body { + margin-bottom: 18px; + padding: 22px; + background: var(--color-surface); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-sm); +} + +.comment-meta { + margin-bottom: 14px; + color: var(--color-text-muted); + font-size: 0.78rem; +} + +.comment-author { + color: var(--color-text); + font-weight: 700; +} + +.comment-author img { + display: inline-block; + margin-right: 9px; + border-radius: 50%; + vertical-align: middle; +} + +.comment-form { + display: grid; + gap: 14px; +} + +.comment-form p { + margin: 0; +} + +.comment-form label { + display: block; + margin-bottom: 6px; + color: var(--color-text-soft); + font-size: 0.82rem; + font-weight: 650; +} + +/* 404 */ +.error-404, +.not-found-page { + display: grid; + min-height: 70vh; + padding-block: calc(var(--header-height) + 80px) 90px; + place-items: center; + text-align: center; +} + +.error-404__inner, +.not-found-page__inner { + width: min(680px, calc(100% - (2 * var(--gutter)))); +} + +.error-404__code, +.not-found-page__code { + display: block; + margin-bottom: 12px; + color: rgba(116, 213, 199, 0.24); + font-family: var(--font-mono); + font-size: clamp(5rem, 16vw, 10rem); + font-weight: 800; + line-height: 0.9; + letter-spacing: -0.1em; +} + +.error-404 h1, +.not-found-page h1 { + margin-bottom: 18px; +} + +.error-404 p, +.not-found-page p { + margin-bottom: 28px; + color: var(--color-text-soft); +} + +.error-404 .search-form, +.not-found-page .search-form { + max-width: 520px; + margin-inline: auto; +} + +/* Footer */ +.site-footer { + position: relative; + padding-block: clamp(54px, 7vw, 88px) 28px; + background: var(--color-bg-deep); + border-top: 1px solid var(--color-line-soft); +} + +.footer-grid { + display: grid; + margin-bottom: 48px; + gap: clamp(32px, 6vw, 78px); + grid-template-columns: minmax(260px, 1.4fr) repeat(2, minmax(140px, 0.6fr)); +} + +.footer-brand { + max-width: 430px; +} + +.footer-brand .brand { + margin-bottom: 18px; +} + +.footer-brand p { + color: var(--color-text-muted); + font-size: 0.9rem; +} + +.footer-title { + margin-bottom: 16px; + color: var(--color-text); + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.footer-menu, +.footer-links, +.social-links { + display: flex; + margin: 0; + padding: 0; + flex-direction: column; + gap: 9px; + list-style: none; +} + +.footer-menu a, +.footer-links a { + color: var(--color-text-muted); + font-size: 0.88rem; + text-decoration: none; +} + +.footer-menu a:hover, +.footer-links a:hover { + color: var(--color-brand); +} + +.social-links { + margin-top: 20px; + flex-direction: row; + flex-wrap: wrap; +} + +.social-links a { + display: grid; + width: 42px; + height: 42px; + place-items: center; + color: var(--color-text-soft); + background: rgba(255, 255, 255, 0.025); + border: 1px solid var(--color-line-soft); + border-radius: 10px; + text-decoration: none; + transition: transform 180ms var(--ease-out), color 180ms ease, border-color 180ms ease; +} + +.social-links a:hover { + color: var(--color-brand); + border-color: rgba(116, 213, 199, 0.38); + transform: translateY(-2px); +} + +.social-links svg { + width: 18px; + height: 18px; +} + +.footer-bottom { + display: flex; + padding-top: 24px; + align-items: center; + justify-content: space-between; + gap: 20px; + color: var(--color-text-muted); + border-top: 1px solid var(--color-line-soft); + font-family: var(--font-mono); + font-size: 0.68rem; +} + +.back-to-top { + display: inline-flex; + min-height: 40px; + align-items: center; + gap: 7px; + color: var(--color-text-muted); + text-decoration: none; +} + +.back-to-top:hover { + color: var(--color-brand); +} + +.whatsapp-float-wrap { + position: fixed; + z-index: 900; + right: 22px; + bottom: 22px; + width: 54px; + height: 54px; +} + +.whatsapp-float { + display: grid; + width: 54px; + height: 54px; + place-items: center; + color: #062722; + background: #55e6bd; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 50%; + box-shadow: 0 12px 34px rgba(32, 191, 142, 0.3); + text-decoration: none; + transition: transform 180ms var(--ease-out), box-shadow 180ms ease; +} + +.whatsapp-float:hover { + color: #062722; + box-shadow: 0 16px 42px rgba(32, 191, 142, 0.42); + transform: translateY(-3px) scale(1.02); +} + +.whatsapp-float svg { + width: 25px; + height: 25px; +} + +/* WordPress helpers */ +.sticky { + border-color: rgba(116, 213, 199, 0.38); +} + +.bypostauthor { + border-left: 2px solid var(--color-brand); +} + +.alignleft { + float: left; + margin: 0 1.5em 1em 0; +} + +.alignright { + float: right; + margin: 0 0 1em 1.5em; +} + +.aligncenter { + display: block; + margin-right: auto; + margin-left: auto; +} + +.wp-block-image img, +.entry-content img { + border-radius: var(--radius-sm); +} + +.wp-block-separator { + margin-block: 2.5em; + border-color: var(--color-line-soft); +} + +.wp-block-button__link { + border-radius: 12px; +} + +/* Motion reveal: content remains visible until JS opts in. */ +.has-reveal [data-reveal] { + opacity: 0; + transform: translateY(18px); + transition: opacity 520ms var(--ease-out), transform 520ms var(--ease-out); +} + +.has-reveal [data-reveal].is-visible { + opacity: 1; + transform: translateY(0); +} + +@media (max-width: 1080px) { + :root { + --gutter: clamp(20px, 4vw, 48px); + } + + .hero__inner { + gap: 36px; + grid-template-columns: minmax(0, 6fr) minmax(280px, 4fr); + } + + .services-grid, + .projects-grid, + .posts-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .project-card--featured { + grid-column: auto; + } + + .project-card--featured .project-card__media { + aspect-ratio: 16 / 10; + } + + .article-layout { + width: min(960px, calc(100% - (2 * var(--gutter)))); + grid-template-columns: minmax(0, 1fr); + } + + .article-layout .sidebar { + position: static; + display: grid; + gap: 18px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .article-layout .sidebar .widget { + margin-bottom: 0; + } +} + +@media (max-width: 920px) { + :root { + --header-height: 72px; + --hero-peek: 64px; + } + + .brand__role, + .site-description, + .header-cta { + display: none; + } + + .nav-toggle { + z-index: 1002; + display: block; + } + + .primary-nav { + position: fixed; + z-index: 1001; + top: 0; + right: 0; + bottom: 0; + display: flex; + width: min(88vw, 390px); + padding: calc(var(--header-height) + 36px) 30px 34px; + align-items: stretch; + flex-direction: column; + justify-content: flex-start; + background: rgba(7, 22, 36, 0.98); + border-left: 1px solid var(--color-line-soft); + box-shadow: -28px 0 70px rgba(0, 0, 0, 0.34); + visibility: hidden; + opacity: 0; + transform: translateX(100%); + transition: transform 260ms var(--ease-out), opacity 200ms ease, visibility 0s linear 260ms; + } + + .primary-nav.is-open, + .primary-nav[aria-hidden="false"] { + visibility: visible; + opacity: 1; + transform: translateX(0); + transition-delay: 0s; + } + + .primary-nav ul, + .primary-menu, + .site-header .menu { + width: 100%; + align-items: stretch; + flex-direction: column; + gap: 4px; + } + + .primary-nav li, + .primary-nav a, + .site-header .menu a { + width: 100%; + } + + .primary-nav a, + .site-header .menu a { + min-height: 54px; + padding-inline: 12px; + font-size: 1rem; + border-bottom: 1px solid rgba(142, 190, 207, 0.08); + } + + .primary-nav a::after, + .site-header .menu a::after { + display: none; + } + + .hero { + padding-block: 54px 96px; + } + + .hero__inner { + grid-template-columns: minmax(0, 1fr); + } + + .hero__content { + max-width: 760px; + } + + .hero__title { + max-width: 14ch; + } + + .hero__media { + position: absolute; + right: -7%; + bottom: 3%; + width: min(43vw, 390px); + opacity: 0.26; + } + + .hero__image, + .hero__media img { + width: 100%; + max-height: 45vh; + } + + .about-layout, + .contact-panel, + .newsletter-panel, + .content-grid, + .archive-layout { + grid-template-columns: minmax(0, 1fr); + } + + .contact-panel__form, + .newsletter-panel__form { + max-width: 680px; + } + + .archive-posts { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 782px) { + .admin-bar .site-header { + top: 46px; + } +} + +@media (max-width: 680px) { + :root { + --gutter: 20px; + --header-height: 68px; + --hero-peek: 56px; + --section-space: clamp(64px, 16vw, 84px); + } + + .brand__text, + .site-title { + display: none; + } + + .custom-logo, + .brand__image, + .site-logo { + width: 42px; + height: 42px; + } + + .hero-stage { + min-height: calc(100svh - var(--hero-peek)); + } + + .hero { + padding-block: 38px 92px; + } + + .hero__title { + margin-bottom: 21px; + font-size: clamp(2.55rem, 12vw, 3.8rem); + line-height: 1.01; + } + + .hero__description, + .hero__lead { + margin-bottom: 24px; + font-size: 1rem; + } + + .hero__media { + right: -24%; + bottom: 1%; + width: 74vw; + opacity: 0.16; + } + + .hero__actions { + align-items: stretch; + flex-direction: column; + } + + .hero__actions .button { + width: 100%; + } + + .scroll-cue { + right: 20px; + bottom: 16px; + left: auto; + transform: none; + } + + .section-heading { + align-items: flex-start; + flex-direction: column; + gap: 20px; + } + + .proof-grid, + .about-proof, + .stats-grid, + .services-grid, + .projects-grid, + .posts-grid, + .archive-posts, + .article-layout .sidebar, + .footer-grid { + grid-template-columns: minmax(0, 1fr); + } + + .service-card { + min-height: 0; + } + + .project-card__body, + .post-card__body { + padding: 21px; + } + + .contact-panel { + padding: 25px; + border-radius: var(--radius-md); + } + + .contact-panel__form, + .form-card { + padding: 18px; + } + + .newsletter-panel { + padding: 25px; + border-radius: var(--radius-md); + } + + .page-hero, + .archive-hero, + .search-hero, + .entry-hero { + padding-block: calc(var(--header-height) + 52px) 62px; + } + + .post-navigation .nav-links { + grid-template-columns: minmax(0, 1fr); + } + + .comment-list .children { + margin-left: 12px; + padding-left: 0; + } + + .footer-bottom { + align-items: flex-start; + flex-direction: column; + } + + .whatsapp-float-wrap { + right: 16px; + bottom: 16px; + } + + .whatsapp-float { + width: 50px; + height: 50px; + } + +} + +@media (max-height: 650px) and (min-width: 681px) { + .hero-stage { + min-height: 720px; + } + + .hero { + padding-block: 36px 90px; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + .has-reveal [data-reveal] { + opacity: 1; + transform: none; + } +} + +@media print { + :root { + --color-bg: #fff; + --color-text: #111; + --color-text-soft: #333; + } + + body { + color: #111; + background: #fff; + } + + .site-header, + .site-footer, + .newsletter-section, + .sidebar, + .post-navigation, + .comments-area { + display: none !important; + } + + .article-layout { + display: block; + width: 100%; + padding: 0; + } + + a { + color: #111; + } +} + +/* Classic template integration */ +.site-shell { + width: min(var(--container), calc(100% - (2 * var(--gutter)))); + margin-inline: auto; +} + +.site-header__inner { + display: flex; + height: 100%; + align-items: center; + justify-content: space-between; + gap: clamp(18px, 2.4vw, 32px); +} + +.site-header__inner .site-branding { + flex: 0 1 auto; + min-width: 0; +} + +.site-branding__text { + display: grid; + min-width: 0; + color: var(--color-text); + line-height: 1.05; + text-decoration: none; +} + +.site-branding__text:hover { color: var(--color-text); } +.site-branding__name { font-size: 0.92rem; font-weight: 780; } +.site-branding__tagline { + min-width: 0; + margin-top: 4px; + overflow: hidden; + color: var(--color-text-muted); + font-family: var(--font-mono); + font-size: 0.59rem; + letter-spacing: 0.045em; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} +.site-branding__tagline--short { display: none; } + +.primary-navigation { display: flex; align-items: center; gap: clamp(16px, 2vw, 30px); } +.primary-navigation a { + position: relative; + display: inline-flex; + min-height: 44px; + align-items: center; + color: var(--color-text-soft); + font-size: 0.84rem; + font-weight: 620; + text-decoration: none; + transition: color 180ms ease; +} + +.primary-navigation a::after { + position: absolute; + right: 0; + bottom: 7px; + left: 0; + height: 1px; + content: ""; + background: var(--color-brand); + transform: scaleX(0); + transform-origin: right; + transition: transform 220ms var(--ease-out); +} + +.primary-navigation a:hover, +.primary-navigation .current-menu-item > a { color: var(--color-text); } +.primary-navigation a:hover::after, +.primary-navigation .current-menu-item > a::after { transform: scaleX(1); transform-origin: left; } + +.site-header__contact { + display: inline-flex; + min-width: max-content; + min-height: 40px; + padding: 9px 14px; + align-items: center; + gap: 9px; + color: var(--color-text-soft); + background: rgba(116, 213, 199, 0.06); + border: 1px solid rgba(116, 213, 199, 0.22); + border-radius: 999px; + font-family: var(--font-mono); + font-size: 0.66rem; + text-decoration: none; +} + +.site-header__contact:hover { color: var(--color-brand); border-color: rgba(116, 213, 199, 0.48); } +.site-header__status, +.hero__availability-dot { + width: 8px; + height: 8px; + flex: 0 0 auto; + background: var(--color-success); + border-radius: 50%; + box-shadow: 0 0 0 5px rgba(123, 224, 161, 0.11); +} + +.menu-toggle { + position: relative; + display: none; + width: 46px; + height: 46px; + padding: 0; + color: var(--color-text); + background: rgba(255, 255, 255, 0.035); + border: 1px solid var(--color-line-soft); + border-radius: 12px; +} + +.menu-toggle__label { + position: absolute !important; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.menu-toggle__icon, +.menu-toggle__icon span { position: absolute; pointer-events: none; } +.menu-toggle__icon { inset: 0; } +.menu-toggle__icon span { + left: 50%; + width: 20px; + height: 2px; + background: currentColor; + border-radius: 999px; + transform: translateX(-50%); + transition: top 200ms ease, transform 200ms ease, opacity 160ms ease; +} +.menu-toggle__icon span:nth-child(1) { top: 15px; } +.menu-toggle__icon span:nth-child(2) { top: 22px; } +.menu-toggle__icon span:nth-child(3) { top: 29px; } +.menu-toggle[aria-expanded="true"] .menu-toggle__icon span:nth-child(1) { top: 22px; transform: translateX(-50%) rotate(45deg); } +.menu-toggle[aria-expanded="true"] .menu-toggle__icon span:nth-child(2) { opacity: 0; } +.menu-toggle[aria-expanded="true"] .menu-toggle__icon span:nth-child(3) { top: 22px; transform: translateX(-50%) rotate(-45deg); } + +.menu-drawer-close { display: none; } + +/* + * WordPress inserts its native Customizer shortcut just outside the left edge + * of a partial by default. The preview iframe clips that area behind the + * Customizer panel, so keep the native shortcut inside each theme section. + */ +.hero-stage > .customize-partial-edit-shortcut, +.section > .customize-partial-edit-shortcut { + z-index: 10; +} + +.hero-stage > .customize-partial-edit-shortcut button, +.section > .customize-partial-edit-shortcut button { + left: 22px !important; +} + +.hero-stage > .customize-partial-edit-shortcut button { + top: calc(var(--header-height) + 16px) !important; +} + +.section > .customize-partial-edit-shortcut button { + top: 18px !important; +} + +/* Native WordPress menu partials are attached to their rendered
    . */ +.primary-navigation .primary-menu[data-customize-partial-id], +.news-category-nav__list[data-customize-partial-id] { + position: relative; +} + +.primary-navigation .primary-menu[data-customize-partial-id] > .customize-partial-edit-shortcut, +.news-category-nav__list[data-customize-partial-id] > .customize-partial-edit-shortcut { + z-index: 20; +} + +.primary-navigation .primary-menu[data-customize-partial-id] > .customize-partial-edit-shortcut button, +.news-category-nav__list[data-customize-partial-id] > .customize-partial-edit-shortcut button { + left: -38px !important; + top: 0 !important; +} + +/* Explicit native shortcuts for the menu container and floating WhatsApp link. */ +#site-navigation[data-customize-partial-id] { + position: relative; +} + +.whatsapp-float-wrap[data-customize-partial-id] { position: fixed; } + +#site-navigation[data-customize-partial-id] > .customize-partial-edit-shortcut, +.whatsapp-float-wrap[data-customize-partial-id] > .customize-partial-edit-shortcut { + z-index: 1005; +} + +#site-navigation[data-customize-partial-id] > .customize-partial-edit-shortcut button { + left: -40px !important; + top: 8px !important; +} + +.whatsapp-float-wrap[data-customize-partial-id] > .customize-partial-edit-shortcut button { + left: -36px !important; + top: 2px !important; +} + +.hero.site-shell { + grid-template-columns: minmax(0, 7fr) minmax(320px, 5fr); + gap: clamp(38px, 6vw, 88px); +} + +.hero__visual { position: relative; z-index: 1; display: grid; place-items: center; } +.hero__visual-glow { + position: absolute; + z-index: -1; + width: 88%; + aspect-ratio: 1; + background: radial-gradient(circle, rgba(80, 229, 207, 0.16), transparent 68%); + filter: blur(12px); +} +.hero__visual .hero__image { width: min(43vw, 570px); max-height: 65vh; object-fit: contain; } +.hero__availability { + display: inline-flex; + margin: 0; + align-items: center; + gap: 10px; + color: var(--color-text-soft); + font-family: var(--font-mono); + font-size: 0.72rem; +} + +.section__header { max-width: 780px; margin-bottom: clamp(34px, 5vw, 58px); } +.section__header--split { + display: grid; + max-width: none; + align-items: end; + gap: 12px clamp(28px, 5vw, 70px); + grid-template-columns: minmax(0, 1fr) minmax(300px, 0.7fr); +} +.section__header--split .eyebrow { grid-column: 1 / -1; } +.section__title { margin-bottom: 0; } +.section__description { max-width: 58ch; margin: 0; color: var(--color-text-soft); } +.section__action { display: flex; margin-top: 36px; justify-content: center; } +.posts-grid--featured { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.section--about { padding-top: clamp(52px, 6vw, 80px); } + +.about-layout__header { position: sticky; top: calc(var(--header-height) + 28px); margin-bottom: 0; } +.about-layout__content .prose p { color: var(--color-text-soft); font-size: clamp(1rem, 1.3vw, 1.16rem); } +.about-layout__content .prose p + p { margin-top: 18px; } +.about-facts { + display: grid; + margin: 36px 0 0; + gap: 12px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} +.about-fact { + min-width: 0; + padding: 19px; + background: linear-gradient(145deg, rgba(17, 44, 66, 0.92), rgba(13, 36, 56, 0.86)); + border: 1px solid var(--color-line-soft); + border-radius: var(--radius-sm); +} +.about-fact dt { margin-bottom: 7px; color: var(--color-brand); font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase; } +.about-fact dd { margin: 0; color: var(--color-text-soft); font-size: 0.82rem; line-height: 1.5; } + +.service-card__number { display: block; margin-bottom: 52px; color: var(--color-brand); } +.service-card__title { margin-bottom: 14px; } +.service-card__description { margin-bottom: 0; } + +.project-card__link { + display: flex; + min-height: 100%; + flex: 1; + flex-direction: column; + color: inherit; + text-decoration: none; +} +.project-card__link:hover { color: inherit; } +.project-card__placeholder img { position: relative; z-index: 1; width: 80px; opacity: 0.88; } +.project-card__type { margin-bottom: 11px; color: var(--color-brand); font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase; } +.project-card__description { margin-bottom: 18px; color: var(--color-text-soft); font-size: 0.9rem; line-height: 1.65; } +.project-card__tags { display: flex; margin: 2px 0 18px; padding: 0; flex-wrap: wrap; gap: 6px; list-style: none; } +.project-card__tags li { + display: inline-flex; + min-height: 28px; + padding: 5px 9px; + align-items: center; + color: var(--color-text-soft); + background: rgba(116, 213, 199, 0.07); + border: 1px solid rgba(116, 213, 199, 0.18); + border-radius: 999px; + font-family: var(--font-mono); + font-size: 0.66rem; +} +.project-card__cta, +.post-card__more, +.text-link { + display: inline-flex; + margin-top: auto; + align-items: center; + gap: 7px; + color: var(--color-brand); + font-size: 0.78rem; + font-weight: 720; + text-decoration: none; +} +.post-card__more { position: relative; z-index: 2; } + +.contact-list { display: grid; margin: 28px 0 0; padding: 0; gap: 10px; list-style: none; } +.contact-list li { display: grid; gap: 2px; } +.contact-list li > span { color: var(--color-text-muted); font-family: var(--font-mono); font-size: 0.65rem; text-transform: uppercase; } +.contact-list a { color: var(--color-text-soft); text-decoration: none; } +.contact-list a:hover { color: var(--color-brand); } +.contact-panel__description { color: var(--color-text-soft); } +.form-privacy { margin: 14px 0 0; color: var(--color-text-muted); font-size: 0.68rem; line-height: 1.5; } +.section--newsletter { padding-block: clamp(54px, 7vw, 90px); background: var(--color-bg-deep); border-top: 1px solid var(--color-line-soft); } + +.article-layout__main { min-width: 0; } +.article-layout__main .posts-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.content-narrow { + width: min(920px, calc(100% - (2 * var(--gutter)))); + padding-block: calc(var(--header-height) + clamp(68px, 8vw, 112px)) var(--section-space); +} +.page-article__header { margin-bottom: 38px; } +.page-article__title { margin-bottom: 0; } +.page-article__media { margin-bottom: 42px; overflow: hidden; border: 1px solid var(--color-line-soft); border-radius: var(--radius-lg); } +.page-article__media img { width: 100%; } +.entry-hero .entry-meta { margin: 24px 0 0; } +.entry-hero .entry-meta a { color: var(--color-brand); text-decoration: none; } +.entry-terms + .entry-terms { margin-top: 10px; } +.entry-terms > span { color: var(--color-text-muted); } +.entry-terms a { color: var(--color-brand); } + +/* Post reading layout — compact editorial treatment inspired by the reference. */ +.single-post-main { + background: linear-gradient(180deg, #061521 0%, #071a29 42%, #061521 100%); +} + +.single-post-main .entry-hero { + padding-block: calc(var(--header-height) + 44px) 26px; + background: linear-gradient(180deg, #071b2a 0%, #081824 100%); + border-bottom: 2px solid var(--color-brand); +} + +.entry-hero__inner { + display: flex; + align-items: center; + flex-direction: column; + text-align: center; +} + +.entry-breadcrumbs { + display: flex; + margin-bottom: 30px; + align-items: center; + gap: 7px; + align-self: stretch; + color: #7890a0; + font-family: var(--font-mono); + font-size: 0.61rem; + text-align: left; +} + +.entry-breadcrumbs a { + color: inherit; + text-decoration: none; +} + +.entry-breadcrumbs a:hover { color: var(--color-brand); } + +.entry-category-badge { + display: inline-flex; + margin-bottom: 12px; + padding: 4px 11px; + color: #052b28; + background: var(--color-brand); + border-radius: 3px; + font-family: var(--font-mono); + font-size: 0.61rem; + font-weight: 800; + letter-spacing: 0.025em; + line-height: 1.1; + text-decoration: none; + text-transform: uppercase; +} + +a.entry-category-badge:hover { color: #052b28; background: var(--color-brand-strong); } + +.single-post-main .entry-hero h1 { + max-width: 28ch; + margin-bottom: 0; + font-size: clamp(2rem, 4.3vw, 3.55rem); + line-height: 1.04; + text-transform: uppercase; +} + +.entry-post-meta { + display: flex; + min-height: 48px; + align-items: center; + justify-content: space-between; + gap: 18px; + color: #c6d2d9; + border-bottom: 1px solid rgba(142, 190, 207, 0.18); + font-family: var(--font-mono); + font-size: 0.62rem; +} + +.entry-post-meta__author { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 7px; +} + +.entry-post-meta__author strong { color: #f4fafb; } + +.entry-share { + display: flex; + align-items: center; + gap: 5px; +} + +.entry-share a { + display: inline-grid; + width: 25px; + height: 25px; + place-items: center; + color: #071624; + background: var(--color-brand); + border-radius: 50%; + font-family: var(--font-sans); + font-size: 0.62rem; + font-weight: 800; + text-decoration: none; +} + +.entry-share a svg { + width: 14px; + height: 14px; +} + +.entry-share a:nth-child(2) { background: #2b86d8; color: #fff; } +.entry-share a:nth-child(3) { background: #050b12; color: #fff; } +.entry-share a:nth-child(4) { background: #2879a9; color: #fff; } +.entry-share a:hover { transform: translateY(-2px); filter: brightness(1.12); } + +.single-post-main .entry-featured-image { + width: min(1160px, calc(100% - (2 * var(--gutter)))); + margin-top: 28px; + border-radius: 0; + box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28); +} + +.single-post-main .entry-featured-image img { + max-height: 610px; + object-fit: cover; +} + +.single-post-main .article-layout { + padding-block: 26px 90px; + gap: clamp(30px, 5vw, 58px); +} + +.single-post-main .entry-content { + color: #d4dfe4; + font-size: 0.9rem; + line-height: 1.58; +} + +.single-post-main .entry-content h2, +.single-post-main .entry-content h3 { + margin-top: 1.7em; + font-size: 1.2rem; + letter-spacing: -0.02em; +} + +.single-post-main .entry-content p, +.single-post-main .entry-content ul, +.single-post-main .entry-content ol { + margin-bottom: 1.15em; +} + +.single-post-main .sidebar { + top: calc(var(--header-height) + 20px); +} + +.single-post-main .sidebar .widget { + padding: 18px; + background: linear-gradient(145deg, rgba(15, 40, 60, 0.96), rgba(8, 27, 42, 0.96)); + border-color: rgba(116, 213, 199, 0.12); + border-radius: 11px; +} + +.single-post-main .sidebar .widget h2, +.single-post-main .sidebar .widget h3 { + margin-bottom: 10px; + font-size: 0.82rem; +} + +.single-post-main .sidebar .widget_newsletter__text, +.single-post-main .sidebar .widget p { + font-size: 0.68rem; + line-height: 1.45; +} + +@media (max-width: 700px) { + .single-post-main .entry-hero { padding-block: calc(var(--header-height) + 28px) 22px; } + .entry-breadcrumbs { margin-bottom: 24px; } + .single-post-main .entry-hero h1 { font-size: clamp(1.65rem, 7vw, 2.25rem); } + .entry-post-meta { align-items: flex-start; flex-direction: column; padding-block: 10px; } + .entry-share { align-self: flex-end; } + .single-post-main .entry-featured-image { margin-top: 18px; } + .single-post-main .article-layout { padding-top: 20px; } +} + +/* Final single-post polish: compact editorial layout from the supplied reference. */ +.single-post-main { + --post-reading-font: "Segoe UI", "Helvetica Neue", Arial, sans-serif; +} + +.single-post-main .entry-hero { + padding-block: calc(var(--header-height) + 22px) 18px; + background: linear-gradient(180deg, #071b2a 0%, #081824 100%); +} + +.single-post-main .entry-breadcrumbs { display: none; } +.single-post-main .entry-category-badge { + margin-bottom: 10px; + padding: 4px 8px; + font-family: var(--post-reading-font); + font-size: 0.57rem; + letter-spacing: 0; +} + +.single-post-main .entry-hero h1 { + max-width: 620px; + font-family: var(--post-reading-font); + font-size: clamp(1.75rem, 4vw, 2.55rem); + font-weight: 750; + letter-spacing: -0.025em; + line-height: 1.08; + text-transform: none; +} + +.single-post-main .entry-post-meta { + min-height: 40px; + font-family: var(--post-reading-font); + font-size: 0.56rem; +} + +.single-post-main .entry-post-meta__author { gap: 4px; } +.single-post-main .entry-share { gap: 4px; } +.single-post-main .entry-share a { width: 19px; height: 19px; } +.single-post-main .entry-share a svg { width: 11px; height: 11px; } + +.single-post-main .entry-featured-image { + width: min(1160px, calc(100% - 6px)); + margin-top: 3px; +} + +.single-post-main .entry-featured-image img { + width: 100%; + max-height: 610px; + object-fit: cover; +} + +.single-post-main .article-layout { + width: min(1160px, calc(100% - 32px)); + padding-block: 18px 70px; + gap: clamp(18px, 4vw, 42px); + grid-template-columns: minmax(0, 1fr) minmax(92px, 0.34fr); +} + +.single-post-main .entry-content { + color: #d7e1e5; + font-family: var(--post-reading-font); + font-size: clamp(0.78rem, 1.1vw, 0.9rem); + line-height: 1.52; +} + +.single-post-main .entry-content h2, +.single-post-main .entry-content h3 { + margin-top: 1.55em; + margin-bottom: 0.55em; + font-family: var(--post-reading-font); + font-size: clamp(1rem, 1.4vw, 1.18rem); + font-weight: 750; + letter-spacing: -0.018em; +} + +.single-post-main .entry-content p, +.single-post-main .entry-content ul, +.single-post-main .entry-content ol { margin-bottom: 0.95em; } + +.single-post-main .sidebar { top: calc(var(--header-height) + 16px); } +.single-post-main .sidebar .widget { + padding: 12px; + background: linear-gradient(145deg, rgba(15, 40, 60, 0.96), rgba(8, 27, 42, 0.96)); + border-color: rgba(116, 213, 199, 0.14); + border-radius: 8px; +} + +.single-post-main .sidebar .widget h2, +.single-post-main .sidebar .widget h3 { + margin-bottom: 8px; + font-family: var(--post-reading-font); + font-size: 0.68rem; +} + +.single-post-main .sidebar .widget p, +.single-post-main .sidebar .widget label, +.single-post-main .sidebar .widget input, +.single-post-main .sidebar .widget button { font-family: var(--post-reading-font); font-size: 0.57rem; } + +@media (max-width: 700px) { + .single-post-main .entry-hero { padding-block: calc(var(--header-height) + 20px) 16px; } + .single-post-main .entry-hero h1 { font-size: clamp(1.52rem, 7vw, 2rem); } + .single-post-main .entry-post-meta { min-height: 38px; } + .single-post-main .article-layout { + width: calc(100% - 16px); + padding-top: 14px; + gap: 14px; + grid-template-columns: minmax(0, 1fr) minmax(88px, 96px); + } + .single-post-main .article-layout .sidebar { display: block; } + .single-post-main .article-layout .sidebar .widget { margin-bottom: 12px; } +} + +@media (max-width: 680px) { + body.single-post { --header-height: 30px; } + body.single-post .site-header__inner { gap: 8px; } + body.single-post .custom-logo { width: 18px; height: 18px; } + body.single-post .site-branding { gap: 5px; } + body.single-post .site-branding__name { font-size: 0.48rem; } + body.single-post .site-branding__tagline--short { display: block; margin-top: 1px; font-size: 0.3rem; } + body.single-post .menu-toggle { display: none; } + body.single-post .primary-navigation { + position: static; + display: flex; + width: auto; + padding: 0; + background: transparent; + border: 0; + box-shadow: none; + visibility: visible; + opacity: 1; + transform: none; + } + body.single-post .primary-navigation ul { flex-direction: row; gap: 7px; } + body.single-post .primary-navigation a { + min-height: 25px; + width: auto; + padding: 0; + border: 0; + font-size: 0.42rem; + } +} + +/* Readable type and fluid stacking on phones. */ +.single-post-main .entry-category-badge { font-size: 0.68rem; } +.single-post-main .entry-post-meta { font-size: 0.72rem; } +.single-post-main .entry-content { + font-size: clamp(0.98rem, 1.2vw, 1.06rem); + line-height: 1.68; +} +.single-post-main .entry-content h2, +.single-post-main .entry-content h3 { font-size: clamp(1.18rem, 1.8vw, 1.35rem); } +.single-post-main .sidebar .widget p, +.single-post-main .sidebar .widget label, +.single-post-main .sidebar .widget input, +.single-post-main .sidebar .widget button { font-size: 0.78rem; } + +/* Sharing, related articles and post-to-post navigation. */ +.single-post-main .entry-post-meta { font-size: 0.82rem; } +.single-post-main .entry-share { gap: 7px; } +.single-post-main .entry-share a { width: 30px; height: 30px; } +.single-post-main .entry-share a svg { width: 17px; height: 17px; } +.single-post-main .entry-share a:first-child { color: #fff; background: #25d366; } + +@media (min-width: 861px) { + .single-post-main .sidebar { + top: calc(var(--header-height) + 42px); + scroll-margin-top: calc(var(--header-height) + 42px); + } +} + +.post-share-panel { + display: flex; + margin-top: 42px; + padding-top: 28px; + align-items: center; + flex-wrap: wrap; + gap: 14px; + border-top: 1px solid var(--color-line-soft); +} +.post-share-panel__title { + margin: 0; + color: var(--color-text); + font-family: var(--post-reading-font); + font-size: 0.84rem; + font-weight: 800; + letter-spacing: 0.075em; + text-transform: uppercase; +} +.post-share-panel__links { + display: flex; + align-items: center; + gap: 9px; +} +.post-share-panel__links a { + display: grid; + width: 44px; + height: 44px; + place-items: center; + color: #fff; + background: var(--color-brand); + border-radius: 50%; + text-decoration: none; + transition: transform 180ms var(--ease-out), filter 180ms ease; +} +.post-share-panel__links a:first-child { background: #25d366; } +.post-share-panel__links a:nth-child(2) { background: #1877f2; } +.post-share-panel__links a:nth-child(3) { background: #050505; } +.post-share-panel__links a:nth-child(4) { background: #0a66c2; } +.post-share-panel__links a:hover { color: #fff; filter: brightness(1.12); transform: translateY(-2px); } +.post-share-panel__links svg { width: 23px; height: 23px; } + +.related-posts { margin-top: 42px; } +.related-posts__heading { + display: grid; + margin-bottom: 24px; + align-items: center; + gap: 12px; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); +} +.related-posts__heading span { height: 1px; background: var(--color-line-soft); } +.related-posts__heading h2 { + margin: 0; + color: var(--color-text-muted); + font-family: var(--post-reading-font); + font-size: 0.82rem; + font-weight: 500; + letter-spacing: 0.04em; +} +.related-posts__grid { display: grid; gap: 16px; grid-template-columns: repeat(3, minmax(0, 1fr)); } +.related-post__link { display: block; color: var(--color-text); text-decoration: none; } +.related-post__image { + width: 100%; + aspect-ratio: 16 / 9; + margin-bottom: 11px; + object-fit: cover; + border-radius: 7px; +} +.related-post__title { + margin: 0; + font-family: var(--post-reading-font); + font-size: 0.92rem; + font-weight: 750; + line-height: 1.35; + letter-spacing: -0.015em; +} +.related-post__link:hover { color: var(--color-brand); } + +.single-post-main .post-navigation--editorial { margin-top: 44px; } +.single-post-main .post-navigation--editorial .nav-links { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.single-post-main .post-navigation--editorial .nav-next { text-align: right; } +.single-post-main .post-navigation--editorial a { min-height: 124px; background: rgba(13, 36, 56, 0.56); } + +@media (max-width: 680px) { + .single-post-main .entry-post-meta { font-size: 0.78rem; } + .single-post-main .entry-share a { width: 34px; height: 34px; } + .single-post-main .entry-share a svg { width: 19px; height: 19px; } + .post-share-panel { margin-top: 34px; gap: 12px; } + .post-share-panel__links { width: 100%; gap: 10px; } + .post-share-panel__links a { width: 46px; height: 46px; } + .post-share-panel__links svg { width: 24px; height: 24px; } + .related-posts { margin-top: 34px; } + .related-posts__grid { gap: 22px; grid-template-columns: minmax(0, 1fr); } + .related-post__title { font-size: 1rem; } + .single-post-main .post-navigation--editorial .nav-links { grid-template-columns: minmax(0, 1fr); } + .single-post-main .post-navigation--editorial .nav-next { text-align: left; } +} + +@media (max-width: 860px) { + .single-post-main .article-layout { + width: min(720px, calc(100% - 32px)); + grid-template-columns: minmax(0, 1fr); + } + .single-post-main .sidebar { + position: static; + display: grid; + gap: 16px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .single-post-main .sidebar .widget { margin-bottom: 0; } +} + +@media (max-width: 680px) { + body.single-post { --header-height: 64px; } + body.single-post .site-header__inner { gap: 12px; } + body.single-post .custom-logo { width: 30px; height: 30px; } + body.single-post .site-branding { gap: 8px; } + body.single-post .site-branding__name { font-size: 0.78rem; } + body.single-post .site-branding__tagline--short { margin-top: 2px; font-size: 0.42rem; } + body.single-post .menu-toggle { display: block; } + body.single-post .primary-navigation { + position: fixed; + z-index: 1001; + top: 0; + right: 0; + bottom: 0; + display: flex; + width: min(88vw, 390px); + padding: calc(var(--header-height) + 36px) 30px 34px; + background: rgba(7, 22, 36, 0.98); + border-left: 1px solid var(--color-line-soft); + box-shadow: -28px 0 70px rgba(0, 0, 0, 0.34); + visibility: hidden; + opacity: 0; + transform: translateX(100%); + } + body.single-post .primary-navigation.is-open, + body.single-post .primary-navigation[aria-hidden="false"] { + visibility: visible; + opacity: 1; + transform: translateX(0); + } + body.single-post .primary-navigation ul { + width: 100%; + align-items: stretch; + flex-direction: column; + gap: 4px; + } + body.single-post .primary-navigation a { + min-height: 52px; + width: 100%; + padding-inline: 12px; + font-size: 1rem; + border-bottom: 1px solid rgba(142, 190, 207, 0.08); + } + .single-post-main .entry-hero { padding-block: calc(var(--header-height) + 18px) 22px; } + .single-post-main .entry-hero h1 { + max-width: 20ch; + font-size: clamp(1.85rem, 8.5vw, 2.3rem); + line-height: 1.1; + } + .single-post-main .entry-post-meta { + min-height: 48px; + align-items: center; + flex-direction: row; + flex-wrap: wrap; + padding-block: 10px; + gap: 10px; + font-size: 0.7rem; + } + .single-post-main .entry-share { margin-left: auto; } + .single-post-main .entry-featured-image { margin-top: 8px; } + .single-post-main .article-layout { + width: calc(100% - 32px); + padding-block: 24px 58px; + gap: 28px; + } + .single-post-main .entry-content { font-size: 1rem; line-height: 1.7; } + .single-post-main .entry-content h2, + .single-post-main .entry-content h3 { font-size: 1.25rem; } + .single-post-main .sidebar { + display: grid; + grid-template-columns: minmax(0, 1fr); + } + .single-post-main .sidebar .widget { padding: 18px; } +} +.page-hero__actions { margin-top: 28px; } +.page-hero__search { max-width: 560px; margin-top: 26px; } +.results-count { color: var(--color-text-muted); } + +.search-form__label { flex: 1; } +.search-form__field { + appearance: none; + width: 100%; + min-height: 48px; + padding: 11px 13px; + color: var(--color-text) !important; + background-color: #071827 !important; + border: 1px solid var(--color-line) !important; + border-radius: 9px; +} +.search-form__field:focus { border-color: var(--color-brand); box-shadow: 0 0 0 3px rgba(116, 213, 199, 0.15); outline: none; } +.search-form__submit { + display: inline-flex; + min-width: 48px; + min-height: 48px; + padding: 10px 15px; + align-items: center; + justify-content: center; + color: var(--color-brand-ink); + background: var(--color-brand); + border: 1px solid var(--color-brand); + border-radius: 9px; + font-weight: 720; +} + +.widget .wp-block-search__label { + display: block; + margin-bottom: 7px; + color: var(--color-text-soft); +} + +.widget .wp-block-search__inside-wrapper { + display: flex; + gap: 8px; +} + +.widget .wp-block-search__input { + appearance: none; + min-width: 0; + min-height: 48px; + padding: 11px 13px; + color: var(--color-text) !important; + background: #071827 !important; + border: 1px solid var(--color-line) !important; + border-radius: 9px; +} + +.widget .wp-block-search__input:focus { + border-color: var(--color-brand) !important; + box-shadow: 0 0 0 3px rgba(116, 213, 199, 0.15); + outline: none; +} + +.widget .wp-block-search__button { + min-height: 48px; + margin-left: 0; + border: 1px solid var(--color-brand); +} + +.error-404 { display: block; min-height: 0; padding: 0; text-align: left; } +.error-404 .page-hero__inner { margin-inline: auto; text-align: center; } +.error-404 .page-hero__actions { display: flex; justify-content: center; } +.error-404 .page-hero__search { margin-inline: auto; } + +.site-footer__main { + display: grid; + margin-bottom: 48px; + gap: clamp(32px, 6vw, 78px); + grid-template-columns: minmax(280px, 1.2fr) minmax(260px, 0.8fr) minmax(150px, 0.45fr); +} +.site-footer__brand { max-width: 470px; } +.site-footer__brand-row { display: flex; margin-bottom: 18px; align-items: center; gap: 12px; } +.site-footer__name { margin: 0; color: var(--color-text); font-weight: 760; } +.site-footer__brand > p { color: var(--color-text-muted); font-size: 0.88rem; } +.site-footer__widgets { display: grid; gap: 18px; grid-template-columns: repeat(2, minmax(0, 1fr)); } +.site-footer__widgets .widget { margin: 0; } +.footer-navigation .footer-menu { margin: 0; padding: 0; list-style: none; } +.site-footer__bottom { + display: flex; + padding-top: 24px; + align-items: center; + justify-content: space-between; + gap: 20px; + color: var(--color-text-muted); + border-top: 1px solid var(--color-line-soft); + font-family: var(--font-mono); + font-size: 0.68rem; +} +.site-footer__bottom p { margin: 0; } +.social-links a { width: auto; min-width: 42px; padding-inline: 12px; font-size: 0.72rem; } + +@media (max-width: 1080px) { + .hero.site-shell { gap: 36px; grid-template-columns: minmax(0, 6fr) minmax(280px, 4fr); } + .site-header__contact { display: none; } + .site-footer__main { grid-template-columns: minmax(260px, 1fr) minmax(260px, 1fr); } + .footer-navigation { grid-column: 1 / -1; } +} + +@media (max-width: 920px) { + :root { --hero-peek: 120px; } + .menu-toggle { z-index: 1002; display: block; } + .primary-navigation { + position: fixed; + z-index: 1001; + top: 0; + right: 0; + bottom: 0; + display: flex; + width: min(88vw, 390px); + padding: calc(var(--header-height) + 36px) 30px 34px; + align-items: stretch; + flex-direction: column; + justify-content: flex-start; + background: rgba(7, 22, 36, 0.98); + border-left: 1px solid var(--color-line-soft); + box-shadow: -28px 0 70px rgba(0, 0, 0, 0.34); + visibility: hidden; + opacity: 0; + transform: translateX(100%); + transition: transform 260ms var(--ease-out), opacity 200ms ease, visibility 0s linear 260ms; + } + .primary-navigation.is-open, + .primary-navigation[aria-hidden="false"] { visibility: visible; opacity: 1; transform: translateX(0); transition-delay: 0s; } + .primary-navigation ul { width: 100%; align-items: stretch; flex-direction: column; gap: 4px; } + .primary-navigation li, + .primary-navigation a { width: 100%; } + .primary-navigation a { min-height: 54px; padding-inline: 12px; font-size: 1rem; border-bottom: 1px solid rgba(142, 190, 207, 0.08); } + .primary-navigation a::after { display: none; } + .hero.site-shell { grid-template-columns: minmax(0, 1fr); } + .hero__visual { position: absolute; right: -7%; bottom: 3%; width: min(43vw, 390px); opacity: 0.25; } + .hero__visual .hero__image { width: 100%; max-height: 45vh; } + .section__header--split { grid-template-columns: minmax(0, 1fr); } + .about-layout__header { position: static; } + .site-footer__main { grid-template-columns: minmax(0, 1fr); } + .footer-navigation { grid-column: auto; } +} + +@media (max-width: 680px) { + :root { --hero-peek: 118px; } + .site-branding__text { display: grid; min-width: 0; } + .site-branding__tagline--full { display: none; } + .site-branding__tagline--short { display: block; max-width: none; overflow: visible; font-size: 0.47rem; letter-spacing: 0.01em; text-overflow: clip; } + .hero { padding-block: 28px 62px; } + .hero__title { font-size: clamp(2.35rem, 11vw, 3.1rem); } + .hero__actions { display: grid; align-items: stretch; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } + .hero__actions .button { width: 100%; padding-inline: 12px; } + .hero__visual { right: -24%; bottom: 1%; width: 74vw; opacity: 0.14; } + .section--about { padding-top: 40px; } + .about-facts, + .site-footer__widgets { grid-template-columns: minmax(0, 1fr); } + .service-card__number { margin-bottom: 30px; } + .site-footer__bottom { align-items: flex-start; flex-direction: column; } + .posts-grid--featured { grid-template-columns: minmax(0, 1fr); } + .article-layout__main .posts-grid { grid-template-columns: minmax(0, 1fr); } +} + +@media print { + .site-header, + .site-footer, + .section--newsletter, + .sidebar, + .whatsapp-float, + .post-navigation, + .comments-area { display: none !important; } +} + +/* Editorial news index and category pages. */ +.news-category-nav { + position: relative; + z-index: 20; + overflow-x: auto; + color: #dfe5ee; + background: #0b1c3f; + border-top: 1px solid rgba(255, 255, 255, 0.13); + border-bottom: 1px solid rgba(255, 255, 255, 0.13); +} + +/* Inner editorial pages must reserve space for the regular site header. */ +.site-header:not(.site-header--overlay) { + position: relative; + background: var(--color-bg); + border-bottom-color: var(--color-line-soft); +} + +.news-category-nav__inner { + display: flex; + min-height: 42px; + padding: 0; + align-items: stretch; + white-space: nowrap; +} + +.news-category-nav__list { + display: flex; + min-height: 42px; + margin: 0; + padding: 0; + align-items: stretch; + list-style: none; +} + +.news-category-nav__list a { + display: inline-flex; + min-height: 42px; + padding: 0 17px; + align-items: center; + color: inherit; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.055em; + text-decoration: none; + text-transform: uppercase; +} + +.news-category-nav__link { + display: inline-flex; + min-height: 42px; + padding: 0 17px; + align-items: center; + color: inherit; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.055em; + text-decoration: none; + text-transform: uppercase; +} + +.news-category-nav__link:hover, +.news-category-nav__link.is-current { + color: #d6f52a; + background: rgba(255, 255, 255, 0.055); +} + +.news-category-nav__list a:hover, +.news-category-nav__list .current-menu-item > a, +.news-category-nav__list .current-menu-ancestor > a { + color: #d6f52a; + background: rgba(255, 255, 255, 0.055); +} + +.news-index { + min-height: 55vh; + color: #0a1b43; + background: #fff; +} + +.news-page-heading { + padding: 17px 0 16px; + color: #fff; + background: #0b1c3f; + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.25); +} + +.news-page-heading h1 { + display: flex; + margin: 0; + align-items: center; + gap: 10px; + color: #fff; + font-size: clamp(1.7rem, 3vw, 2.25rem); + font-weight: 780; + letter-spacing: -0.025em; +} + +.news-page-heading h1::before { + width: 7px; + height: 21px; + content: ""; + background: #d6f52a; + border-radius: 3px; +} + +.news-page-heading__description { + max-width: 68ch; + margin: 8px 0 0 17px; + color: rgba(255, 255, 255, 0.8); + font-size: 0.94rem; +} + +.news-index__content { + padding-top: clamp(34px, 4vw, 44px); + padding-bottom: clamp(58px, 7vw, 90px); +} + +.latest-news-grid { + display: grid; + gap: 28px 22px; + grid-template-columns: minmax(0, 2fr) repeat(2, minmax(0, 1fr)); +} + +.latest-news-card { + min-width: 0; +} + +.latest-news-card--featured { + grid-row: span 2; +} + +.latest-news-card__media { + display: block; + overflow: hidden; + aspect-ratio: 16 / 9; + background: #dce2e8; + border-radius: 6px; +} + +.latest-news-card--featured .latest-news-card__media { + height: 100%; + max-height: 460px; + aspect-ratio: auto; +} + +.latest-news-card__image, +.latest-news-card__placeholder { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 240ms var(--ease-out); +} + +.latest-news-card__placeholder { + display: block; + background: linear-gradient(135deg, #bfc9d2, #eff2f4); +} + +.latest-news-card:hover .latest-news-card__image, +.latest-news-card:focus-within .latest-news-card__image { + transform: scale(1.035); +} + +.latest-news-card__body { + padding-top: 10px; +} + +.latest-news-card__category { + display: inline-flex; + margin-bottom: 10px; + padding: 4px 8px; + color: #07142e; + background: #d6f52a; + border-radius: 4px; + font-size: 0.66rem; + font-weight: 800; + letter-spacing: 0.035em; + line-height: 1; + text-decoration: none; + text-transform: uppercase; +} + +.latest-news-card__category:hover { color: #07142e; background: #c5e72b; } + +.latest-news-card__title { + margin: 0 0 10px; + color: #0a1b43; + font-size: clamp(1rem, 1.35vw, 1.13rem); + font-weight: 760; + letter-spacing: -0.025em; + line-height: 1.2; +} + +.latest-news-card--featured .latest-news-card__title { + font-size: clamp(1.6rem, 2.5vw, 2.25rem); + line-height: 1.16; +} + +.latest-news-card__title a { color: inherit; text-decoration: none; } +.latest-news-card__title a:hover { color: #35560d; } + +.latest-news-card__date { + display: block; + color: #0a1b43; + font-size: 0.78rem; + line-height: 1.3; +} + +.latest-news-card__excerpt { + margin: 11px 0 0; + color: #17264a; + font-size: 0.95rem; + line-height: 1.45; +} + +@media (max-width: 860px) { + .latest-news-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .latest-news-card--featured { grid-column: 1 / -1; grid-row: auto; } + .latest-news-card--featured .latest-news-card__media { height: auto; aspect-ratio: 16 / 8; } +} + +@media (max-width: 560px) { + .news-category-nav__link { padding-inline: 13px; font-size: 0.68rem; } + .news-page-heading { padding-block: 14px; } + .news-index__content { padding-top: 26px; } + .latest-news-grid { gap: 28px; grid-template-columns: minmax(0, 1fr); } + .latest-news-card--featured { grid-column: auto; } + .latest-news-card--featured .latest-news-card__media { aspect-ratio: 16 / 9; } +} + +/* The reference establishes the editorial composition; these rules retain the theme identity. */ +.news-category-nav { + color: var(--color-text-soft); + background: var(--color-bg-alt); + border-color: var(--color-line-soft); +} + +.news-category-nav__link:hover, +.news-category-nav__link.is-current { + color: var(--color-brand-strong); + background: rgba(116, 213, 199, 0.09); +} + +.news-category-nav__list a:hover, +.news-category-nav__list .current-menu-item > a, +.news-category-nav__list .current-menu-ancestor > a { + color: var(--color-brand-strong); + background: rgba(116, 213, 199, 0.09); +} + +.news-index { + color: var(--color-text); + background: var(--color-bg-deep); +} + +.news-page-heading { + background: + radial-gradient(circle at 78% 35%, rgba(116, 213, 199, 0.11), transparent 28%), + linear-gradient(145deg, var(--color-bg), var(--color-bg-alt)); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.2); +} + +.news-page-heading h1::before { background: var(--color-brand); } +.news-page-heading__description { color: var(--color-text-soft); } +.latest-news-card__media { background: var(--color-surface); } +.latest-news-card__placeholder { background: linear-gradient(135deg, var(--color-surface-raised), var(--color-bg-alt)); } + +.latest-news-card__category { + color: var(--color-brand-ink); + background: var(--color-brand); +} + +.latest-news-card__category:hover { color: var(--color-brand-ink); background: var(--color-brand-strong); } +.latest-news-card__title, +.latest-news-card__date { color: var(--color-text); } +.latest-news-card__title a:hover { color: var(--color-brand-strong); } +.latest-news-card__excerpt { color: var(--color-text-soft); } + +.latest-news-list { + width: min(100%, 720px); + margin-top: clamp(64px, 8vw, 100px); +} + +.latest-news-list__heading { + display: flex; + margin: 0 0 40px; + align-items: center; + gap: 10px; + color: var(--color-text); + font-size: clamp(1.15rem, 1.8vw, 1.45rem); +} + +.latest-news-list__heading::before { + width: 5px; + height: 21px; + content: ""; + background: var(--color-brand); + border-radius: 4px; +} + +.latest-news-list__items { display: grid; gap: 28px; } + +.latest-news-list__item { + display: grid; + min-width: 0; + align-items: center; + gap: 17px; + grid-template-columns: 120px minmax(0, 1fr); +} + +.latest-news-list__image { + display: block; + overflow: hidden; + aspect-ratio: 16 / 10; + background: var(--color-surface); + border-radius: var(--radius-sm); +} + +.latest-news-list__image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 220ms var(--ease-out); +} + +.latest-news-list__item:hover .latest-news-list__image img { transform: scale(1.05); } +.latest-news-list__body .latest-news-card__category { margin-bottom: 8px; } + +.latest-news-list__body h3 { + margin: 0 0 7px; + color: var(--color-text); + font-size: clamp(0.94rem, 1.4vw, 1.08rem); + line-height: 1.28; +} + +.latest-news-list__body h3 a { color: inherit; text-decoration: none; } +.latest-news-list__body h3 a:hover { color: var(--color-brand-strong); } +.latest-news-list__body time { color: var(--color-text-muted); font-size: 0.76rem; } + +@media (max-width: 560px) { + .latest-news-list { margin-top: 58px; } + .latest-news-list__heading { margin-bottom: 28px; } + .latest-news-list__item { gap: 13px; grid-template-columns: 92px minmax(0, 1fr); } +} + +/* Editorial page header order: site header, categories, then page heading. */ +.site-header--editorial, +.site-header--editorial.is-scrolled { + position: sticky; + top: 0; + right: auto; + left: auto; + background: var(--color-bg); + border-bottom: 1px solid var(--color-line-soft); + box-shadow: none; + backdrop-filter: none; +} + +/* WordPress adds a top offset for the admin bar to fixed headers. This + header is in normal document flow, so that offset would cover the category + navigation below it. */ +.admin-bar .site-header--editorial, +.admin-bar .site-header--editorial.is-scrolled { + /* WordPress already applies a top document margin for #wpadminbar. */ + top: 0; +} + +.site-header--editorial + .news-category-nav { + position: relative; + z-index: 10; +} + +.site-header--editorial + .news-category-nav + .news-index .news-page-heading { + position: relative; + z-index: 1; +} + +.mobile-news-categories { display: none; } + +@media (max-width: 920px) { + /* One solid, scrollable drawer for every page type. */ + .primary-navigation, + .site-header--editorial .primary-navigation { + position: fixed !important; + top: 0 !important; + right: 0 !important; + bottom: 0 !important; + height: 100dvh !important; + min-height: 100dvh !important; + max-height: none !important; + z-index: 100000 !important; + /* Overrides the legacy mobile drawer padding (which left a large gap). */ + padding: 62px 24px 78px !important; + background-color: #071624 !important; + background-image: none !important; + isolation: isolate; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: var(--color-brand) transparent; + } + .primary-navigation.is-open, + .primary-navigation[aria-hidden="false"] { + opacity: 1 !important; + } + .menu-toggle { z-index: 100001 !important; } + .menu-drawer-close { + position: absolute; + top: 14px; + right: 18px; + z-index: 3; + display: grid; + width: 46px; + height: 46px; + padding: 0; + place-items: center; + color: var(--color-text); + background: rgba(255, 255, 255, 0.035); + border: 1px solid var(--color-line-soft); + border-radius: 12px; + } + .menu-drawer-close span { + position: absolute; + width: 20px; + height: 2px; + background: currentColor; + border-radius: 999px; + } + .menu-drawer-close span:first-child { transform: rotate(45deg); } + .menu-drawer-close span:last-child { transform: rotate(-45deg); } + .admin-bar .primary-navigation { top: 32px !important; height: calc(100dvh - 32px) !important; min-height: calc(100dvh - 32px) !important; } + .primary-navigation::-webkit-scrollbar, + .site-header--editorial .primary-navigation::-webkit-scrollbar { width: 6px; } + .primary-navigation::-webkit-scrollbar-thumb, + .site-header--editorial .primary-navigation::-webkit-scrollbar-thumb { background: var(--color-brand); border-radius: 999px; } + .primary-navigation::after, + .site-header--editorial .primary-navigation::after { + position: absolute; + right: 22px; + bottom: 17px; + z-index: 2; + display: grid; + width: 34px; + height: 34px; + place-items: center; + color: var(--color-brand-ink); + background: var(--color-brand); + border-radius: 50%; + box-shadow: 0 8px 22px rgba(0, 0, 0, 0.3); + content: "↓"; + font-size: 1.1rem; + font-weight: 800; + opacity: 0; + pointer-events: none; + transform: translateY(8px); + transition: opacity 160ms ease, transform 160ms ease; + } + .primary-navigation.is-open.has-more-content::after, + .site-header--editorial .primary-navigation.is-open.has-more-content::after { + opacity: 1; + transform: translateY(0); + } + .site-header--editorial + .news-category-nav { display: none; } + .primary-navigation .mobile-news-categories { + display: block; + width: 100%; + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid var(--color-line-soft); + } + .mobile-news-categories__title { + margin: 0 0 8px; + color: var(--color-text-muted); + font-family: var(--font-mono); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + } + .primary-navigation .mobile-news-categories__list { + display: grid; + width: 100%; + margin: 0; + padding: 0; + gap: 2px; + list-style: none; + } + .primary-navigation .mobile-news-categories__list a { + min-height: 46px; + color: var(--color-text-soft); + } + .primary-navigation .mobile-news-categories__list a.is-current, + .primary-navigation .mobile-news-categories__list .current-menu-item > a, + .primary-navigation .mobile-news-categories__list .current-menu-ancestor > a { color: var(--color-brand-strong); } + .admin-bar .site-header--editorial .primary-navigation { top: 32px; } +} + +@media (max-width: 782px) { + .admin-bar .site-header--editorial, + .admin-bar .site-header--editorial.is-scrolled { top: 0; } + .admin-bar .site-header--editorial .primary-navigation { top: 46px; } + .admin-bar .primary-navigation { top: 46px !important; height: calc(100dvh - 46px) !important; min-height: calc(100dvh - 46px) !important; } +} + +/* Keep the long professional tagline from stretching the mobile header. */ +@media (max-width: 680px) { + .site-header__inner { gap: 12px; } + .site-header__inner .site-branding { + min-width: 0; + max-width: calc(100% - 64px); + } + .site-header__inner .site-branding__text { min-width: 0; max-width: 100%; } + .site-header__inner .site-branding__tagline--full { display: none !important; } + .site-header__inner .site-branding__tagline--short { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .site-header__inner .menu-toggle { flex: 0 0 46px; } +} + +@media (max-width: 680px) { + .single-post-main .entry-post-meta { justify-content: center; } + .single-post-main .entry-share { + width: 100%; + margin: 6px auto 0 !important; + justify-content: center; + } +} + +/* The single-post hero used to compensate for an absolute header. The + editorial header is now in normal flow, so that extra top space must not + be added again. */ +.site-header--editorial ~ .single-post-main .entry-hero { + padding-top: clamp(28px, 4vw, 54px); +} + +@media (max-width: 700px) { + .site-header--editorial ~ .single-post-main .entry-hero { + padding-top: 24px; + } +} diff --git a/wp-content/themes/gustavoo-portfolio/assets/fonts/INTER-LICENSE.txt b/wp-content/themes/gustavoo-portfolio/assets/fonts/INTER-LICENSE.txt new file mode 100644 index 0000000..b525cbf --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/assets/fonts/INTER-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/wp-content/themes/gustavoo-portfolio/assets/fonts/inter-variable.woff2 b/wp-content/themes/gustavoo-portfolio/assets/fonts/inter-variable.woff2 new file mode 100644 index 0000000..350bbbc Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/fonts/inter-variable.woff2 differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/favicon-32.png b/wp-content/themes/gustavoo-portfolio/assets/images/favicon-32.png new file mode 100644 index 0000000..0c5b628 Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/favicon-32.png differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/hero-art.png b/wp-content/themes/gustavoo-portfolio/assets/images/hero-art.png new file mode 100644 index 0000000..2dd4aae Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/hero-art.png differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/hero-art.webp b/wp-content/themes/gustavoo-portfolio/assets/images/hero-art.webp new file mode 100644 index 0000000..de69f53 Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/hero-art.webp differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.png b/wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.png new file mode 100644 index 0000000..8b24e47 Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.png differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.webp b/wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.webp new file mode 100644 index 0000000..8d3f538 Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.webp differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/site-icon-192.png b/wp-content/themes/gustavoo-portfolio/assets/images/site-icon-192.png new file mode 100644 index 0000000..c61a7bf Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/site-icon-192.png differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/images/site-icon-512.png b/wp-content/themes/gustavoo-portfolio/assets/images/site-icon-512.png new file mode 100644 index 0000000..8b24e47 Binary files /dev/null and b/wp-content/themes/gustavoo-portfolio/assets/images/site-icon-512.png differ diff --git a/wp-content/themes/gustavoo-portfolio/assets/js/navigation.js b/wp-content/themes/gustavoo-portfolio/assets/js/navigation.js new file mode 100644 index 0000000..da56876 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/assets/js/navigation.js @@ -0,0 +1,273 @@ +(function () { + "use strict"; + + const ready = (callback) => { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", callback, { once: true }); + return; + } + + callback(); + }; + + ready(function () { + const header = document.querySelector(".site-header"); + const toggle = document.querySelector("[data-nav-toggle], .nav-toggle, .menu-toggle"); + const navigation = document.querySelector("[data-primary-nav], .primary-nav, .primary-navigation"); + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + let lastFocusedElement = null; + + const updateHeader = () => { + if (!header) { + return; + } + + header.classList.toggle("is-scrolled", window.scrollY > 24); + }; + + updateHeader(); + window.addEventListener("scroll", updateHeader, { passive: true }); + + if (toggle && navigation) { + const drawerClose = navigation.querySelector("[data-nav-close]"); + const navigationParent = navigation.parentNode; + const navigationNextSibling = navigation.nextSibling; + let navigationIsDrawer = false; + + const placeNavigation = (asDrawer) => { + if (asDrawer && !navigationIsDrawer) { + document.body.appendChild(navigation); + navigationIsDrawer = true; + } else if (!asDrawer && navigationIsDrawer) { + navigationParent.insertBefore(navigation, navigationNextSibling); + navigationIsDrawer = false; + } + }; + + if (!navigation.id) { + navigation.id = "site-navigation"; + } + + toggle.setAttribute("aria-controls", navigation.id); + toggle.setAttribute("aria-expanded", "false"); + navigation.setAttribute("aria-hidden", "true"); + + const focusableSelector = [ + "a[href]", + "button:not([disabled])", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "[tabindex]:not([tabindex='-1'])", + ].join(","); + + const closeMenu = (restoreFocus) => { + toggle.setAttribute("aria-expanded", "false"); + const toggleLabel = toggle.querySelector(".menu-toggle__label"); + if (toggleLabel && window.gustavooPortfolioNavigation) { + toggleLabel.textContent = window.gustavooPortfolioNavigation.expand; + } + navigation.setAttribute("aria-hidden", "true"); + navigation.classList.remove("is-open"); + navigation.classList.remove("has-more-content"); + document.body.classList.remove("nav-open"); + + if (restoreFocus && lastFocusedElement) { + lastFocusedElement.focus(); + } + }; + + const openMenu = () => { + lastFocusedElement = document.activeElement; + toggle.setAttribute("aria-expanded", "true"); + const toggleLabel = toggle.querySelector(".menu-toggle__label"); + if (toggleLabel && window.gustavooPortfolioNavigation) { + toggleLabel.textContent = window.gustavooPortfolioNavigation.collapse; + } + navigation.setAttribute("aria-hidden", "false"); + navigation.classList.add("is-open"); + document.body.classList.add("nav-open"); + + window.requestAnimationFrame(function () { + updateNavigationScrollHint(); + }); + + const firstFocusable = navigation.querySelector(focusableSelector); + if (firstFocusable) { + window.requestAnimationFrame(() => firstFocusable.focus()); + } + }; + + const updateNavigationScrollHint = () => { + const hasMoreContent = navigation.scrollHeight > navigation.clientHeight + 4 + && navigation.scrollTop + navigation.clientHeight < navigation.scrollHeight - 4; + navigation.classList.toggle("has-more-content", hasMoreContent); + }; + + toggle.addEventListener("click", function () { + if (toggle.getAttribute("aria-expanded") === "true") { + closeMenu(false); + } else { + openMenu(); + } + }); + + if (drawerClose) { + drawerClose.addEventListener("click", function () { + closeMenu(true); + }); + } + + navigation.addEventListener("click", function (event) { + if (event.target.closest("a")) { + closeMenu(false); + } + }); + + navigation.addEventListener("scroll", updateNavigationScrollHint, { passive: true }); + window.addEventListener("resize", updateNavigationScrollHint, { passive: true }); + + document.addEventListener("keydown", function (event) { + if (toggle.getAttribute("aria-expanded") !== "true") { + return; + } + + if (event.key === "Escape") { + event.preventDefault(); + closeMenu(true); + return; + } + + if (event.key !== "Tab") { + return; + } + + const focusable = Array.from(navigation.querySelectorAll(focusableSelector)); + focusable.push(toggle); + + if (!focusable.length) { + return; + } + + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }); + + const desktopQuery = window.matchMedia("(min-width: 921px)"); + const handleDesktop = (event) => { + placeNavigation(!event.matches); + + if (event.matches) { + closeMenu(false); + navigation.removeAttribute("aria-hidden"); + } else if (toggle.getAttribute("aria-expanded") !== "true") { + navigation.setAttribute("aria-hidden", "true"); + } + }; + + handleDesktop(desktopQuery); + desktopQuery.addEventListener("change", handleDesktop); + } + + if (window.location.hash.length > 1) { + const hashId = decodeURIComponent(window.location.hash.slice(1)); + const hashTarget = document.getElementById(hashId); + + if (hashTarget) { + window.requestAnimationFrame(function () { + window.setTimeout(function () { + hashTarget.scrollIntoView({ behavior: "auto", block: "start" }); + }, 120); + }); + } + } + + document.querySelectorAll('a[href^="#"]:not([href="#"])').forEach(function (link) { + link.addEventListener("click", function (event) { + const id = link.getAttribute("href").slice(1); + const target = document.getElementById(id); + + if (!target) { + return; + } + + event.preventDefault(); + + // The header becomes fixed after the page is scrolled, so + // scrollIntoView() cannot scroll the document when targeting it. + if (id === "masthead") { + window.scrollTo({ + top: 0, + behavior: reduceMotion.matches ? "auto" : "smooth", + }); + } else { + target.scrollIntoView({ + behavior: reduceMotion.matches ? "auto" : "smooth", + block: "start", + }); + } + + if (window.history && window.history.replaceState) { + window.history.replaceState(null, "", `#${id}`); + } + }); + }); + + const revealItems = Array.from(document.querySelectorAll("[data-reveal]")); + + if (revealItems.length && "IntersectionObserver" in window && !reduceMotion.matches) { + document.documentElement.classList.add("has-reveal"); + + const revealObserver = new IntersectionObserver( + function (entries, observer) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) { + return; + } + + const delay = Number.parseInt(entry.target.dataset.revealDelay || "0", 10); + if (delay > 0) { + entry.target.style.transitionDelay = `${Math.min(delay, 500)}ms`; + } + + entry.target.classList.add("is-visible"); + observer.unobserve(entry.target); + }); + }, + { rootMargin: "0px 0px -8%", threshold: 0.12 } + ); + + revealItems.forEach((item) => revealObserver.observe(item)); + } else { + revealItems.forEach((item) => item.classList.add("is-visible")); + } + + const progress = document.querySelector("[data-reading-progress]"); + const article = document.querySelector(".entry-content"); + + if (progress && article) { + const updateProgress = () => { + const articleTop = article.getBoundingClientRect().top + window.scrollY; + const articleHeight = article.offsetHeight; + const viewportHeight = window.innerHeight; + const distance = articleHeight - viewportHeight; + const current = window.scrollY - articleTop; + const percentage = distance > 0 ? Math.min(100, Math.max(0, (current / distance) * 100)) : 100; + progress.style.setProperty("--reading-progress", `${percentage}%`); + progress.setAttribute("aria-valuenow", String(Math.round(percentage))); + }; + + updateProgress(); + window.addEventListener("scroll", updateProgress, { passive: true }); + window.addEventListener("resize", updateProgress, { passive: true }); + } + }); +})(); diff --git a/wp-content/themes/gustavoo-portfolio/category.php b/wp-content/themes/gustavoo-portfolio/category.php new file mode 100644 index 0000000..f78354e --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/category.php @@ -0,0 +1,34 @@ + +
    +
    +
    +

    +
    +
    +
    +
    + +
    + 0 === $gustavoo_news_index ) ); + $gustavoo_news_index++; + } + ?> +
    + $gustavoo_displayed_post_ids, 'category_id' => get_queried_object_id() ) ); ?> + + + + +
    +
    + diff --git a/wp-content/themes/gustavoo-portfolio/comments.php b/wp-content/themes/gustavoo-portfolio/comments.php new file mode 100644 index 0000000..b164b39 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/comments.php @@ -0,0 +1,65 @@ + + +
    + +

    + +

    + +
      + 64, + 'short_ping' => true, + 'style' => 'ol', + ) + ); + ?> +
    + + sprintf( ' %s', esc_html__( 'Comentários anteriores', 'gustavoo-portfolio' ) ), + 'next_text' => sprintf( '%s ', esc_html__( 'Próximos comentários', 'gustavoo-portfolio' ) ), + ) + ); + ?> + + + +

    + + + 'submit button button--primary', + 'label_submit' => __( 'Publicar comentário', 'gustavoo-portfolio' ), + 'title_reply' => __( 'Deixe um comentário', 'gustavoo-portfolio' ), + 'title_reply_before' => '

    ', + 'title_reply_after' => '

    ', + 'comment_notes_before' => '

    ' . esc_html__( 'Seu endereço de e-mail não será publicado. Campos obrigatórios são indicados.', 'gustavoo-portfolio' ) . '

    ', + ) + ); + ?> +
    diff --git a/wp-content/themes/gustavoo-portfolio/footer.php b/wp-content/themes/gustavoo-portfolio/footer.php new file mode 100644 index 0000000..f43773f --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/footer.php @@ -0,0 +1,66 @@ + + +
    + + + +
    + + + + + + diff --git a/wp-content/themes/gustavoo-portfolio/front-page.php b/wp-content/themes/gustavoo-portfolio/front-page.php new file mode 100644 index 0000000..5fe608e --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/front-page.php @@ -0,0 +1,41 @@ + + +
    + + + + + + + +
    +
    + +
    +
    + + + + + +
    + + +> + + + + + +> + + + + +
    +
    +
    + + + + + + +
    + + + + + +
    +
    + + + +
    +

    + +
    + +
    + 0 === $gustavoo_news_index ) ); + $gustavoo_news_index++; + } + ?> +
    + $gustavoo_displayed_post_ids ) ); ?> + + + + +
    +
    + + Customize. */ +function gustavoo_portfolio_customize_register( $wp_customize ) { + $wp_customize->add_panel( + 'gustavoo_portfolio_content', + array( + 'title' => __( 'Conteúdo do tema', 'gustavoo-portfolio' ), + 'description' => __( 'Edite os conteúdos exibidos no site e use os lápis na prévia para ir direto à seção desejada.', 'gustavoo-portfolio' ), + 'priority' => 30, + ) + ); + + $sections = array( + 'hero' => __( 'Hero', 'gustavoo-portfolio' ), + 'about' => __( 'Sobre', 'gustavoo-portfolio' ), + 'services' => __( 'Serviços', 'gustavoo-portfolio' ), + 'projects' => __( 'Projetos', 'gustavoo-portfolio' ), + 'blog' => __( 'Blog', 'gustavoo-portfolio' ), + 'contact' => __( 'Contato', 'gustavoo-portfolio' ), + 'newsletter' => __( 'Newsletter', 'gustavoo-portfolio' ), + 'social' => __( 'Redes sociais', 'gustavoo-portfolio' ), + ); + + foreach ( $sections as $id => $title ) { + $wp_customize->add_section( + 'gustavoo_portfolio_' . $id, + array( + 'title' => $title, + 'panel' => 'gustavoo_portfolio_content', + 'priority' => 10, + ) + ); + } + + $fields = array( + 'hero_eyebrow' => array( 'hero', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'hero_title' => array( 'hero', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'hero_description' => array( 'hero', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ), + 'hero_primary_label' => array( 'hero', __( 'Botão principal — texto', 'gustavoo-portfolio' ), 'text' ), + 'hero_primary_url' => array( 'hero', __( 'Botão principal — link', 'gustavoo-portfolio' ), 'link' ), + 'hero_secondary_label' => array( 'hero', __( 'Botão secundário — texto', 'gustavoo-portfolio' ), 'text' ), + 'hero_secondary_url' => array( 'hero', __( 'Botão secundário — link', 'gustavoo-portfolio' ), 'link' ), + 'about_eyebrow' => array( 'about', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'about_title' => array( 'about', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'about_text' => array( 'about', __( 'Texto principal', 'gustavoo-portfolio' ), 'textarea' ), + 'about_secondary_text' => array( 'about', __( 'Texto complementar', 'gustavoo-portfolio' ), 'textarea' ), + 'services_eyebrow' => array( 'services', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'services_title' => array( 'services', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'services_description' => array( 'services', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ), + 'projects_eyebrow' => array( 'projects', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'projects_title' => array( 'projects', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'projects_text' => array( 'projects', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ), + 'projects_limit' => array( 'projects', __( 'Quantidade de projetos', 'gustavoo-portfolio' ), 'number' ), + 'blog_eyebrow' => array( 'blog', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'blog_title' => array( 'blog', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'blog_text' => array( 'blog', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ), + 'blog_limit' => array( 'blog', __( 'Quantidade de posts', 'gustavoo-portfolio' ), 'number' ), + 'contact_eyebrow' => array( 'contact', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'contact_heading' => array( 'contact', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'contact_text' => array( 'contact', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ), + 'email' => array( 'contact', __( 'E-mail', 'gustavoo-portfolio' ), 'email' ), + 'whatsapp' => array( 'contact', __( 'WhatsApp — número', 'gustavoo-portfolio' ), 'text' ), + 'whatsapp_message' => array( 'contact', __( 'WhatsApp — mensagem inicial', 'gustavoo-portfolio' ), 'textarea' ), + 'newsletter_eyebrow' => array( 'newsletter', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ), + 'newsletter_heading' => array( 'newsletter', __( 'Título', 'gustavoo-portfolio' ), 'text' ), + 'newsletter_text' => array( 'newsletter', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ), + 'github_url' => array( 'social', __( 'GitHub', 'gustavoo-portfolio' ), 'url' ), + 'linkedin_url' => array( 'social', __( 'LinkedIn', 'gustavoo-portfolio' ), 'url' ), + 'instagram_url' => array( 'social', __( 'Instagram', 'gustavoo-portfolio' ), 'url' ), + 'availability_label' => array( 'social', __( 'Status de disponibilidade', 'gustavoo-portfolio' ), 'text' ), + ); + + foreach ( $fields as $key => $field ) { + $setting_id = 'gustavoo_portfolio_' . $key; + $sanitize = 'textarea' === $field[2] ? 'sanitize_textarea_field' : ( 'number' === $field[2] ? 'absint' : ( in_array( $field[2], array( 'url', 'link' ), true ) ? 'gustavoo_portfolio_customize_link' : ( 'email' === $field[2] ? 'sanitize_email' : 'sanitize_text_field' ) ) ); + + $wp_customize->add_setting( + $setting_id, + array( + 'default' => gustavoo_portfolio_get_setting( $key ), + 'sanitize_callback' => $sanitize, + // Each front-page section is refreshed by its native Customizer + // partial, allowing WordPress to expose its own edit shortcut. + 'transport' => 'postMessage', + ) + ); + + $control_args = array( + 'label' => $field[1], + 'section' => 'gustavoo_portfolio_' . $field[0], + 'type' => in_array( $field[2], array( 'textarea', 'number', 'email', 'url' ), true ) ? $field[2] : 'text', + ); + + if ( 'number' === $field[2] ) { + $control_args['input_attrs'] = array( 'min' => 1, 'max' => 24 ); + } + + $wp_customize->add_control( $setting_id, $control_args ); + } + + if ( ! isset( $wp_customize->selective_refresh ) ) { + return; + } + + $partials = array( + 'hero' => array( '.hero-stage', 'template-parts/front/hero', array( 'hero_eyebrow', 'hero_title', 'hero_description', 'hero_primary_label', 'hero_primary_url', 'hero_secondary_label', 'hero_secondary_url' ) ), + 'about' => array( '#sobre', 'template-parts/front/about', array( 'about_eyebrow', 'about_title', 'about_text', 'about_secondary_text' ) ), + 'services' => array( '#servicos', 'template-parts/front/services', array( 'services_eyebrow', 'services_title', 'services_description' ) ), + 'projects' => array( '#projetos', 'template-parts/front/projects', array( 'projects_eyebrow', 'projects_title', 'projects_text', 'projects_limit' ) ), + 'blog' => array( '#blog', 'template-parts/front/posts', array( 'blog_eyebrow', 'blog_title', 'blog_text', 'blog_limit' ) ), + 'contact' => array( '#contato', 'template-parts/front/contact', array( 'contact_eyebrow', 'contact_heading', 'contact_text', 'email', 'whatsapp', 'whatsapp_message', 'github_url', 'linkedin_url', 'instagram_url' ) ), + ); + + foreach ( $partials as $id => $partial ) { + $settings = array_map( + static function ( $key ) { + return 'gustavoo_portfolio_' . $key; + }, + $partial[2] + ); + + /* + * A native edit shortcut is tied to the partial's primary setting. + * Using the first actual setting as the partial ID makes the pencil + * focus a real control instead of an unresolvable synthetic partial. + */ + $wp_customize->selective_refresh->add_partial( + $settings[0], + array( + 'selector' => $partial[0], + 'settings' => $settings, + 'primary_setting' => $settings[0], + 'container_inclusive' => true, + 'render_callback' => static function () use ( $partial ) { + return gustavoo_portfolio_customize_render_part( $partial[1] ); + }, + ) + ); + } + + $wp_customize->selective_refresh->add_partial( + 'gustavoo_portfolio_whatsapp_shortcut', + array( + 'selector' => '.whatsapp-float-wrap', + 'settings' => array( 'gustavoo_portfolio_whatsapp', 'gustavoo_portfolio_whatsapp_message' ), + 'primary_setting' => 'gustavoo_portfolio_whatsapp', + 'container_inclusive' => true, + 'render_callback' => static function () { + return gustavoo_portfolio_customize_render_part( 'template-parts/global/whatsapp-float' ); + }, + ) + ); +} +add_action( 'customize_register', 'gustavoo_portfolio_customize_register' ); + +/** Register native Customizer shortcuts for the theme's menu locations. */ +function gustavoo_portfolio_customize_menu_shortcuts( $wp_customize ) { + if ( ! isset( $wp_customize->selective_refresh ) ) { + return; + } + + $menus = array( + 'primary' => '#site-navigation', + 'news_categories' => '.news-category-nav', + ); + + foreach ( $menus as $location => $selector ) { + $setting = 'nav_menu_locations[' . $location . ']'; + + if ( ! $wp_customize->get_setting( $setting ) ) { + continue; + } + + $wp_customize->selective_refresh->add_partial( + 'gustavoo_portfolio_menu_shortcut_' . $location, + array( + 'selector' => $selector, + 'settings' => array( $setting ), + 'primary_setting' => $setting, + ) + ); + } +} +add_action( 'customize_register', 'gustavoo_portfolio_customize_menu_shortcuts', 20 ); diff --git a/wp-content/themes/gustavoo-portfolio/inc/defaults.php b/wp-content/themes/gustavoo-portfolio/inc/defaults.php new file mode 100644 index 0000000..a6b7035 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/inc/defaults.php @@ -0,0 +1,173 @@ + + */ +function gustavoo_portfolio_get_defaults() { + $defaults = array( + 'hero_eyebrow' => __( 'Desenvolvimento de ponta a ponta', 'gustavoo-portfolio' ), + 'hero_title' => __( 'Transformo ideias em produtos digitais sólidos.', 'gustavoo-portfolio' ), + 'hero_description' => __( 'Gustavo, Desenvolvedor Full Stack Sênior. Web, mobile, infraestrutura e automação para tirar projetos do papel e fazê-los crescer.', 'gustavoo-portfolio' ), + 'hero_primary_label' => __( 'Ver projetos', 'gustavoo-portfolio' ), + 'hero_primary_url' => '#projetos', + 'hero_secondary_label' => __( 'Fale comigo', 'gustavoo-portfolio' ), + 'hero_secondary_url' => '#contato', + 'about_eyebrow' => __( 'Sobre mim', 'gustavoo-portfolio' ), + 'about_title' => __( 'Responsabilidade técnica do planejamento à produção.', 'gustavoo-portfolio' ), + 'about_text' => __( 'Mais do que escrever código, meu objetivo é garantir que seu projeto saia do papel e funcione exatamente como planejado. Assumo a responsabilidade técnica de ponta a ponta para que você foque no que importa: crescer.', 'gustavoo-portfolio' ), + 'about_secondary_text' => __( 'Já participei do desenvolvimento e lançamento de múltiplos projetos digitais, atuando da arquitetura técnica à divulgação e ao crescimento das plataformas. Essa combinação de visão técnica e entendimento de mercado orienta cada entrega.', 'gustavoo-portfolio' ), + 'services_eyebrow' => __( 'Como posso ajudar', 'gustavoo-portfolio' ), + 'services_title' => __( 'Soluções completas para produtos digitais.', 'gustavoo-portfolio' ), + 'services_description' => __( 'Estratégia, código e infraestrutura trabalhando juntos para criar experiências rápidas, seguras e preparadas para evoluir.', 'gustavoo-portfolio' ), + 'projects_eyebrow' => __( 'Portfólio', 'gustavoo-portfolio' ), + 'projects_title' => __( 'Projetos selecionados', 'gustavoo-portfolio' ), + 'projects_text' => __( 'Uma seleção de produtos, experiências e soluções que ajudei a colocar no mundo.', 'gustavoo-portfolio' ), + 'projects_limit' => 6, + 'blog_eyebrow' => __( 'Blog', 'gustavoo-portfolio' ), + 'blog_title' => __( 'Código, produto e bastidores', 'gustavoo-portfolio' ), + 'blog_text' => __( 'Análises práticas sobre desenvolvimento, infraestrutura, automação e crescimento de produtos digitais.', 'gustavoo-portfolio' ), + 'blog_limit' => 4, + 'contact_eyebrow' => __( 'Vamos conversar', 'gustavoo-portfolio' ), + 'contact_heading' => __( 'Tem um projeto em mente?', 'gustavoo-portfolio' ), + 'contact_text' => __( 'Conte o que você precisa construir ou melhorar. Responderei com os próximos passos para transformar a ideia em uma entrega concreta.', 'gustavoo-portfolio' ), + 'contact_form_id' => 0, + 'newsletter_eyebrow' => __( 'Newsletter', 'gustavoo-portfolio' ), + 'newsletter_heading' => __( 'Ideias úteis, direto na sua caixa de entrada.', 'gustavoo-portfolio' ), + 'newsletter_text' => __( 'Receba conteúdos sobre desenvolvimento, produto, infraestrutura e automação. Sem ruído e sem spam.', 'gustavoo-portfolio' ), + 'newsletter_form_id' => 0, + 'email' => (string) get_option( 'admin_email', '' ), + 'whatsapp' => '', + 'whatsapp_message' => __( 'Olá! Gostaria de falar sobre um projeto.', 'gustavoo-portfolio' ), + 'github_url' => '', + 'linkedin_url' => '', + 'instagram_url' => '', + 'projects_archive_url' => '', + 'availability_label' => __( 'Disponível para novos projetos', 'gustavoo-portfolio' ), + ); + + /** + * Filter theme defaults before saved settings are merged. + * + * @param array $defaults Default settings. + */ + return apply_filters( 'gustavoo_portfolio_defaults', $defaults ); +} + +/** + * Return the saved theme settings merged with safe local defaults. + * + * @return array + */ +function gustavoo_portfolio_get_settings() { + $settings = get_option( 'gustavoo_portfolio_settings', array() ); + + if ( ! is_array( $settings ) ) { + $settings = array(); + } + + return wp_parse_args( $settings, gustavoo_portfolio_get_defaults() ); +} + +/** + * Read one theme setting. A few aliases preserve compatibility with early builds. + * + * @param string $key Setting key. + * @param mixed $fallback Optional fallback. + * @return mixed + */ +function gustavoo_portfolio_get_setting( $key, $fallback = null ) { + $customizer_key = 'gustavoo_portfolio_' . $key; + $customizer_values = get_theme_mods(); + + if ( is_array( $customizer_values ) && array_key_exists( $customizer_key, $customizer_values ) ) { + return $customizer_values[ $customizer_key ]; + } + + $settings = gustavoo_portfolio_get_settings(); + $aliases = array( + 'hero_eyebrow' => array( 'hero_kicker' ), + 'hero_description' => array( 'hero_text' ), + 'contact_form_id' => array( 'fluent_contact_form_id', 'lead_form_id' ), + 'newsletter_form_id' => array( 'fluent_newsletter_form_id', 'fluentcrm_form_id' ), + 'contact_heading' => array( 'contact_title' ), + 'newsletter_heading' => array( 'newsletter_title' ), + ); + + if ( array_key_exists( $key, $settings ) && '' !== $settings[ $key ] && null !== $settings[ $key ] ) { + return $settings[ $key ]; + } + + if ( isset( $aliases[ $key ] ) ) { + foreach ( $aliases[ $key ] as $alias ) { + if ( array_key_exists( $alias, $settings ) && '' !== $settings[ $alias ] && null !== $settings[ $alias ] ) { + return $settings[ $alias ]; + } + } + } + + if ( null !== $fallback ) { + return $fallback; + } + + $defaults = gustavoo_portfolio_get_defaults(); + + return $defaults[ $key ] ?? ''; +} + +/** + * Return the service cards used by the front page. + * + * @return array> + */ +function gustavoo_portfolio_get_services() { + $services = array( + array( + 'number' => '01', + 'title' => __( 'Web, Mobile e E-commerce', 'gustavoo-portfolio' ), + 'description' => __( 'Sites institucionais, aplicações para iOS e Android, softwares desktop, lojas e marketplaces personalizados com foco em experiência e conversão.', 'gustavoo-portfolio' ), + ), + array( + 'number' => '02', + 'title' => __( 'WordPress avançado', 'gustavoo-portfolio' ), + 'description' => __( 'Temas e plugins sob medida, integrações específicas, recuperação de sites e otimização para Core Web Vitals.', 'gustavoo-portfolio' ), + ), + array( + 'number' => '03', + 'title' => __( 'Infraestrutura e servidores', 'gustavoo-portfolio' ), + 'description' => __( 'Ambientes Linux, VPS e servidores dedicados configurados para manter aplicações estáveis, rápidas, seguras e escaláveis.', 'gustavoo-portfolio' ), + ), + array( + 'number' => '04', + 'title' => __( 'Automação e integrações', 'gustavoo-portfolio' ), + 'description' => __( 'APIs e fluxos com n8n para conectar sistemas, organizar dados e eliminar tarefas operacionais repetitivas.', 'gustavoo-portfolio' ), + ), + array( + 'number' => '05', + 'title' => __( 'Games e experiências interativas', 'gustavoo-portfolio' ), + 'description' => __( 'Jogos 2D e 3D para celulares, computadores e navegadores, incluindo ranking, multiplayer e compras dentro do jogo.', 'gustavoo-portfolio' ), + ), + array( + 'number' => '06', + 'title' => __( 'Divulgação e crescimento', 'gustavoo-portfolio' ), + 'description' => __( 'Estratégias de conteúdo, comunidades, parcerias e mídia paga para aproximar produtos digitais do público certo.', 'gustavoo-portfolio' ), + ), + ); + + /** + * Filter the service cards shown on the front page. + * + * @param array> $services Service cards. + */ + return apply_filters( 'gustavoo_portfolio_services', $services ); +} diff --git a/wp-content/themes/gustavoo-portfolio/inc/enqueue.php b/wp-content/themes/gustavoo-portfolio/inc/enqueue.php new file mode 100644 index 0000000..1efa447 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/inc/enqueue.php @@ -0,0 +1,75 @@ + __( 'Abrir menu', 'gustavoo-portfolio' ), + 'collapse' => __( 'Fechar menu', 'gustavoo-portfolio' ), + ) + ); + } + + if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) { + wp_enqueue_script( 'comment-reply' ); + } +} +add_action( 'wp_enqueue_scripts', 'gustavoo_portfolio_enqueue_assets' ); diff --git a/wp-content/themes/gustavoo-portfolio/inc/forms.php b/wp-content/themes/gustavoo-portfolio/inc/forms.php new file mode 100644 index 0000000..4c45a35 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/inc/forms.php @@ -0,0 +1,247 @@ +form_fields, true ); + + if ( ! is_array( $structure ) || ! isset( $structure['fields'] ) || ! is_array( $structure['fields'] ) ) { + return; + } + + $defaults = fluentformLoadFile( 'Services/FormBuilder/DefaultElements.php' ); + $message = $defaults['general']['textarea'] ?? array(); + + if ( ! $message ) { + return; + } + + $fields_by_name = array(); + $changed = false; + + foreach ( $structure['fields'] as $field ) { + $name = (string) ( $field['attributes']['name'] ?? '' ); + + if ( ! in_array( $name, array( 'email', 'whatsapp', 'message' ), true ) ) { + $changed = true; + continue; + } + + if ( isset( $fields_by_name[ $name ] ) ) { + $changed = true; + continue; + } + + $fields_by_name[ $name ] = $field; + } + + if ( isset( $fields_by_name['email'] ) ) { + $fields_by_name['email']['settings']['validation_rules']['required']['value'] = true; + } + + if ( isset( $fields_by_name['whatsapp'] ) ) { + $whatsapp = $fields_by_name['whatsapp']; + $whatsapp['settings']['validation_rules']['required']['value'] = true; + $whatsapp['settings']['temp_mask'] = 'custom'; + $whatsapp['settings']['data-mask-reverse'] = 'no'; + $whatsapp['attributes']['data-mask'] = '(00) 00000-0000'; + $fields_by_name['whatsapp'] = $whatsapp; + } + + if ( ! isset( $fields_by_name['whatsapp'] ) ) { + $whatsapp = $defaults['general']['input_mask'] ?? array(); + if ( $whatsapp ) { + $whatsapp['uniqElKey'] = 'el_gso_contact_whatsapp'; + $whatsapp['attributes']['name'] = 'whatsapp'; + $whatsapp['attributes']['placeholder'] = '(31) 99999-9999'; + $whatsapp['attributes']['data-mask'] = '(00) 00000-0000'; + $whatsapp['settings']['label'] = __( 'WhatsApp', 'gustavoo-portfolio' ); + $whatsapp['settings']['admin_field_label'] = __( 'WhatsApp', 'gustavoo-portfolio' ); + $whatsapp['settings']['mobile_keyboard_type'] = 'tel'; + $whatsapp['settings']['temp_mask'] = 'custom'; + $whatsapp['settings']['data-mask-reverse'] = 'no'; + $whatsapp['settings']['validation_rules']['required']['value'] = true; + $fields_by_name['whatsapp'] = $whatsapp; + } + } + + if ( ! isset( $fields_by_name['message'] ) || 'textarea' !== (string) ( $fields_by_name['message']['element'] ?? '' ) ) { + $message['uniqElKey'] = 'el_gso_contact_message'; + $message['attributes']['name'] = 'message'; + $message['attributes']['placeholder'] = __( 'Descreva o motivo do contato e o que você quer construir.', 'gustavoo-portfolio' ); + $message['attributes']['rows'] = 5; + $message['settings']['label'] = __( 'Como posso ajudar?', 'gustavoo-portfolio' ); + $message['settings']['admin_field_label'] = __( 'Motivo do contato', 'gustavoo-portfolio' ); + $message['settings']['validation_rules']['required']['value'] = true; + $fields_by_name['message'] = $message; + } + + $fields = array(); + foreach ( array( 'email', 'whatsapp', 'message' ) as $required_name ) { + if ( isset( $fields_by_name[ $required_name ] ) ) { + $fields[] = $fields_by_name[ $required_name ]; + } + } + $changed = wp_json_encode( $structure['fields'] ) !== wp_json_encode( $fields ); + + if ( ! $changed ) { + return; + } + + $structure['fields'] = $fields; + $form_fields = wp_json_encode( $structure, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); + + if ( class_exists( '\FluentForm\App\Services\Form\FormService' ) ) { + ( new \FluentForm\App\Services\Form\FormService() )->update( + array( + 'form_id' => $form->id, + 'title' => $form->title, + 'status' => $form->status, + 'formFields' => $form_fields, + ) + ); + } else { + $form->form_fields = $form_fields; + $form->save(); + } +} +add_action( 'init', 'gustavoo_portfolio_ensure_contact_form_fields', 30 ); + +/** + * Preserve contact fields in submissions while an older form schema is being + * migrated. Fluent Forms only whitelists fields known by its saved schema. + * + * @param array $form_data Sanitized submission data. + * @param int $form_id Submitted form ID. + * @param array $input_configs Fluent Forms input configuration. + * @return array + */ +function gustavoo_portfolio_preserve_contact_submission_fields( $form_data, $form_id, $input_configs ) { + $contact_form_id = absint( gustavoo_portfolio_get_setting( 'contact_form_id' ) ); + + if ( ! $contact_form_id || $contact_form_id !== absint( $form_id ) ) { + return $form_data; + } + + if ( isset( $_POST['whatsapp'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Fluent Forms validates the submission. + $form_data['whatsapp'] = sanitize_text_field( wp_unslash( $_POST['whatsapp'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + + if ( isset( $_POST['message'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Fluent Forms validates the submission. + $form_data['message'] = sanitize_textarea_field( wp_unslash( $_POST['message'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + + return $form_data; +} +add_filter( 'fluentform/insert_response_data', 'gustavoo_portfolio_preserve_contact_submission_fields', 10, 3 ); + +/** + * Render a Fluent Forms form from a numeric, administrator-controlled ID. + * + * The theme deliberately never stores or executes arbitrary shortcodes. Contact + * synchronization is configured through the FluentCRM feed inside Fluent Forms. + * + * @param int $form_id Form ID. + * @param string $context Form context, used for classes and filters. + * @return string + */ +function gustavoo_portfolio_render_fluent_form( $form_id, $context = 'default' ) { + $form_id = absint( $form_id ); + $context = sanitize_html_class( $context ); + + if ( $form_id && shortcode_exists( 'fluentform' ) ) { + if ( 'contact' === $context ) { + gustavoo_portfolio_ensure_contact_form_fields(); + } + + $shortcode = sprintf( '[fluentform id="%d"]', $form_id ); + $markup = do_shortcode( $shortcode ); + + // Older persisted forms may still contain only the e-mail field. Keep the + // requested fields visible inside the real Fluent Forms
    . + if ( 'contact' === $context && false !== stripos( $markup, '
    ' ) ) { + $missing_fields = ''; + + if ( false === stripos( $markup, 'name="whatsapp"' ) ) { + $missing_fields .= '
    '; + } + + if ( false === stripos( $markup, 'name="message"' ) ) { + $missing_fields .= '
    '; + } + + if ( $missing_fields ) { + $markup = preg_replace( '#'; + $fallback .= '

    ' . esc_html__( 'O formulário está sendo configurado. Você ainda pode entrar em contato diretamente.', 'gustavoo-portfolio' ) . '

    '; + $fallback .= '' . esc_html( $button_label ) . ''; + + if ( current_user_can( 'manage_options' ) ) { + $fallback .= '

    '; + $fallback .= esc_html__( 'Configure o ID do Fluent Forms nas opções do portfólio.', 'gustavoo-portfolio' ); + $fallback .= '

    '; + } + + $fallback .= ''; + + return $fallback; +} + +/** + * Print a Fluent Forms form. Output originates either from the trusted plugin + * shortcode or from fully escaped fallback markup built above. + * + * @param int $form_id Form ID. + * @param string $context Form context. + * @return void + */ +function gustavoo_portfolio_the_fluent_form( $form_id, $context = 'default' ) { + echo gustavoo_portfolio_render_fluent_form( $form_id, $context ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Trusted plugin output or escaped local fallback. +} diff --git a/wp-content/themes/gustavoo-portfolio/inc/setup.php b/wp-content/themes/gustavoo-portfolio/inc/setup.php new file mode 100644 index 0000000..9df4a5a --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/inc/setup.php @@ -0,0 +1,146 @@ + 160, + 'width' => 160, + 'flex-height' => true, + 'flex-width' => true, + ) + ); + + register_nav_menus( + array( + 'primary' => __( 'Menu principal', 'gustavoo-portfolio' ), + 'news_categories' => __( 'Menu de categorias de notícias', 'gustavoo-portfolio' ), + 'footer' => __( 'Menu do rodapé', 'gustavoo-portfolio' ), + ) + ); + + add_image_size( 'gustavoo-project-card', 720, 480, true ); + add_image_size( 'gustavoo-post-card', 720, 460, true ); +} +add_action( 'after_setup_theme', 'gustavoo_portfolio_setup' ); + +/** + * Define a comfortable content width for embeds and media. + * + * @return void + */ +function gustavoo_portfolio_content_width() { + $GLOBALS['content_width'] = apply_filters( 'gustavoo_portfolio_content_width', 780 ); +} +add_action( 'after_setup_theme', 'gustavoo_portfolio_content_width', 0 ); + +/** + * Add useful state classes without coupling templates to presentation logic. + * + * @param string[] $classes Existing body classes. + * @return string[] + */ +function gustavoo_portfolio_body_classes( $classes ) { + if ( is_front_page() ) { + $classes[] = 'has-overlay-header'; + } + + if ( ! is_front_page() && is_active_sidebar( 'sidebar-blog' ) ) { + $classes[] = 'has-blog-sidebar'; + } + + if ( ! is_singular() ) { + $classes[] = 'is-list-view'; + } + + return array_unique( $classes ); +} +add_filter( 'body_class', 'gustavoo_portfolio_body_classes' ); + +/** Set the posts page browser title to the editorial section name. */ +function gustavoo_portfolio_blog_document_title( $title ) { + if ( is_home() ) { + $title['title'] = __( 'Últimas notícias', 'gustavoo-portfolio' ); + } + + return $title; +} +add_filter( 'document_title_parts', 'gustavoo_portfolio_blog_document_title' ); + +/** Keep the editorial top grid focused on the five newest posts. */ +function gustavoo_portfolio_news_posts_per_page( $query ) { + if ( is_admin() || ! $query->is_main_query() || ! ( $query->is_home() || $query->is_category() ) ) { + return; + } + + $query->set( 'posts_per_page', 5 ); +} +add_action( 'pre_get_posts', 'gustavoo_portfolio_news_posts_per_page' ); + +/** Refresh category rewrite rules once after the editorial templates are installed. */ +function gustavoo_portfolio_refresh_editorial_rewrites() { + $version = '1.0.0'; + + if ( $version === get_option( 'gustavoo_portfolio_editorial_rewrite_version' ) ) { + return; + } + + flush_rewrite_rules( false ); + update_option( 'gustavoo_portfolio_editorial_rewrite_version', $version, false ); +} +add_action( 'init', 'gustavoo_portfolio_refresh_editorial_rewrites', 99 ); + +/** + * Print a fallback icon only when WordPress has no configured Site Icon. + * + * @return void + */ +function gustavoo_portfolio_fallback_site_icon() { + if ( has_site_icon() || ! function_exists( 'gustavoo_portfolio_get_image_source' ) ) { + return; + } + + $icon_url = gustavoo_portfolio_get_image_source( 'logo-mark', 'png' ); + + if ( $icon_url ) { + printf( '' . "\n", esc_url( $icon_url ) ); + } +} +add_action( 'wp_head', 'gustavoo_portfolio_fallback_site_icon', 2 ); diff --git a/wp-content/themes/gustavoo-portfolio/inc/template-tags.php b/wp-content/themes/gustavoo-portfolio/inc/template-tags.php new file mode 100644 index 0000000..745a413 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/inc/template-tags.php @@ -0,0 +1,569 @@ + $has_webp ? get_theme_file_uri( $webp_path ) : '', + 'png' => $has_png ? get_theme_file_uri( $png_path ) : '', + 'fallback' => $has_png ? get_theme_file_uri( $png_path ) : get_theme_file_uri( $webp_path ), + ); +} + +/** + * Print the configured logo or the bundled brand mark. + * + * @return void + */ +function gustavoo_portfolio_the_brand() { + $site_name = get_bloginfo( 'name' ); + + if ( has_custom_logo() ) { + echo get_custom_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Core-generated custom logo markup. + return; + } + + $sources = gustavoo_portfolio_get_picture_sources( 'logo-mark' ); + ?> + + + + + + + + + + + true, + 'exclude' => array( absint( get_option( 'default_category' ) ) ), + 'orderby' => 'name', + 'order' => 'ASC', + ) + ); + + if ( empty( $categories ) ) { + return; + } + + $current_category_id = is_category() ? (int) get_queried_object_id() : 0; + ?> + + +
    +

    + 'news_categories', + 'menu_class' => 'mobile-news-categories__list', + 'container' => false, + 'fallback_cb' => false, + 'depth' => 1, + ) + ); + ?> +
    + true, + 'exclude' => array( absint( get_option( 'default_category' ) ) ), + 'orderby' => 'name', + 'order' => 'ASC', + ) + ); + + if ( empty( $categories ) ) { + return; + } + + $current_category_id = is_category() ? (int) get_queried_object_id() : 0; + ?> +
    +

    + +
    + +
      + + + + + +
    + + */ +function gustavoo_portfolio_get_social_links() { + $profiles = array( + 'linkedin' => array( + 'label' => __( 'LinkedIn', 'gustavoo-portfolio' ), + 'url' => (string) gustavoo_portfolio_get_setting( 'linkedin_url' ), + ), + 'instagram' => array( + 'label' => __( 'Instagram', 'gustavoo-portfolio' ), + 'url' => (string) gustavoo_portfolio_get_setting( 'instagram_url' ), + ), + ); + + return array_filter( + $profiles, + static function ( $profile ) { + return ! empty( $profile['url'] ); + } + ); +} + +/** + * Print social links with explicit new-window text for assistive technology. + * + * @param string $class Optional list class. + * @return void + */ +function gustavoo_portfolio_social_links( $class = 'social-links' ) { + $profiles = gustavoo_portfolio_get_social_links(); + + if ( empty( $profiles ) ) { + return; + } + + printf( '
      ', esc_attr( $class ) ); + + foreach ( $profiles as $slug => $profile ) { + printf( + '', + esc_attr( $slug ), + esc_url( $profile['url'] ), + esc_html( $profile['label'] ), + esc_html__( '(abre em uma nova guia)', 'gustavoo-portfolio' ) + ); + } + + echo '
    '; +} + +/** + * Print share links for a blog post using inline brand SVG icons. + * + * @param string $class Optional wrapper class. + * @return void + */ +function gustavoo_portfolio_post_share_links( $class = 'entry-share' ) { + $share_url = rawurlencode( get_permalink() ); + $share_title = rawurlencode( get_the_title() ); + $links = array( + 'WhatsApp' => array( + 'url' => 'https://api.whatsapp.com/send?text=' . $share_title . '%20' . $share_url, + 'icon' => '', + ), + 'Facebook' => array( + 'url' => 'https://www.facebook.com/sharer/sharer.php?u=' . $share_url, + 'icon' => '', + ), + 'X' => array( + 'url' => 'https://twitter.com/intent/tweet?text=' . $share_title . '&url=' . $share_url, + 'icon' => '', + ), + 'LinkedIn' => array( + 'url' => 'https://www.linkedin.com/sharing/share-offsite/?url=' . $share_url, + 'icon' => '', + ), + ); + + printf( '
    ', esc_attr( $class ), esc_attr__( 'Compartilhar postagem', 'gustavoo-portfolio' ) ); + + foreach ( $links as $label => $link ) { + printf( + '%3$s%2$s', + esc_url( $link['url'] ), + esc_attr( $label ), + $link['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static SVG markup. + ); + } + + echo '
    '; +} + +/** + * Convert a configured phone number to a WhatsApp URL. + * + * @param string $phone Raw phone number. + * @param string $message Optional pre-filled conversation message. + * @return string + */ +function gustavoo_portfolio_get_whatsapp_url( $phone, $message = '' ) { + $digits = preg_replace( '/\D+/', '', (string) $phone ); + + if ( ! $digits ) { + return ''; + } + + if ( in_array( strlen( $digits ), array( 10, 11 ), true ) ) { + $digits = '55' . $digits; + } + + $url = 'https://wa.me/' . $digits; + $message = trim( wp_strip_all_tags( (string) $message ) ); + + if ( '' !== $message ) { + $url .= '?text=' . rawurlencode( $message ); + } + + return $url; +} + +/** Print the configured floating WhatsApp contact link. */ +function gustavoo_portfolio_the_whatsapp_float() { + $phone = (string) gustavoo_portfolio_get_setting( 'whatsapp' ); + $message = (string) gustavoo_portfolio_get_setting( 'whatsapp_message' ); + $url = gustavoo_portfolio_get_whatsapp_url( $phone, $message ); + + if ( ! $url ) { + return; + } + ?> +
    > + + + +
    + 'gso_project', + 'post_status' => 'publish', + 'posts_per_page' => $limit * 3, + 'orderby' => array( + 'menu_order' => 'ASC', + 'date' => 'DESC', + ), + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'update_post_term_cache' => true, + ); + + $featured_args = $base_args; + $featured_args['meta_query'] = array( + array( + 'key' => '_gso_project_featured', + 'value' => array( '1', 'yes', 'on', 'true' ), + 'compare' => 'IN', + ), + ); + + $featured = get_posts( $featured_args ); + $others = get_posts( $base_args ); + $projects = array(); + $seen = array(); + + foreach ( array_merge( $featured, $others ) as $project ) { + if ( isset( $seen[ $project->ID ] ) ) { + continue; + } + + $url = gustavoo_portfolio_validate_project_url( get_post_meta( $project->ID, '_gso_project_url', true ) ); + + if ( ! $url ) { + continue; + } + + $seen[ $project->ID ] = true; + $projects[] = $project; + + if ( count( $projects ) >= $limit ) { + break; + } + } + + return $projects; +} + +/** + * Print post publication metadata. + * + * @return void + */ +function gustavoo_portfolio_posted_on() { + $published = sprintf( + '', + esc_attr( get_the_date( DATE_W3C ) ), + esc_html( get_the_date() ) + ); + + printf( + '%1$s %2$s', + esc_html__( 'Publicado em', 'gustavoo-portfolio' ), + $published // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped immediately above. + ); +} + +/** + * Print the post author link. + * + * @return void + */ +function gustavoo_portfolio_posted_by() { + printf( + '', + esc_html__( 'Por', 'gustavoo-portfolio' ), + esc_url( get_author_posts_url( (int) get_the_author_meta( 'ID' ) ) ), + esc_html( get_the_author() ) + ); +} + +/** + * Estimate reading time from the current post content. + * + * @param int|null $post_id Optional post ID. + * @return string + */ +function gustavoo_portfolio_reading_time( $post_id = null ) { + $post_id = $post_id ? absint( $post_id ) : get_the_ID(); + $content = wp_strip_all_tags( strip_shortcodes( (string) get_post_field( 'post_content', $post_id ) ) ); + $words = preg_split( '/\s+/u', trim( $content ), -1, PREG_SPLIT_NO_EMPTY ); + $minutes = max( 1, (int) ceil( count( is_array( $words ) ? $words : array() ) / 220 ) ); + + return sprintf( + /* translators: %d: reading time in minutes. */ + _n( '%d min de leitura', '%d min de leitura', $minutes, 'gustavoo-portfolio' ), + $minutes + ); +} + +/** + * Print category and tag links for a post. + * + * @return void + */ +function gustavoo_portfolio_entry_terms() { + $categories = get_the_category_list( esc_html_x( ', ', 'category list separator', 'gustavoo-portfolio' ) ); + $tags = get_the_tag_list( '', esc_html_x( ', ', 'tag list separator', 'gustavoo-portfolio' ) ); + + if ( $categories ) { + printf( + '
    %1$s %2$s
    ', + esc_html__( 'Categorias:', 'gustavoo-portfolio' ), + wp_kses_post( $categories ) + ); + } + + if ( $tags ) { + printf( + '
    %1$s %2$s
    ', + esc_html__( 'Tags:', 'gustavoo-portfolio' ), + wp_kses_post( $tags ) + ); + } +} + +/** + * Print main-query pagination with accessible labels. + * + * @return void + */ +function gustavoo_portfolio_pagination() { + the_posts_pagination( + array( + 'mid_size' => 1, + 'prev_text' => sprintf( ' %s', esc_html__( 'Anteriores', 'gustavoo-portfolio' ) ), + 'next_text' => sprintf( '%s ', esc_html__( 'Próximos', 'gustavoo-portfolio' ) ), + 'screen_reader_text' => __( 'Navegação entre páginas', 'gustavoo-portfolio' ), + ) + ); +} diff --git a/wp-content/themes/gustavoo-portfolio/inc/widgets.php b/wp-content/themes/gustavoo-portfolio/inc/widgets.php new file mode 100644 index 0000000..533f719 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/inc/widgets.php @@ -0,0 +1,204 @@ + '
    ', + 'after_widget' => '
    ', + 'before_title' => '

    ', + 'after_title' => '

    ', + ); + + register_sidebar( + array_merge( + $shared, + array( + 'name' => __( 'Barra lateral do blog', 'gustavoo-portfolio' ), + 'id' => 'sidebar-blog', + 'description' => __( 'Widgets exibidos ao lado de posts, arquivos e buscas.', 'gustavoo-portfolio' ), + ) + ) + ); + + register_sidebar( + array_merge( + $shared, + array( + 'name' => __( 'Rodapé — coluna 1', 'gustavoo-portfolio' ), + 'id' => 'footer-1', + 'description' => __( 'Primeira coluna de widgets do rodapé.', 'gustavoo-portfolio' ), + ) + ) + ); + + register_sidebar( + array_merge( + $shared, + array( + 'name' => __( 'Rodapé — coluna 2', 'gustavoo-portfolio' ), + 'id' => 'footer-2', + 'description' => __( 'Segunda coluna de widgets do rodapé.', 'gustavoo-portfolio' ), + ) + ) + ); +} +add_action( 'widgets_init', 'gustavoo_portfolio_register_sidebars' ); + +/** + * Newsletter widget backed by the same safe Fluent Forms renderer as the CTA. + */ +class Gustavao_Portfolio_Newsletter_Widget extends WP_Widget { + /** + * Register the widget with WordPress. + */ + public function __construct() { + parent::__construct( + 'gustavoo_portfolio_newsletter', + __( 'Gustavo — Newsletter', 'gustavoo-portfolio' ), + array( + 'classname' => 'widget_newsletter', + 'description' => __( 'Exibe a chamada de newsletter integrada ao Fluent Forms e FluentCRM.', 'gustavoo-portfolio' ), + 'customize_selective_refresh' => true, + ) + ); + } + + /** + * Render widget front end. + * + * @param array $args Sidebar wrappers. + * @param array $instance Saved instance. + * @return void + */ + public function widget( $args, $instance ) { + $title = ! empty( $instance['title'] ) ? $instance['title'] : gustavoo_portfolio_get_setting( 'newsletter_heading' ); + $text = ! empty( $instance['text'] ) ? $instance['text'] : gustavoo_portfolio_get_setting( 'newsletter_text' ); + $form_id = ! empty( $instance['form_id'] ) ? absint( $instance['form_id'] ) : absint( gustavoo_portfolio_get_setting( 'newsletter_form_id' ) ); + + echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + + if ( $title ) { + echo $args['before_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + echo esc_html( $title ); + echo $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + } + + if ( $text ) { + printf( '

    %s

    ', esc_html( $text ) ); + } + + gustavoo_portfolio_the_fluent_form( $form_id, 'newsletter' ); + + echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper. + } + + /** + * Render widget controls. + * + * @param array $instance Saved instance. + * @return void + */ + public function form( $instance ) { + $title = isset( $instance['title'] ) ? (string) $instance['title'] : ''; + $text = isset( $instance['text'] ) ? (string) $instance['text'] : ''; + $form_id = isset( $instance['form_id'] ) ? absint( $instance['form_id'] ) : 0; + ?> +

    + + +

    +

    + + +

    +

    + + +

    + $new_instance New values. + * @param array $old_instance Previous values. + * @return array + */ + public function update( $new_instance, $old_instance ) { + unset( $old_instance ); + + return array( + 'title' => isset( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '', + 'text' => isset( $new_instance['text'] ) ? sanitize_textarea_field( $new_instance['text'] ) : '', + 'form_id' => isset( $new_instance['form_id'] ) ? absint( $new_instance['form_id'] ) : 0, + ); + } +} + +/** + * Register the newsletter widget after core has loaded WP_Widget. + * + * @return void + */ +function gustavoo_portfolio_register_widgets() { + register_widget( 'Gustavao_Portfolio_Newsletter_Widget' ); +} +add_action( 'widgets_init', 'gustavoo_portfolio_register_widgets' ); + +/** + * Add one newsletter widget to the blog sidebar on first theme activation. + * + * Existing widget arrangements are preserved and the operation is idempotent. + * + * @return void + */ +function gustavoo_portfolio_seed_newsletter_widget() { + $widget_id_base = 'gustavoo_portfolio_newsletter'; + $instances = get_option( 'widget_' . $widget_id_base, array() ); + $instances = is_array( $instances ) ? $instances : array(); + $sidebars = wp_get_sidebars_widgets(); + $sidebars = is_array( $sidebars ) ? $sidebars : array(); + + foreach ( $sidebars as $widgets ) { + foreach ( (array) $widgets as $widget_id ) { + if ( str_starts_with( (string) $widget_id, $widget_id_base . '-' ) ) { + return; + } + } + } + + $indexes = array_filter( array_keys( $instances ), 'is_int' ); + $index = $indexes ? max( $indexes ) + 1 : 1; + + $instances[ $index ] = array( + 'title' => __( 'Newsletter', 'gustavoo-portfolio' ), + 'text' => __( 'Receba novos artigos sobre desenvolvimento, infraestrutura e automação.', 'gustavoo-portfolio' ), + 'form_id' => 0, + ); + $instances['_multiwidget'] = 1; + + $sidebars['sidebar-blog'] = array_values( + array_merge( + array( $widget_id_base . '-' . $index ), + isset( $sidebars['sidebar-blog'] ) ? (array) $sidebars['sidebar-blog'] : array() + ) + ); + + update_option( 'widget_' . $widget_id_base, $instances, false ); + update_option( 'sidebars_widgets', $sidebars, false ); +} +add_action( 'after_switch_theme', 'gustavoo_portfolio_seed_newsletter_widget' ); diff --git a/wp-content/themes/gustavoo-portfolio/index.php b/wp-content/themes/gustavoo-portfolio/index.php new file mode 100644 index 0000000..d636941 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/index.php @@ -0,0 +1,41 @@ + + +
    +
    +
    +

    +

    +
    +
    + +
    +
    + +
    + +
    + + + + +
    + + +
    +
    + + + +
    +
    + +
    +
    + + + +
    + + +
    +
    + +

    + found_posts, 'gustavoo-portfolio' ) ), + esc_html( number_format_i18n( (int) $wp_query->found_posts ) ) + ); + ?> +

    +
    + +
    + + + + +
    + + +
    +
    + + + diff --git a/wp-content/themes/gustavoo-portfolio/sidebar.php b/wp-content/themes/gustavoo-portfolio/sidebar.php new file mode 100644 index 0000000..2d7f3b6 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/sidebar.php @@ -0,0 +1,35 @@ + + diff --git a/wp-content/themes/gustavoo-portfolio/single.php b/wp-content/themes/gustavoo-portfolio/single.php new file mode 100644 index 0000000..95b6e47 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/single.php @@ -0,0 +1,138 @@ + +
    +
    +
    + + + + + + ', '' ); ?> +
    +
    + + + + +
    + 'entry-featured-image__image' ) ); ?> +
    + + +
    +
    > +
    + + '', + ) + ); + ?> +
    + +
    + +
    + + + +
    +

    + +
    + + + wp_list_pluck( $gustavoo_categories, 'term_id' ), + 'post__not_in' => array( get_the_ID() ), + 'posts_per_page' => 3, + 'ignore_sticky_posts' => true, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + ?> + have_posts() ) : ?> + + + + + + + + + + + + +
    + + +
    +
    + + +
    +

    + +

    + + +

    + +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/content/content-page.php b/wp-content/themes/gustavoo-portfolio/template-parts/content/content-page.php new file mode 100644 index 0000000..35e92d6 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/content/content-page.php @@ -0,0 +1,32 @@ + + +
    > +
    +

    + ', '' ); ?> +
    + + +
    + 'page-article__image' ) ); ?> +
    + + +
    + + '', + ) + ); + ?> +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/front/about.php b/wp-content/themes/gustavoo-portfolio/template-parts/front/about.php new file mode 100644 index 0000000..89692d5 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/front/about.php @@ -0,0 +1,41 @@ + + +
    +
    +
    +

    +

    +
    + +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/front/contact.php b/wp-content/themes/gustavoo-portfolio/template-parts/front/contact.php new file mode 100644 index 0000000..4a6097a --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/front/contact.php @@ -0,0 +1,46 @@ + + +
    +
    +
    +

    +

    +

    + + +
      + +
    • + +
    + + + +
    + +
    + + +

    + Política de Privacidade.', 'gustavoo-portfolio' ) ), + esc_url( $gustavoo_privacy_url ) + ); + ?> +

    + +
    +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/front/hero.php b/wp-content/themes/gustavoo-portfolio/template-parts/front/hero.php new file mode 100644 index 0000000..23720b8 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/front/hero.php @@ -0,0 +1,54 @@ + + +
    +
    +
    + +

    + + +

    +

    + +
    + + + + + + + + + + + +
    + +
    + + +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/front/posts.php b/wp-content/themes/gustavoo-portfolio/template-parts/front/posts.php new file mode 100644 index 0000000..56bcd49 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/front/posts.php @@ -0,0 +1,47 @@ + 'post', + 'post_status' => 'publish', + 'posts_per_page' => $gustavoo_blog_limit, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + ) +); +?> + +
    +
    +
    +
    +

    +

    +
    +

    +
    + + have_posts() ) : ?> + + +

    + +

    + + + +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/front/projects.php b/wp-content/themes/gustavoo-portfolio/template-parts/front/projects.php new file mode 100644 index 0000000..a3cbde1 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/front/projects.php @@ -0,0 +1,49 @@ + + +
    +
    +
    +
    +

    +

    +
    +

    +
    + + +
    + +
    + +
    +

    + + + +
    + + + +

    + +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/front/services.php b/wp-content/themes/gustavoo-portfolio/template-parts/front/services.php new file mode 100644 index 0000000..aa2239c --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/front/services.php @@ -0,0 +1,31 @@ + + +
    +
    +
    +
    +

    +

    +
    +

    +
    + +
    + +
    + +

    +

    +
    + +
    +
    +
    diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-card.php b/wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-card.php new file mode 100644 index 0000000..cb4c40e --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-card.php @@ -0,0 +1,22 @@ + + diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-list.php b/wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-list.php new file mode 100644 index 0000000..23cf870 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-list.php @@ -0,0 +1,47 @@ + 'post', + 'post_status' => 'publish', + 'posts_per_page' => 4, + 'post__not_in' => array_map( 'absint', $gustavoo_args['exclude'] ?? array() ), + 'ignore_sticky_posts' => true, + 'orderby' => 'date', + 'order' => 'DESC', +); + +if ( ! empty( $gustavoo_args['category_id'] ) ) { + $gustavoo_query_args['cat'] = absint( $gustavoo_args['category_id'] ); +} + +$gustavoo_latest_posts = new WP_Query( $gustavoo_query_args ); + +if ( ! $gustavoo_latest_posts->have_posts() ) { + return; +} +?> +
    +

    +
    + have_posts() ) : $gustavoo_latest_posts->the_post(); ?> + + + +
    +
    + diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/global/newsletter-cta.php b/wp-content/themes/gustavoo-portfolio/template-parts/global/newsletter-cta.php new file mode 100644 index 0000000..7b10d51 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/global/newsletter-cta.php @@ -0,0 +1,35 @@ + + + diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/global/post-card.php b/wp-content/themes/gustavoo-portfolio/template-parts/global/post-card.php new file mode 100644 index 0000000..006c9e9 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/global/post-card.php @@ -0,0 +1,45 @@ + + + diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/global/project-card.php b/wp-content/themes/gustavoo-portfolio/template-parts/global/project-card.php new file mode 100644 index 0000000..5d46659 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/global/project-card.php @@ -0,0 +1,84 @@ + + + diff --git a/wp-content/themes/gustavoo-portfolio/template-parts/global/whatsapp-float.php b/wp-content/themes/gustavoo-portfolio/template-parts/global/whatsapp-float.php new file mode 100644 index 0000000..c0994a1 --- /dev/null +++ b/wp-content/themes/gustavoo-portfolio/template-parts/global/whatsapp-float.php @@ -0,0 +1,8 @@ +