From 2df14d57dd6bd38a586b82bdf4b79e5d41a0560f Mon Sep 17 00:00:00 2001 From: gustavooth Date: Thu, 23 Jul 2026 20:32:08 -0300 Subject: [PATCH] first commit --- .gitignore | 41 + README.md | 121 + compose.yaml | 46 + wp-content/plugins/.gitkeep | 1 + .../gustavoo-portfolio-core.php | 90 + .../includes/class-gso-newsletter-widget.php | 110 + .../includes/class-gso-portfolio-core.php | 89 + .../includes/class-gso-portfolio-settings.php | 459 ++ .../includes/class-gso-projects.php | 563 +++ .../includes/class-gso-seeder.php | 619 +++ wp-content/themes/.gitkeep | 1 + wp-content/themes/gustavoo-portfolio/404.php | 57 + .../themes/gustavoo-portfolio/archive.php | 44 + .../gustavoo-portfolio/assets/css/editor.css | 57 + .../gustavoo-portfolio/assets/css/main.css | 4131 +++++++++++++++++ .../assets/fonts/INTER-LICENSE.txt | 93 + .../assets/fonts/inter-variable.woff2 | Bin 0 -> 326628 bytes .../assets/images/favicon-32.png | Bin 0 -> 1612 bytes .../assets/images/hero-art.png | Bin 0 -> 504026 bytes .../assets/images/hero-art.webp | Bin 0 -> 69010 bytes .../assets/images/logo-mark.png | Bin 0 -> 39473 bytes .../assets/images/logo-mark.webp | Bin 0 -> 19922 bytes .../assets/images/site-icon-192.png | Bin 0 -> 9755 bytes .../assets/images/site-icon-512.png | Bin 0 -> 39473 bytes .../assets/js/navigation.js | 273 ++ .../themes/gustavoo-portfolio/category.php | 34 + .../themes/gustavoo-portfolio/comments.php | 65 + .../themes/gustavoo-portfolio/footer.php | 66 + .../themes/gustavoo-portfolio/front-page.php | 41 + .../themes/gustavoo-portfolio/functions.php | 30 + .../themes/gustavoo-portfolio/header.php | 70 + wp-content/themes/gustavoo-portfolio/home.php | 38 + .../gustavoo-portfolio/inc/customizer.php | 212 + .../gustavoo-portfolio/inc/defaults.php | 173 + .../themes/gustavoo-portfolio/inc/enqueue.php | 75 + .../themes/gustavoo-portfolio/inc/forms.php | 247 + .../themes/gustavoo-portfolio/inc/setup.php | 146 + .../gustavoo-portfolio/inc/template-tags.php | 569 +++ .../themes/gustavoo-portfolio/inc/widgets.php | 204 + .../themes/gustavoo-portfolio/index.php | 41 + wp-content/themes/gustavoo-portfolio/page.php | 27 + .../themes/gustavoo-portfolio/screenshot.png | Bin 0 -> 973260 bytes .../themes/gustavoo-portfolio/search.php | 61 + .../themes/gustavoo-portfolio/searchform.php | 23 + .../themes/gustavoo-portfolio/sidebar.php | 35 + .../themes/gustavoo-portfolio/single.php | 138 + .../themes/gustavoo-portfolio/style.css | 13 + .../template-parts/content/content-none.php | 17 + .../template-parts/content/content-page.php | 32 + .../template-parts/front/about.php | 41 + .../template-parts/front/contact.php | 46 + .../template-parts/front/hero.php | 54 + .../template-parts/front/posts.php | 47 + .../template-parts/front/projects.php | 49 + .../template-parts/front/services.php | 31 + .../global/latest-news-card.php | 22 + .../global/latest-news-list.php | 47 + .../template-parts/global/newsletter-cta.php | 35 + .../template-parts/global/post-card.php | 45 + .../template-parts/global/project-card.php | 84 + .../template-parts/global/whatsapp-float.php | 8 + .../themes/gustavoo-portfolio/theme.json | 119 + 62 files changed, 9780 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 compose.yaml create mode 100644 wp-content/plugins/.gitkeep create mode 100644 wp-content/plugins/gustavoo-portfolio-core/gustavoo-portfolio-core.php create mode 100644 wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-newsletter-widget.php create mode 100644 wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-portfolio-core.php create mode 100644 wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-portfolio-settings.php create mode 100644 wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-projects.php create mode 100644 wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-seeder.php create mode 100644 wp-content/themes/.gitkeep create mode 100644 wp-content/themes/gustavoo-portfolio/404.php create mode 100644 wp-content/themes/gustavoo-portfolio/archive.php create mode 100644 wp-content/themes/gustavoo-portfolio/assets/css/editor.css create mode 100644 wp-content/themes/gustavoo-portfolio/assets/css/main.css create mode 100644 wp-content/themes/gustavoo-portfolio/assets/fonts/INTER-LICENSE.txt create mode 100644 wp-content/themes/gustavoo-portfolio/assets/fonts/inter-variable.woff2 create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/favicon-32.png create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/hero-art.png create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/hero-art.webp create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.png create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/logo-mark.webp create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/site-icon-192.png create mode 100644 wp-content/themes/gustavoo-portfolio/assets/images/site-icon-512.png create mode 100644 wp-content/themes/gustavoo-portfolio/assets/js/navigation.js create mode 100644 wp-content/themes/gustavoo-portfolio/category.php create mode 100644 wp-content/themes/gustavoo-portfolio/comments.php create mode 100644 wp-content/themes/gustavoo-portfolio/footer.php create mode 100644 wp-content/themes/gustavoo-portfolio/front-page.php create mode 100644 wp-content/themes/gustavoo-portfolio/functions.php create mode 100644 wp-content/themes/gustavoo-portfolio/header.php create mode 100644 wp-content/themes/gustavoo-portfolio/home.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/customizer.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/defaults.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/enqueue.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/forms.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/setup.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/template-tags.php create mode 100644 wp-content/themes/gustavoo-portfolio/inc/widgets.php create mode 100644 wp-content/themes/gustavoo-portfolio/index.php create mode 100644 wp-content/themes/gustavoo-portfolio/page.php create mode 100644 wp-content/themes/gustavoo-portfolio/screenshot.png create mode 100644 wp-content/themes/gustavoo-portfolio/search.php create mode 100644 wp-content/themes/gustavoo-portfolio/searchform.php create mode 100644 wp-content/themes/gustavoo-portfolio/sidebar.php create mode 100644 wp-content/themes/gustavoo-portfolio/single.php create mode 100644 wp-content/themes/gustavoo-portfolio/style.css create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/content/content-none.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/content/content-page.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/front/about.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/front/contact.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/front/hero.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/front/posts.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/front/projects.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/front/services.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-card.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/global/latest-news-list.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/global/newsletter-cta.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/global/post-card.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/global/project-card.php create mode 100644 wp-content/themes/gustavoo-portfolio/template-parts/global/whatsapp-float.php create mode 100644 wp-content/themes/gustavoo-portfolio/theme.json 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